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",
"version": "1.13.0",
"version": "1.15.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "my-weekly-todo-list",
"version": "1.13.0",
"version": "1.15.0",
"license": "MIT",
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",

View File

@ -1,6 +1,6 @@
{
"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",
"main": "index.js",
"scripts": {

View File

@ -1815,7 +1815,7 @@ export default function WeeklyView() {
const populatedLists = fetchedLists.map((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
@ -3349,6 +3349,14 @@ export default function WeeklyView() {
dayOfWeek?: number,
slot?: string,
) => {
// Reject someday list drags on day slots
if (draggingListId) {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "none";
}
return;
}
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "move";
@ -5473,36 +5481,52 @@ export default function WeeklyView() {
<div
className={`weekly-someday-lists-grid cols-${Math.min(7, Math.max(1, viewDays))}`}
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
? somedayLists
: [{ id: "default", title: "LISTE", tasks: [] as Task[] }];
const sliced = 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;
return baseLists.slice(0, Math.max(somedayLists.length, viewDays));
})()
.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
key={list.id}
className={`weekly-someday-list ${draggingListId === list.id ? "is-dragging" : ""} p-2 transition-colors duration-200`}
style={{
minHeight: "200px",
cursor: "text", // Indicate actionable area
cursor: "text",
display: "flex",
flexDirection: "column",
}}
onMouseDown={(e) => {
// Only focus bottom input if clicking background area, not slots
const target = e.target as HTMLElement;
if (
target.closest(".task-list-slot") ||
@ -5523,11 +5547,9 @@ export default function WeeklyView() {
draggable
onDragStart={(e) => {
const target = e.target as HTMLElement;
// Allow task items to be dragged freely
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) {
e.preventDefault();
return;
@ -5541,19 +5563,15 @@ export default function WeeklyView() {
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
if (!draggingListId) return;
const container = somedayGridRef.current;
if (!container) return;
const children = Array.from(container.children) as HTMLElement[];
let targetIdx = children.length - 1;
for (let i = 0; i < children.length; i++) {
const rect = children[i].getBoundingClientRect();
if (e.clientX < rect.left + rect.width / 2) {
targetIdx = i;
break;
}
if (!draggingListId || draggingListId === list.id) return;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const midX = rect.left + rect.width / 2;
// Drop before or after this list
if (e.clientX < midX) {
setDropTargetListIndex(listIdx);
} else {
setDropTargetListIndex(listIdx + 1);
}
setDropTargetListIndex(targetIdx);
}}
onDrop={async (e) => {
e.preventDefault();
@ -5586,18 +5604,22 @@ export default function WeeklyView() {
setDropTargetListIndex(null);
return;
}
const draggedIndex = somedayLists.findIndex(
const dragIdx = somedayLists.findIndex(
(l) => l.id === droppedListId,
);
if (draggedIndex === -1) {
if (dragIdx === -1) {
setDraggingListId(null);
setDropTargetListIndex(null);
return;
}
const newLists = [...somedayLists];
const [draggedItem] = newLists.splice(draggedIndex, 1);
newLists.splice(dropTargetListIndex, 0, draggedItem);
const [moved] = newLists.splice(dragIdx, 1);
// Adjust target index since we removed an item before it
const insertIdx = dragIdx < dropTargetListIndex
? dropTargetListIndex - 1
: dropTargetListIndex;
newLists.splice(insertIdx, 0, moved);
setSomedayLists(newLists);
setDraggingListId(null);
@ -5916,7 +5938,9 @@ export default function WeeklyView() {
})()}
</div>
</div>
))}
);
return [indicator, listEl, endIndicator].filter(Boolean);
})}
{/* Modal for adding lists if needed, or just rely on placeholders */}
{isAddingSomedayList && (
@ -6968,11 +6992,21 @@ function TaskItem({
}}
/>
)}
{needsSync && (
<span title="Needs to be synced" className="text-yellow-500 flex-shrink-0">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38" />
</svg>
{task.externalProvider && (
<span
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}`}
className="flex-shrink-0"
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
@ -7134,7 +7168,8 @@ function TaskItem({
</button>
)}
{/* Recurrence */}
{/* Recurrence (hidden for someday tasks) */}
{!isSomeday && (
<button
className={`task-action-btn ${task.isRecurring ? "active" : ""}`}
onClick={(e) => {
@ -7159,6 +7194,7 @@ function TaskItem({
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
)}
{/* Notes */}
<button
@ -7288,6 +7324,24 @@ function TaskItem({
</svg>
</button>
</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>

File diff suppressed because one or more lines are too long