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:
parent
b56bfdeb52
commit
1334656300
@ -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": {
|
||||
|
||||
@ -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,38 +3323,85 @@ 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.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
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));
|
||||
setImportStatusMsg({
|
||||
type: "success",
|
||||
text: `Stopped syncing "${list.title}".`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete list", error);
|
||||
setImportStatusMsg({
|
||||
type: "error",
|
||||
text: "Failed to stop syncing list.",
|
||||
});
|
||||
}
|
||||
// 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",
|
||||
});
|
||||
if (res.ok) {
|
||||
setSomedayLists((prev) => prev.filter((l) => l.id !== existing.id));
|
||||
setImportStatusMsg({
|
||||
type: "success",
|
||||
text: `Stopped syncing "${list.title}".`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete list", error);
|
||||
setImportStatusMsg({
|
||||
type: "error",
|
||||
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 {
|
||||
// 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).`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Core import logic — accepts provider directly so it works both from modal and sidebar
|
||||
const doImport = async (
|
||||
provider: "google" | "apple" | "outlook" | "synology",
|
||||
@ -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,22 +11213,121 @@ 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",
|
||||
padding: "2px 8px",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
color: "var(--weekly-text-light, #888)",
|
||||
textTransform: "uppercase",
|
||||
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",
|
||||
padding: "2px 8px",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
color: "var(--weekly-text-light, #888)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
gap: "8px",
|
||||
padding: "8px 10px",
|
||||
borderRadius: "6px",
|
||||
background: "#fef2f2",
|
||||
border: "1px solid #fecaca",
|
||||
fontSize: "0.82rem",
|
||||
color: "#991b1b",
|
||||
}}>
|
||||
<span style={{ flex: 1 }}>List</span>
|
||||
<span style={{ width: "50px", textAlign: "center" }}>Sync</span>
|
||||
<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) =>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user