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:
parent
76445bffc7
commit
9cde2e4de2
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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
Loading…
Reference in New Issue
Block a user