feat: inline unsync confirmation and sync all/unsync all buttons

Replace browser confirm() popup when unchecking synced task lists with
an inline red confirmation banner. Add sync all / unsync all buttons
per provider for batch operations. i18n for EN, DE, FR, ES, IT.

v1.26.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-11 08:31:53 +01:00
parent b56bfdeb52
commit 1334656300
2 changed files with 231 additions and 37 deletions

View File

@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
"version": "1.25.2",
"version": "1.26.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js",
"scripts": {

View File

@ -345,6 +345,11 @@ const translations: Record<string, any> = {
connectOutlook: "Connect Outlook",
syncTasks: "Sync Tasks",
syncTasksDesc: "Sync tasks with Google Tasks or Microsoft To-Do.",
unsyncConfirmMsg: "Stop syncing \"{title}\"? Its tasks will be moved to trash.",
unsyncConfirm: "Stop syncing",
unsyncCancel: "Cancel",
syncAll: "Sync all",
unsyncAll: "Unsync all",
fetchingLists: "(fetching lists...)",
listHeader: "List",
syncHeader: "Sync",
@ -533,6 +538,11 @@ const translations: Record<string, any> = {
connectOutlook: "Outlook verbinden",
syncTasks: "Aufgaben synchronisieren",
syncTasksDesc: "Aufgaben mit Google Tasks oder Microsoft To-Do synchronisieren.",
unsyncConfirmMsg: "Synchronisierung von \"{title}\" beenden? Die Aufgaben werden in den Papierkorb verschoben.",
unsyncConfirm: "Sync beenden",
unsyncCancel: "Abbrechen",
syncAll: "Alle synchronisieren",
unsyncAll: "Alle trennen",
fetchingLists: "(Listen werden geladen...)",
listHeader: "Liste",
syncHeader: "Sync",
@ -720,6 +730,11 @@ const translations: Record<string, any> = {
connectOutlook: "Connecter Outlook",
syncTasks: "Synchroniser les tâches",
syncTasksDesc: "Synchronisez les tâches avec Google Tasks ou Microsoft To-Do.",
unsyncConfirmMsg: "Arrêter la synchronisation de \"{title}\" ? Ses tâches seront mises à la corbeille.",
unsyncConfirm: "Arrêter la sync",
unsyncCancel: "Annuler",
syncAll: "Tout synchroniser",
unsyncAll: "Tout désynchroniser",
fetchingLists: "(chargement des listes...)",
listHeader: "Liste",
syncHeader: "Sync",
@ -907,6 +922,11 @@ const translations: Record<string, any> = {
connectOutlook: "Conectar Outlook",
syncTasks: "Sincronizar tareas",
syncTasksDesc: "Sincroniza tareas con Google Tasks o Microsoft To-Do.",
unsyncConfirmMsg: "¿Dejar de sincronizar \"{title}\"? Sus tareas se moverán a la papelera.",
unsyncConfirm: "Dejar de sincronizar",
unsyncCancel: "Cancelar",
syncAll: "Sincronizar todo",
unsyncAll: "Desincronizar todo",
fetchingLists: "(cargando listas...)",
listHeader: "Lista",
syncHeader: "Sync",
@ -1094,6 +1114,11 @@ const translations: Record<string, any> = {
connectOutlook: "Connetti Outlook",
syncTasks: "Sincronizza attività",
syncTasksDesc: "Sincronizza le attività con Google Tasks o Microsoft To-Do.",
unsyncConfirmMsg: "Interrompere la sincronizzazione di \"{title}\"? Le attività verranno spostate nel cestino.",
unsyncConfirm: "Interrompi sync",
unsyncCancel: "Annulla",
syncAll: "Sincronizza tutto",
unsyncAll: "Desincronizza tutto",
fetchingLists: "(caricamento liste...)",
listHeader: "Lista",
syncHeader: "Sync",
@ -1541,6 +1566,10 @@ export default function WeeklyView() {
type: "success" | "error";
text: string;
} | null>(null);
const [unsyncConfirm, setUnsyncConfirm] = useState<{
provider: "google" | "apple" | "outlook" | "synology";
list: { id: string; title: string };
} | null>(null);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [importProvider, setImportProvider] = useState<
"google" | "apple" | "outlook" | "synology" | null
@ -3294,14 +3323,22 @@ export default function WeeklyView() {
);
if (existing) {
// Unsync/Remove
if (
!confirm(
`Are you sure you want to stop syncing the list "${list.title}"? This will move its tasks to the trash.`,
)
) {
// Show inline confirmation instead of browser confirm()
setUnsyncConfirm({ provider, list });
return;
} else {
// Sync/Import
await doImport(provider, [list]);
}
};
const confirmUnsync = async () => {
if (!unsyncConfirm) return;
const { provider, list } = unsyncConfirm;
const existing = somedayLists.find(
(l) => l.externalId === list.id && l.externalProvider === provider,
);
if (!existing) { setUnsyncConfirm(null); return; }
try {
const res = await fetch(`/api/someday-lists?id=${existing.id}`, {
method: "DELETE",
@ -3320,9 +3357,48 @@ export default function WeeklyView() {
text: "Failed to stop syncing list.",
});
}
setUnsyncConfirm(null);
};
const handleSyncAll = async (
provider: "google" | "outlook" | "synology",
lists: { id: string; title: string }[],
syncOn: boolean,
) => {
if (syncOn) {
const unsynced = lists.filter(
(list) => !somedayLists.some(
(sl) => sl.externalId === list.id && sl.externalProvider === provider,
),
);
if (unsynced.length > 0) await doImport(provider, unsynced);
} else {
// Sync/Import
await doImport(provider, [list]);
// Unsync all synced lists
const synced = lists.filter(
(list) => somedayLists.some(
(sl) => sl.externalId === list.id && sl.externalProvider === provider,
),
);
for (const list of synced) {
const existing = somedayLists.find(
(l) => l.externalId === list.id && l.externalProvider === provider,
);
if (!existing) continue;
try {
const res = await fetch(`/api/someday-lists?id=${existing.id}`, {
method: "DELETE",
});
if (res.ok) {
setSomedayLists((prev) => prev.filter((l) => l.id !== existing.id));
}
} catch (error) {
console.error("Failed to unsync list", error);
}
}
setImportStatusMsg({
type: "success",
text: `Stopped syncing ${synced.length} list(s).`,
});
}
};
@ -7543,6 +7619,10 @@ export default function WeeklyView() {
isFetchingProviderLists={isFetchingProviderLists}
somedayLists={somedayLists}
handleToggleTaskList={handleToggleTaskList}
unsyncConfirm={unsyncConfirm}
onConfirmUnsync={confirmUnsync}
onCancelUnsync={() => setUnsyncConfirm(null)}
handleSyncAll={handleSyncAll}
fetchAvailableTaskLists={fetchAvailableTaskLists}
setCurrentWeekStart={setCurrentWeekStart}
projects={projects}
@ -8823,6 +8903,17 @@ interface SettingsSidebarProps {
provider: "google" | "apple" | "outlook" | "synology",
list: { id: string; title: string },
) => Promise<void>;
unsyncConfirm: {
provider: "google" | "apple" | "outlook" | "synology";
list: { id: string; title: string };
} | null;
onConfirmUnsync: () => Promise<void>;
onCancelUnsync: () => void;
handleSyncAll: (
provider: "google" | "outlook" | "synology",
lists: { id: string; title: string }[],
syncOn: boolean,
) => Promise<void>;
fetchAvailableTaskLists: (
provider: "google" | "apple" | "outlook" | "synology",
) => Promise<void>;
@ -9038,6 +9129,10 @@ function SettingsSidebar({
isFetchingProviderLists,
somedayLists,
handleToggleTaskList,
unsyncConfirm,
onConfirmUnsync,
onCancelUnsync,
handleSyncAll,
fetchAvailableTaskLists,
initialTab,
setCurrentWeekStart,
@ -11118,8 +11213,19 @@ function SettingsSidebar({
gap: "4px",
}}
>
{/* Column header */}
{providerLists.length > 0 && (
{/* Column header with sync all / unsync all */}
{providerLists.length > 0 && (() => {
const allSynced = providerLists.every(
(list: { id: string; title: string }) => somedayLists.some(
(sl: SomedayList) => sl.externalId === list.id && sl.externalProvider === conn.provider,
),
);
const noneSynced = providerLists.every(
(list: { id: string; title: string }) => !somedayLists.some(
(sl: SomedayList) => sl.externalId === list.id && sl.externalProvider === conn.provider,
),
);
return (
<div style={{
display: "flex",
alignItems: "center",
@ -11131,9 +11237,97 @@ function SettingsSidebar({
letterSpacing: "0.05em",
}}>
<span style={{ flex: 1 }}>List</span>
{!allSynced && (
<button
onClick={() => handleSyncAll(
conn.provider as "google" | "outlook" | "synology",
providerLists,
true,
)}
disabled={importingTasksState}
style={{
background: "none",
border: "none",
cursor: "pointer",
fontSize: "0.7rem",
color: "var(--weekly-accent, #6366f1)",
padding: "2px 6px",
fontWeight: 500,
textTransform: "none",
}}
>{t.syncAll}</button>
)}
{!noneSynced && (
<button
onClick={() => handleSyncAll(
conn.provider as "google" | "outlook" | "synology",
providerLists,
false,
)}
disabled={importingTasksState}
style={{
background: "none",
border: "none",
cursor: "pointer",
fontSize: "0.7rem",
color: "#ef4444",
padding: "2px 6px",
fontWeight: 500,
textTransform: "none",
}}
>{t.unsyncAll}</button>
)}
<span style={{ width: "50px", textAlign: "center" }}>Sync</span>
</div>
);
})()}
{/* Inline unsync confirmation */}
{unsyncConfirm && unsyncConfirm.provider === conn.provider && (
<div style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "8px 10px",
borderRadius: "6px",
background: "#fef2f2",
border: "1px solid #fecaca",
fontSize: "0.82rem",
color: "#991b1b",
}}>
<span style={{ flex: 1 }}>
{t.unsyncConfirmMsg.replace("{title}", unsyncConfirm.list.title)}
</span>
<button
onClick={onConfirmUnsync}
style={{
background: "#ef4444",
color: "#fff",
border: "none",
borderRadius: "4px",
padding: "4px 10px",
cursor: "pointer",
fontSize: "0.8rem",
fontWeight: 600,
whiteSpace: "nowrap",
}}
>{t.unsyncConfirm}</button>
<button
onClick={onCancelUnsync}
style={{
background: "none",
border: "1px solid #d1d5db",
borderRadius: "4px",
padding: "4px 10px",
cursor: "pointer",
fontSize: "0.8rem",
color: "#666",
whiteSpace: "nowrap",
}}
>{t.unsyncCancel}</button>
</div>
)}
{providerLists.map((list: { id: string; title: string }) => {
const isSynced = somedayLists.some(
(sl: SomedayList) =>