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",
|
||||
"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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user