feat: provider logos on synced tasks, someday list drag & drop fix, hide recurrence for someday

- Show provider-specific icons (Google, Microsoft, Apple, Synology) before and after synced task titles
- Fix someday list reorder drag & drop: stable index calculation with drop indicator line
- Prevent dropping someday lists onto day slots (shows not-allowed cursor)
- Hide recurring button for someday tasks
- Fix Google Tasks subtask duplicates by filtering out child tasks from top-level list

v1.15.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-07 21:20:27 +01:00
parent 76445bffc7
commit 9cde2e4de2
4 changed files with 100 additions and 46 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.13.0", "version": "1.15.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.13.0", "version": "1.15.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.11.1", "@auth/prisma-adapter": "^2.11.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.14.1", "version": "1.15.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": {

View File

@ -1815,7 +1815,7 @@ export default function WeeklyView() {
const populatedLists = fetchedLists.map((list) => ({ const populatedLists = fetchedLists.map((list) => ({
...list, ...list,
tasks: somedayTasks.filter((t: Task) => t.somedayListId === list.id), tasks: somedayTasks.filter((t: Task) => t.somedayListId === list.id && !t.parentTaskId),
})); }));
// Rescue orphaned someday tasks: if their list was deleted, move them to calendar // Rescue orphaned someday tasks: if their list was deleted, move them to calendar
@ -3349,6 +3349,14 @@ export default function WeeklyView() {
dayOfWeek?: number, dayOfWeek?: number,
slot?: string, slot?: string,
) => { ) => {
// Reject someday list drags on day slots
if (draggingListId) {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "none";
}
return;
}
e.preventDefault(); e.preventDefault();
if (e.dataTransfer) { if (e.dataTransfer) {
e.dataTransfer.dropEffect = "move"; e.dataTransfer.dropEffect = "move";
@ -5473,36 +5481,52 @@ export default function WeeklyView() {
<div <div
className={`weekly-someday-lists-grid cols-${Math.min(7, Math.max(1, viewDays))}`} className={`weekly-someday-lists-grid cols-${Math.min(7, Math.max(1, viewDays))}`}
style={{ display: "flex", flexDirection: "row", flexWrap: "nowrap" }} style={{ display: "flex", flexDirection: "row", flexWrap: "nowrap" }}
onDragLeave={(e) => {
// Clear indicator when leaving the someday grid entirely
if (draggingListId && !e.currentTarget.contains(e.relatedTarget as Node)) {
setDropTargetListIndex(null);
}
}}
> >
{(() => { {(() => {
const baseLists = somedayLists.length > 0 const baseLists = somedayLists.length > 0
? somedayLists ? somedayLists
: [{ id: "default", title: "LISTE", tasks: [] as Task[] }]; : [{ id: "default", title: "LISTE", tasks: [] as Task[] }];
const sliced = baseLists.slice(0, Math.max(somedayLists.length, viewDays)); return baseLists.slice(0, Math.max(somedayLists.length, viewDays));
// Compute visual order during drag
if (draggingListId && dropTargetListIndex !== null) {
const dragIdx = sliced.findIndex(l => l.id === draggingListId);
if (dragIdx !== -1 && dragIdx !== dropTargetListIndex) {
const reordered = [...sliced];
const [moved] = reordered.splice(dragIdx, 1);
reordered.splice(dropTargetListIndex, 0, moved);
return reordered;
}
}
return sliced;
})() })()
.map((list) => ( .flatMap((list, listIdx, arr) => {
const indicator = draggingListId && dropTargetListIndex === listIdx && draggingListId !== list.id ? (
<div key={`drop-indicator-${listIdx}`} style={{
width: "3px",
flexShrink: 0,
background: "#6366f1",
borderRadius: "2px",
alignSelf: "stretch",
transition: "opacity 0.15s",
}} />
) : null;
// After last item, check for drop at end
const endIndicator = listIdx === arr.length - 1 && draggingListId && dropTargetListIndex === arr.length && draggingListId !== list.id ? (
<div key="drop-indicator-end" style={{
width: "3px",
flexShrink: 0,
background: "#6366f1",
borderRadius: "2px",
alignSelf: "stretch",
transition: "opacity 0.15s",
}} />
) : null;
const listEl = (
<div <div
key={list.id} key={list.id}
className={`weekly-someday-list ${draggingListId === list.id ? "is-dragging" : ""} p-2 transition-colors duration-200`} className={`weekly-someday-list ${draggingListId === list.id ? "is-dragging" : ""} p-2 transition-colors duration-200`}
style={{ style={{
minHeight: "200px", minHeight: "200px",
cursor: "text", // Indicate actionable area cursor: "text",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}} }}
onMouseDown={(e) => { onMouseDown={(e) => {
// Only focus bottom input if clicking background area, not slots
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if ( if (
target.closest(".task-list-slot") || target.closest(".task-list-slot") ||
@ -5523,11 +5547,9 @@ export default function WeeklyView() {
draggable draggable
onDragStart={(e) => { onDragStart={(e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
// Allow task items to be dragged freely
if (target.closest(".weekly-task-item")) { if (target.closest(".weekly-task-item")) {
return; // Let the TaskItem handle its own drag return;
} }
// Only allow list drag if started from the handle
if (!isDragFromHandle.current) { if (!isDragFromHandle.current) {
e.preventDefault(); e.preventDefault();
return; return;
@ -5541,19 +5563,15 @@ export default function WeeklyView() {
onDragOver={(e) => { onDragOver={(e) => {
e.preventDefault(); e.preventDefault();
e.dataTransfer.dropEffect = "move"; e.dataTransfer.dropEffect = "move";
if (!draggingListId) return; if (!draggingListId || draggingListId === list.id) return;
const container = somedayGridRef.current; const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
if (!container) return; const midX = rect.left + rect.width / 2;
const children = Array.from(container.children) as HTMLElement[]; // Drop before or after this list
let targetIdx = children.length - 1; if (e.clientX < midX) {
for (let i = 0; i < children.length; i++) { setDropTargetListIndex(listIdx);
const rect = children[i].getBoundingClientRect(); } else {
if (e.clientX < rect.left + rect.width / 2) { setDropTargetListIndex(listIdx + 1);
targetIdx = i;
break;
}
} }
setDropTargetListIndex(targetIdx);
}} }}
onDrop={async (e) => { onDrop={async (e) => {
e.preventDefault(); e.preventDefault();
@ -5586,18 +5604,22 @@ export default function WeeklyView() {
setDropTargetListIndex(null); setDropTargetListIndex(null);
return; return;
} }
const draggedIndex = somedayLists.findIndex( const dragIdx = somedayLists.findIndex(
(l) => l.id === droppedListId, (l) => l.id === droppedListId,
); );
if (draggedIndex === -1) { if (dragIdx === -1) {
setDraggingListId(null); setDraggingListId(null);
setDropTargetListIndex(null); setDropTargetListIndex(null);
return; return;
} }
const newLists = [...somedayLists]; const newLists = [...somedayLists];
const [draggedItem] = newLists.splice(draggedIndex, 1); const [moved] = newLists.splice(dragIdx, 1);
newLists.splice(dropTargetListIndex, 0, draggedItem); // Adjust target index since we removed an item before it
const insertIdx = dragIdx < dropTargetListIndex
? dropTargetListIndex - 1
: dropTargetListIndex;
newLists.splice(insertIdx, 0, moved);
setSomedayLists(newLists); setSomedayLists(newLists);
setDraggingListId(null); setDraggingListId(null);
@ -5916,7 +5938,9 @@ export default function WeeklyView() {
})()} })()}
</div> </div>
</div> </div>
))} );
return [indicator, listEl, endIndicator].filter(Boolean);
})}
{/* Modal for adding lists if needed, or just rely on placeholders */} {/* Modal for adding lists if needed, or just rely on placeholders */}
{isAddingSomedayList && ( {isAddingSomedayList && (
@ -6968,11 +6992,21 @@ function TaskItem({
}} }}
/> />
)} )}
{needsSync && ( {task.externalProvider && (
<span title="Needs to be synced" className="text-yellow-500 flex-shrink-0"> <span
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> title={`Synced with ${task.externalProvider === "outlook" ? "Microsoft To Do" : task.externalProvider === "google" ? "Google Tasks" : task.externalProvider === "apple" ? "Apple Reminders" : task.externalProvider === "synology" ? "Synology Tasks" : task.externalProvider}`}
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38" /> className="flex-shrink-0"
</svg> style={{ display: "inline-flex", alignItems: "center", opacity: needsSync ? 0.5 : 0.6, fontSize: "10px" }}
>
{task.externalProvider === "google" ? (
<FontAwesomeIcon icon={faGoogle} style={{ width: 11, height: 11, color: "#4285F4" }} />
) : task.externalProvider === "outlook" ? (
<FontAwesomeIcon icon={faMicrosoft} style={{ width: 11, height: 11, color: "#0078D4" }} />
) : task.externalProvider === "apple" ? (
<FontAwesomeIcon icon={faApple} style={{ width: 11, height: 11, color: "#555" }} />
) : task.externalProvider === "synology" ? (
<FontAwesomeIcon icon={faServer} style={{ width: 11, height: 11, color: "#007AFF" }} />
) : null}
</span> </span>
)} )}
<span <span
@ -7134,7 +7168,8 @@ function TaskItem({
</button> </button>
)} )}
{/* Recurrence */} {/* Recurrence (hidden for someday tasks) */}
{!isSomeday && (
<button <button
className={`task-action-btn ${task.isRecurring ? "active" : ""}`} className={`task-action-btn ${task.isRecurring ? "active" : ""}`}
onClick={(e) => { onClick={(e) => {
@ -7159,6 +7194,7 @@ function TaskItem({
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg> </svg>
</button> </button>
)}
{/* Notes */} {/* Notes */}
<button <button
@ -7288,6 +7324,24 @@ function TaskItem({
</svg> </svg>
</button> </button>
</div> </div>
{/* Provider icon at far right */}
{task.externalProvider && (
<span
className="flex-shrink-0"
style={{ display: "inline-flex", alignItems: "center", marginLeft: "4px", opacity: 0.45, pointerEvents: "none" }}
>
{task.externalProvider === "google" ? (
<FontAwesomeIcon icon={faGoogle} style={{ width: 10, height: 10, color: "#4285F4" }} />
) : task.externalProvider === "outlook" ? (
<FontAwesomeIcon icon={faMicrosoft} style={{ width: 10, height: 10, color: "#0078D4" }} />
) : task.externalProvider === "apple" ? (
<FontAwesomeIcon icon={faApple} style={{ width: 10, height: 10, color: "#555" }} />
) : task.externalProvider === "synology" ? (
<FontAwesomeIcon icon={faServer} style={{ width: 10, height: 10, color: "#007AFF" }} />
) : null}
</span>
)}
</> </>
)} )}
</div> </div>

File diff suppressed because one or more lines are too long