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",
|
"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",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -345,6 +345,11 @@ const translations: Record<string, any> = {
|
|||||||
connectOutlook: "Connect Outlook",
|
connectOutlook: "Connect Outlook",
|
||||||
syncTasks: "Sync Tasks",
|
syncTasks: "Sync Tasks",
|
||||||
syncTasksDesc: "Sync tasks with Google Tasks or Microsoft To-Do.",
|
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...)",
|
fetchingLists: "(fetching lists...)",
|
||||||
listHeader: "List",
|
listHeader: "List",
|
||||||
syncHeader: "Sync",
|
syncHeader: "Sync",
|
||||||
@ -533,6 +538,11 @@ const translations: Record<string, any> = {
|
|||||||
connectOutlook: "Outlook verbinden",
|
connectOutlook: "Outlook verbinden",
|
||||||
syncTasks: "Aufgaben synchronisieren",
|
syncTasks: "Aufgaben synchronisieren",
|
||||||
syncTasksDesc: "Aufgaben mit Google Tasks oder Microsoft To-Do 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...)",
|
fetchingLists: "(Listen werden geladen...)",
|
||||||
listHeader: "Liste",
|
listHeader: "Liste",
|
||||||
syncHeader: "Sync",
|
syncHeader: "Sync",
|
||||||
@ -720,6 +730,11 @@ const translations: Record<string, any> = {
|
|||||||
connectOutlook: "Connecter Outlook",
|
connectOutlook: "Connecter Outlook",
|
||||||
syncTasks: "Synchroniser les tâches",
|
syncTasks: "Synchroniser les tâches",
|
||||||
syncTasksDesc: "Synchronisez les tâches avec Google Tasks ou Microsoft To-Do.",
|
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...)",
|
fetchingLists: "(chargement des listes...)",
|
||||||
listHeader: "Liste",
|
listHeader: "Liste",
|
||||||
syncHeader: "Sync",
|
syncHeader: "Sync",
|
||||||
@ -907,6 +922,11 @@ const translations: Record<string, any> = {
|
|||||||
connectOutlook: "Conectar Outlook",
|
connectOutlook: "Conectar Outlook",
|
||||||
syncTasks: "Sincronizar tareas",
|
syncTasks: "Sincronizar tareas",
|
||||||
syncTasksDesc: "Sincroniza tareas con Google Tasks o Microsoft To-Do.",
|
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...)",
|
fetchingLists: "(cargando listas...)",
|
||||||
listHeader: "Lista",
|
listHeader: "Lista",
|
||||||
syncHeader: "Sync",
|
syncHeader: "Sync",
|
||||||
@ -1094,6 +1114,11 @@ const translations: Record<string, any> = {
|
|||||||
connectOutlook: "Connetti Outlook",
|
connectOutlook: "Connetti Outlook",
|
||||||
syncTasks: "Sincronizza attività",
|
syncTasks: "Sincronizza attività",
|
||||||
syncTasksDesc: "Sincronizza le attività con Google Tasks o Microsoft To-Do.",
|
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...)",
|
fetchingLists: "(caricamento liste...)",
|
||||||
listHeader: "Lista",
|
listHeader: "Lista",
|
||||||
syncHeader: "Sync",
|
syncHeader: "Sync",
|
||||||
@ -1541,6 +1566,10 @@ export default function WeeklyView() {
|
|||||||
type: "success" | "error";
|
type: "success" | "error";
|
||||||
text: string;
|
text: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [unsyncConfirm, setUnsyncConfirm] = useState<{
|
||||||
|
provider: "google" | "apple" | "outlook" | "synology";
|
||||||
|
list: { id: string; title: string };
|
||||||
|
} | null>(null);
|
||||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||||
const [importProvider, setImportProvider] = useState<
|
const [importProvider, setImportProvider] = useState<
|
||||||
"google" | "apple" | "outlook" | "synology" | null
|
"google" | "apple" | "outlook" | "synology" | null
|
||||||
@ -3294,38 +3323,85 @@ export default function WeeklyView() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Unsync/Remove
|
// Show inline confirmation instead of browser confirm()
|
||||||
if (
|
setUnsyncConfirm({ provider, list });
|
||||||
!confirm(
|
return;
|
||||||
`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.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Sync/Import
|
// Sync/Import
|
||||||
await doImport(provider, [list]);
|
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
|
// Core import logic — accepts provider directly so it works both from modal and sidebar
|
||||||
const doImport = async (
|
const doImport = async (
|
||||||
provider: "google" | "apple" | "outlook" | "synology",
|
provider: "google" | "apple" | "outlook" | "synology",
|
||||||
@ -7543,6 +7619,10 @@ export default function WeeklyView() {
|
|||||||
isFetchingProviderLists={isFetchingProviderLists}
|
isFetchingProviderLists={isFetchingProviderLists}
|
||||||
somedayLists={somedayLists}
|
somedayLists={somedayLists}
|
||||||
handleToggleTaskList={handleToggleTaskList}
|
handleToggleTaskList={handleToggleTaskList}
|
||||||
|
unsyncConfirm={unsyncConfirm}
|
||||||
|
onConfirmUnsync={confirmUnsync}
|
||||||
|
onCancelUnsync={() => setUnsyncConfirm(null)}
|
||||||
|
handleSyncAll={handleSyncAll}
|
||||||
fetchAvailableTaskLists={fetchAvailableTaskLists}
|
fetchAvailableTaskLists={fetchAvailableTaskLists}
|
||||||
setCurrentWeekStart={setCurrentWeekStart}
|
setCurrentWeekStart={setCurrentWeekStart}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
@ -8823,6 +8903,17 @@ interface SettingsSidebarProps {
|
|||||||
provider: "google" | "apple" | "outlook" | "synology",
|
provider: "google" | "apple" | "outlook" | "synology",
|
||||||
list: { id: string; title: string },
|
list: { id: string; title: string },
|
||||||
) => Promise<void>;
|
) => 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: (
|
fetchAvailableTaskLists: (
|
||||||
provider: "google" | "apple" | "outlook" | "synology",
|
provider: "google" | "apple" | "outlook" | "synology",
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
@ -9038,6 +9129,10 @@ function SettingsSidebar({
|
|||||||
isFetchingProviderLists,
|
isFetchingProviderLists,
|
||||||
somedayLists,
|
somedayLists,
|
||||||
handleToggleTaskList,
|
handleToggleTaskList,
|
||||||
|
unsyncConfirm,
|
||||||
|
onConfirmUnsync,
|
||||||
|
onCancelUnsync,
|
||||||
|
handleSyncAll,
|
||||||
fetchAvailableTaskLists,
|
fetchAvailableTaskLists,
|
||||||
initialTab,
|
initialTab,
|
||||||
setCurrentWeekStart,
|
setCurrentWeekStart,
|
||||||
@ -11118,22 +11213,121 @@ function SettingsSidebar({
|
|||||||
gap: "4px",
|
gap: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Column header */}
|
{/* Column header with sync all / unsync all */}
|
||||||
{providerLists.length > 0 && (
|
{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={{
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "2px 8px",
|
gap: "8px",
|
||||||
fontSize: "0.75rem",
|
padding: "8px 10px",
|
||||||
fontWeight: 600,
|
borderRadius: "6px",
|
||||||
color: "var(--weekly-text-light, #888)",
|
background: "#fef2f2",
|
||||||
textTransform: "uppercase",
|
border: "1px solid #fecaca",
|
||||||
letterSpacing: "0.05em",
|
fontSize: "0.82rem",
|
||||||
|
color: "#991b1b",
|
||||||
}}>
|
}}>
|
||||||
<span style={{ flex: 1 }}>List</span>
|
<span style={{ flex: 1 }}>
|
||||||
<span style={{ width: "50px", textAlign: "center" }}>Sync</span>
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{providerLists.map((list: { id: string; title: string }) => {
|
{providerLists.map((list: { id: string; title: string }) => {
|
||||||
const isSynced = somedayLists.some(
|
const isSynced = somedayLists.some(
|
||||||
(sl: SomedayList) =>
|
(sl: SomedayList) =>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user