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",
|
||||
"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",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -1548,6 +1548,67 @@ h3 {
|
||||
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 {
|
||||
appearance: none;
|
||||
|
||||
@ -261,6 +261,7 @@ const translations: Record<string, any> = {
|
||||
newTabName: "New tab name:",
|
||||
assignTab: "Assign to tab",
|
||||
renameTab: "Double-click to rename",
|
||||
dissolveTab: "Remove tab (keep lists)",
|
||||
loading: "Loading your tasks...",
|
||||
sycing: "Syncing...",
|
||||
synced: "Synced",
|
||||
@ -448,6 +449,7 @@ const translations: Record<string, any> = {
|
||||
newTabName: "Neuer Tab-Name:",
|
||||
assignTab: "Tab zuweisen",
|
||||
renameTab: "Doppelklick zum Umbenennen",
|
||||
dissolveTab: "Tab entfernen (Listen behalten)",
|
||||
loading: "Lade Aufgaben...",
|
||||
syncing: "Synchronisiere...",
|
||||
synced: "Synchronisiert",
|
||||
@ -634,6 +636,7 @@ const translations: Record<string, any> = {
|
||||
newTabName: "Nom du nouvel onglet :",
|
||||
assignTab: "Assigner à un onglet",
|
||||
renameTab: "Double-cliquez pour renommer",
|
||||
dissolveTab: "Supprimer l'onglet (garder les listes)",
|
||||
loading: "Chargement de vos tâches…",
|
||||
sycing: "Synchronisation…",
|
||||
synced: "Synchronisé",
|
||||
@ -820,6 +823,7 @@ const translations: Record<string, any> = {
|
||||
newTabName: "Nombre de nueva pestaña:",
|
||||
assignTab: "Asignar a pestaña",
|
||||
renameTab: "Doble clic para renombrar",
|
||||
dissolveTab: "Eliminar pestaña (mantener listas)",
|
||||
loading: "Cargando tus tareas…",
|
||||
sycing: "Sincronizando…",
|
||||
synced: "Sincronizado",
|
||||
@ -1006,6 +1010,7 @@ const translations: Record<string, any> = {
|
||||
newTabName: "Nome nuova scheda:",
|
||||
assignTab: "Assegna a scheda",
|
||||
renameTab: "Doppio clic per rinominare",
|
||||
dissolveTab: "Rimuovi scheda (mantieni liste)",
|
||||
loading: "Caricamento delle attività…",
|
||||
sycing: "Sincronizzazione…",
|
||||
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(() => {
|
||||
if (activeSomedayTab === null) return somedayLists;
|
||||
return somedayLists.filter(l => (l.tab || null) === activeSomedayTab);
|
||||
@ -2613,7 +2635,8 @@ export default function WeeklyView() {
|
||||
data.lists.map((l: any) => ({
|
||||
id: l.id,
|
||||
title: l.title,
|
||||
tasks: l.tasks || [], // Tasks will be overwritten/populated by fetchTasks
|
||||
tab: l.tab || null,
|
||||
tasks: l.tasks || [],
|
||||
externalId: l.externalId || null,
|
||||
externalProvider: l.externalProvider || null,
|
||||
})),
|
||||
@ -6386,8 +6409,8 @@ export default function WeeklyView() {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div key={tab} className="someday-tab-wrapper">
|
||||
<button
|
||||
key={tab}
|
||||
className={`someday-tab-btn ${activeSomedayTab === tab ? "active" : ""}`}
|
||||
onClick={() => setSomedayTab(tab)}
|
||||
onDoubleClick={() => {
|
||||
@ -6398,6 +6421,17 @@ export default function WeeklyView() {
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
<button
|
||||
className="someday-tab-dissolve"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dissolveTab(tab);
|
||||
}}
|
||||
title={t.dissolveTab}
|
||||
>
|
||||
<X size={8} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
@ -6473,8 +6507,8 @@ export default function WeeklyView() {
|
||||
onClick={() => setSomedayTab(null)}
|
||||
>{t.allTabs}</button>
|
||||
{somedayTabs.map(tab => (
|
||||
<div key={tab} className="someday-tab-wrapper-h">
|
||||
<button
|
||||
key={tab}
|
||||
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""}`}
|
||||
onClick={() => setSomedayTab(tab)}
|
||||
onDoubleClick={() => {
|
||||
@ -6482,6 +6516,12 @@ export default function WeeklyView() {
|
||||
setRenamingTabValue(tab);
|
||||
}}
|
||||
>{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)}
|
||||
>{t.allTabs}</button>
|
||||
{somedayTabs.map(tab => (
|
||||
<div key={tab} className="someday-tab-wrapper-h">
|
||||
<button
|
||||
key={tab}
|
||||
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""}`}
|
||||
onClick={() => setSomedayTab(tab)}
|
||||
>{tab}</button>
|
||||
<button
|
||||
className="someday-tab-dissolve-h"
|
||||
onClick={(e) => { e.stopPropagation(); dissolveTab(tab); }}
|
||||
title={t.dissolveTab}
|
||||
><X size={10} /></button>
|
||||
</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
|
||||
* @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[]> => {
|
||||
try {
|
||||
const client = createClient(serverUrl, username, password);
|
||||
await client.login();
|
||||
await loginClient(client, serverUrl, username);
|
||||
|
||||
const calendars = await client.fetchCalendars();
|
||||
|
||||
@ -91,7 +121,7 @@ export const getUpcomingEvents = async (
|
||||
): Promise<SynologyCalendarEvent[]> => {
|
||||
try {
|
||||
const client = createClient(serverUrl, username, password);
|
||||
await client.login();
|
||||
await loginClient(client, serverUrl, username);
|
||||
|
||||
const calendars = await client.fetchCalendars();
|
||||
const getPath = (url: string) => {
|
||||
@ -264,7 +294,7 @@ export const createEvent = async (
|
||||
): Promise<SynologyCalendarEvent> => {
|
||||
try {
|
||||
const client = createClient(serverUrl, username, password);
|
||||
await client.login();
|
||||
await loginClient(client, serverUrl, username);
|
||||
|
||||
const calendars = await client.fetchCalendars();
|
||||
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||
@ -381,7 +411,7 @@ export const updateEvent = async (
|
||||
): Promise<SynologyCalendarEvent> => {
|
||||
try {
|
||||
const client = createClient(serverUrl, username, password);
|
||||
await client.login();
|
||||
await loginClient(client, serverUrl, username);
|
||||
|
||||
let targetObject: any = null;
|
||||
|
||||
@ -504,7 +534,7 @@ export const deleteEvent = async (
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const client = createClient(serverUrl, username, password);
|
||||
await client.login();
|
||||
await loginClient(client, serverUrl, username);
|
||||
|
||||
const parsed = parseCaldavId(eventId);
|
||||
if (parsed) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user