fix: tab persistence, dissolve tabs, and Synology CalDAV discovery fallback
- Fix someday tabs disappearing on reload (include tab field in fetch mapping) - Add dissolve/delete tab feature with X button on hover (all 5 languages) - Fix Synology calendar "cannot find homeUrl" by falling back to derived homeUrl when well-known CalDAV discovery fails v1.25.2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ff6ed1f949
commit
b56bfdeb52
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.25.1",
|
"version": "1.25.2",
|
||||||
"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": {
|
||||||
|
|||||||
@ -1548,6 +1548,67 @@ h3 {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Someday Tab Wrappers with dissolve buttons */
|
||||||
|
.someday-tab-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.someday-tab-dissolve {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: -2px;
|
||||||
|
background: var(--weekly-bg, #fff);
|
||||||
|
border: 1px solid var(--weekly-border, #e5e7eb);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--weekly-text-light, #999);
|
||||||
|
padding: 1px;
|
||||||
|
line-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.someday-tab-wrapper:hover .someday-tab-dissolve {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.someday-tab-dissolve:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.someday-tab-wrapper-h {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.someday-tab-dissolve-h {
|
||||||
|
position: absolute;
|
||||||
|
right: -4px;
|
||||||
|
top: -4px;
|
||||||
|
background: var(--weekly-bg, #fff);
|
||||||
|
border: 1px solid var(--weekly-border, #e5e7eb);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--weekly-text-light, #999);
|
||||||
|
padding: 1px;
|
||||||
|
line-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.someday-tab-wrapper-h:hover .someday-tab-dissolve-h {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.someday-tab-dissolve-h:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
/* Someday Tab Select (in list headers) */
|
/* Someday Tab Select (in list headers) */
|
||||||
.someday-tab-select {
|
.someday-tab-select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|||||||
@ -261,6 +261,7 @@ const translations: Record<string, any> = {
|
|||||||
newTabName: "New tab name:",
|
newTabName: "New tab name:",
|
||||||
assignTab: "Assign to tab",
|
assignTab: "Assign to tab",
|
||||||
renameTab: "Double-click to rename",
|
renameTab: "Double-click to rename",
|
||||||
|
dissolveTab: "Remove tab (keep lists)",
|
||||||
loading: "Loading your tasks...",
|
loading: "Loading your tasks...",
|
||||||
sycing: "Syncing...",
|
sycing: "Syncing...",
|
||||||
synced: "Synced",
|
synced: "Synced",
|
||||||
@ -448,6 +449,7 @@ const translations: Record<string, any> = {
|
|||||||
newTabName: "Neuer Tab-Name:",
|
newTabName: "Neuer Tab-Name:",
|
||||||
assignTab: "Tab zuweisen",
|
assignTab: "Tab zuweisen",
|
||||||
renameTab: "Doppelklick zum Umbenennen",
|
renameTab: "Doppelklick zum Umbenennen",
|
||||||
|
dissolveTab: "Tab entfernen (Listen behalten)",
|
||||||
loading: "Lade Aufgaben...",
|
loading: "Lade Aufgaben...",
|
||||||
syncing: "Synchronisiere...",
|
syncing: "Synchronisiere...",
|
||||||
synced: "Synchronisiert",
|
synced: "Synchronisiert",
|
||||||
@ -634,6 +636,7 @@ const translations: Record<string, any> = {
|
|||||||
newTabName: "Nom du nouvel onglet :",
|
newTabName: "Nom du nouvel onglet :",
|
||||||
assignTab: "Assigner à un onglet",
|
assignTab: "Assigner à un onglet",
|
||||||
renameTab: "Double-cliquez pour renommer",
|
renameTab: "Double-cliquez pour renommer",
|
||||||
|
dissolveTab: "Supprimer l'onglet (garder les listes)",
|
||||||
loading: "Chargement de vos tâches…",
|
loading: "Chargement de vos tâches…",
|
||||||
sycing: "Synchronisation…",
|
sycing: "Synchronisation…",
|
||||||
synced: "Synchronisé",
|
synced: "Synchronisé",
|
||||||
@ -820,6 +823,7 @@ const translations: Record<string, any> = {
|
|||||||
newTabName: "Nombre de nueva pestaña:",
|
newTabName: "Nombre de nueva pestaña:",
|
||||||
assignTab: "Asignar a pestaña",
|
assignTab: "Asignar a pestaña",
|
||||||
renameTab: "Doble clic para renombrar",
|
renameTab: "Doble clic para renombrar",
|
||||||
|
dissolveTab: "Eliminar pestaña (mantener listas)",
|
||||||
loading: "Cargando tus tareas…",
|
loading: "Cargando tus tareas…",
|
||||||
sycing: "Sincronizando…",
|
sycing: "Sincronizando…",
|
||||||
synced: "Sincronizado",
|
synced: "Sincronizado",
|
||||||
@ -1006,6 +1010,7 @@ const translations: Record<string, any> = {
|
|||||||
newTabName: "Nome nuova scheda:",
|
newTabName: "Nome nuova scheda:",
|
||||||
assignTab: "Assegna a scheda",
|
assignTab: "Assegna a scheda",
|
||||||
renameTab: "Doppio clic per rinominare",
|
renameTab: "Doppio clic per rinominare",
|
||||||
|
dissolveTab: "Rimuovi scheda (mantieni liste)",
|
||||||
loading: "Caricamento delle attività…",
|
loading: "Caricamento delle attività…",
|
||||||
sycing: "Sincronizzazione…",
|
sycing: "Sincronizzazione…",
|
||||||
synced: "Sincronizzato",
|
synced: "Sincronizzato",
|
||||||
@ -1481,6 +1486,23 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dissolveTab = async (tabName: string) => {
|
||||||
|
const listsToUpdate = somedayLists.filter(l => l.tab === tabName);
|
||||||
|
setSomedayLists(prev => prev.map(l => l.tab === tabName ? { ...l, tab: null } : l));
|
||||||
|
if (activeSomedayTab === tabName) setSomedayTab(null);
|
||||||
|
for (const list of listsToUpdate) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/someday-lists", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ id: list.id, tab: null }),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to dissolve tab for list:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredSomedayLists = useMemo(() => {
|
const filteredSomedayLists = useMemo(() => {
|
||||||
if (activeSomedayTab === null) return somedayLists;
|
if (activeSomedayTab === null) return somedayLists;
|
||||||
return somedayLists.filter(l => (l.tab || null) === activeSomedayTab);
|
return somedayLists.filter(l => (l.tab || null) === activeSomedayTab);
|
||||||
@ -2613,7 +2635,8 @@ export default function WeeklyView() {
|
|||||||
data.lists.map((l: any) => ({
|
data.lists.map((l: any) => ({
|
||||||
id: l.id,
|
id: l.id,
|
||||||
title: l.title,
|
title: l.title,
|
||||||
tasks: l.tasks || [], // Tasks will be overwritten/populated by fetchTasks
|
tab: l.tab || null,
|
||||||
|
tasks: l.tasks || [],
|
||||||
externalId: l.externalId || null,
|
externalId: l.externalId || null,
|
||||||
externalProvider: l.externalProvider || null,
|
externalProvider: l.externalProvider || null,
|
||||||
})),
|
})),
|
||||||
@ -6386,18 +6409,29 @@ export default function WeeklyView() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<div key={tab} className="someday-tab-wrapper">
|
||||||
key={tab}
|
<button
|
||||||
className={`someday-tab-btn ${activeSomedayTab === tab ? "active" : ""}`}
|
className={`someday-tab-btn ${activeSomedayTab === tab ? "active" : ""}`}
|
||||||
onClick={() => setSomedayTab(tab)}
|
onClick={() => setSomedayTab(tab)}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
setEditingTabName(tab);
|
setEditingTabName(tab);
|
||||||
setRenamingTabValue(tab);
|
setRenamingTabValue(tab);
|
||||||
}}
|
}}
|
||||||
title={t.renameTab}
|
title={t.renameTab}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="someday-tab-dissolve"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dissolveTab(tab);
|
||||||
|
}}
|
||||||
|
title={t.dissolveTab}
|
||||||
|
>
|
||||||
|
<X size={8} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -6473,15 +6507,21 @@ export default function WeeklyView() {
|
|||||||
onClick={() => setSomedayTab(null)}
|
onClick={() => setSomedayTab(null)}
|
||||||
>{t.allTabs}</button>
|
>{t.allTabs}</button>
|
||||||
{somedayTabs.map(tab => (
|
{somedayTabs.map(tab => (
|
||||||
<button
|
<div key={tab} className="someday-tab-wrapper-h">
|
||||||
key={tab}
|
<button
|
||||||
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""}`}
|
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""}`}
|
||||||
onClick={() => setSomedayTab(tab)}
|
onClick={() => setSomedayTab(tab)}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
setEditingTabName(tab);
|
setEditingTabName(tab);
|
||||||
setRenamingTabValue(tab);
|
setRenamingTabValue(tab);
|
||||||
}}
|
}}
|
||||||
>{tab}</button>
|
>{tab}</button>
|
||||||
|
<button
|
||||||
|
className="someday-tab-dissolve-h"
|
||||||
|
onClick={(e) => { e.stopPropagation(); dissolveTab(tab); }}
|
||||||
|
title={t.dissolveTab}
|
||||||
|
><X size={10} /></button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -6496,11 +6536,17 @@ export default function WeeklyView() {
|
|||||||
onClick={() => setSomedayTab(null)}
|
onClick={() => setSomedayTab(null)}
|
||||||
>{t.allTabs}</button>
|
>{t.allTabs}</button>
|
||||||
{somedayTabs.map(tab => (
|
{somedayTabs.map(tab => (
|
||||||
<button
|
<div key={tab} className="someday-tab-wrapper-h">
|
||||||
key={tab}
|
<button
|
||||||
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""}`}
|
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""}`}
|
||||||
onClick={() => setSomedayTab(tab)}
|
onClick={() => setSomedayTab(tab)}
|
||||||
>{tab}</button>
|
>{tab}</button>
|
||||||
|
<button
|
||||||
|
className="someday-tab-dissolve-h"
|
||||||
|
onClick={(e) => { e.stopPropagation(); dissolveTab(tab); }}
|
||||||
|
title={t.dissolveTab}
|
||||||
|
><X size={10} /></button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -39,6 +39,36 @@ const createClient = (serverUrl: string, username: string, password: string) =>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login with fallback: Synology's CalDAV doesn't support well-known discovery,
|
||||||
|
* so if login() fails with "cannot find homeUrl", retry with the serverUrl as homeUrl.
|
||||||
|
*/
|
||||||
|
const loginClient = async (client: DAVClient, serverUrl: string, username: string) => {
|
||||||
|
try {
|
||||||
|
await client.login();
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.message?.includes('homeUrl')) {
|
||||||
|
// Synology doesn't support .well-known discovery — derive homeUrl from serverUrl
|
||||||
|
let homeUrl = serverUrl.replace(/\/$/, '');
|
||||||
|
if (!homeUrl.includes('/caldav')) {
|
||||||
|
homeUrl = `${homeUrl}/caldav/${username}`;
|
||||||
|
}
|
||||||
|
// Strip any calendar-specific path segments (UUID), keep up to username
|
||||||
|
const match = homeUrl.match(/^(https?:\/\/[^/]+\/caldav(?:\.php)?\/[^/]+)\/?/);
|
||||||
|
if (match) homeUrl = match[1] + '/';
|
||||||
|
|
||||||
|
console.log('[SYNOLOGY CALENDAR] Well-known discovery failed, retrying with homeUrl:', homeUrl);
|
||||||
|
(client as any).account = {
|
||||||
|
serverUrl: client.serverUrl,
|
||||||
|
accountType: 'caldav',
|
||||||
|
homeUrl,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate credentials by attempting to fetch calendars
|
* Validate credentials by attempting to fetch calendars
|
||||||
* @returns List of found calendars if successful
|
* @returns List of found calendars if successful
|
||||||
@ -46,7 +76,7 @@ const createClient = (serverUrl: string, username: string, password: string) =>
|
|||||||
export const validateCredentials = async (serverUrl: string, username: string, password: string): Promise<SynologyCalendar[]> => {
|
export const validateCredentials = async (serverUrl: string, username: string, password: string): Promise<SynologyCalendar[]> => {
|
||||||
try {
|
try {
|
||||||
const client = createClient(serverUrl, username, password);
|
const client = createClient(serverUrl, username, password);
|
||||||
await client.login();
|
await loginClient(client, serverUrl, username);
|
||||||
|
|
||||||
const calendars = await client.fetchCalendars();
|
const calendars = await client.fetchCalendars();
|
||||||
|
|
||||||
@ -91,7 +121,7 @@ export const getUpcomingEvents = async (
|
|||||||
): Promise<SynologyCalendarEvent[]> => {
|
): Promise<SynologyCalendarEvent[]> => {
|
||||||
try {
|
try {
|
||||||
const client = createClient(serverUrl, username, password);
|
const client = createClient(serverUrl, username, password);
|
||||||
await client.login();
|
await loginClient(client, serverUrl, username);
|
||||||
|
|
||||||
const calendars = await client.fetchCalendars();
|
const calendars = await client.fetchCalendars();
|
||||||
const getPath = (url: string) => {
|
const getPath = (url: string) => {
|
||||||
@ -264,7 +294,7 @@ export const createEvent = async (
|
|||||||
): Promise<SynologyCalendarEvent> => {
|
): Promise<SynologyCalendarEvent> => {
|
||||||
try {
|
try {
|
||||||
const client = createClient(serverUrl, username, password);
|
const client = createClient(serverUrl, username, password);
|
||||||
await client.login();
|
await loginClient(client, serverUrl, username);
|
||||||
|
|
||||||
const calendars = await client.fetchCalendars();
|
const calendars = await client.fetchCalendars();
|
||||||
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||||
@ -381,7 +411,7 @@ export const updateEvent = async (
|
|||||||
): Promise<SynologyCalendarEvent> => {
|
): Promise<SynologyCalendarEvent> => {
|
||||||
try {
|
try {
|
||||||
const client = createClient(serverUrl, username, password);
|
const client = createClient(serverUrl, username, password);
|
||||||
await client.login();
|
await loginClient(client, serverUrl, username);
|
||||||
|
|
||||||
let targetObject: any = null;
|
let targetObject: any = null;
|
||||||
|
|
||||||
@ -504,7 +534,7 @@ export const deleteEvent = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const client = createClient(serverUrl, username, password);
|
const client = createClient(serverUrl, username, password);
|
||||||
await client.login();
|
await loginClient(client, serverUrl, username);
|
||||||
|
|
||||||
const parsed = parseCaldavId(eventId);
|
const parsed = parseCaldavId(eventId);
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user