diff --git a/artifacts/tx-os/src/components/notes/folders-rail.tsx b/artifacts/tx-os/src/components/notes/folders-rail.tsx
index 53340e60..dbad962f 100644
--- a/artifacts/tx-os/src/components/notes/folders-rail.tsx
+++ b/artifacts/tx-os/src/components/notes/folders-rail.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useDroppable } from "@dnd-kit/core";
import {
@@ -78,18 +78,27 @@ export const folderDropId = (target: "unfiled" | number) =>
// its own droppable id (one hook per row).
function DroppableRow({
target,
+ ownerId,
className,
testId,
children,
}: {
target: "unfiled" | number;
+ // When set, the drop target belongs to a shared folder owned by this
+ // user id. handleDragEnd uses this to enforce the "same-owner only"
+ // rule the server applies to editors. Leave undefined for own folders
+ // and the Unfiled bucket.
+ ownerId?: number;
className?: string;
testId?: string;
children: React.ReactNode;
}) {
const { setNodeRef, isOver } = useDroppable({
- id: folderDropId(target),
- data: { type: "folder-drop", target },
+ id:
+ ownerId === undefined
+ ? folderDropId(target)
+ : `${folderDropId(target)}-shared-${ownerId}`,
+ data: { type: "folder-drop", target, ownerId },
});
return (
);
+ return canDrop ? (
+
+ {rowInner}
+
+ ) : (
+ {rowInner}
+ );
})}
)}
diff --git a/artifacts/tx-os/src/pages/notes.tsx b/artifacts/tx-os/src/pages/notes.tsx
index b6830540..2217a06d 100644
--- a/artifacts/tx-os/src/pages/notes.tsx
+++ b/artifacts/tx-os/src/pages/notes.tsx
@@ -257,6 +257,7 @@ export default function NotesPage() {
// for shared folders. Null means "no editable shared-folder context".
const sharedFolderBucketRef = useRef<{
folderId: number;
+ ownerId: number;
notes: Note[];
} | null>(null);
@@ -348,6 +349,10 @@ export default function NotesPage() {
| {
type?: string;
target?: "unfiled" | number;
+ // Owner of the target droppable. Set ONLY for shared-folder
+ // rows; absent (undefined) on own folders + the Unfiled bucket
+ // — those targets implicitly belong to the current user.
+ ownerId?: number;
noteId?: number;
folderId?: number | null;
isPinned?: boolean;
@@ -365,7 +370,19 @@ export default function NotesPage() {
if (!isOwn && !isEditableShared) return;
if (o?.type === "folder-drop" && o.target !== undefined) {
- if (!isOwn) return;
+ // The server enforces: an editor may only move a note BETWEEN
+ // folders owned by the same owner. Mirror that constraint
+ // client-side so cross-owner drops never round-trip:
+ // • Target with no ownerId = own folder / Unfiled. Allow only
+ // for the user's OWN notes.
+ // • Target with ownerId = a shared folder we can edit.
+ // Allow only when the source note also lives in that owner's
+ // namespace (i.e. came from sharedBucket with the same owner).
+ if (o.ownerId === undefined) {
+ if (!isOwn) return;
+ } else {
+ if (!isEditableShared || sharedBucket?.ownerId !== o.ownerId) return;
+ }
moveNoteToTarget(o.target, a.noteId);
return;
}
@@ -3348,7 +3365,7 @@ function SharedFolderView({
onBack: () => void;
onEdit: (n: Note) => void;
onSend: (n: Note) => void;
- bucketRef: React.MutableRefObject<{ folderId: number; notes: Note[] } | null>;
+ bucketRef: React.MutableRefObject<{ folderId: number; ownerId: number; notes: Note[] } | null>;
}) {
const { t, i18n } = useTranslation();
const lang = i18n.language;
@@ -3364,7 +3381,11 @@ function SharedFolderView({
bucketRef.current = null;
return;
}
- bucketRef.current = { folderId, notes: data.notes };
+ bucketRef.current = {
+ folderId,
+ ownerId: data.folder.ownerId,
+ notes: data.notes,
+ };
return () => {
if (bucketRef.current?.folderId === folderId) {
bucketRef.current = null;