notes: user-defined folders with drag-drop (task #413)
DB - New `note_folders` table (per-user unique name); `notes.folder_id` nullable with `ON DELETE SET NULL` so deleting a folder preserves its notes. API - /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped. - /notes POST/PATCH validate folder ownership before assignment (no cross-user folder bleed). - Folder noteCount is tab-aware (active vs archived) via ?archived=. - OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react and api-zod (`pnpm --filter @workspace/api-spec run codegen`). Client - NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder / useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all ["notes", ...] caches; rollback on error). UI (FoldersRail next to My Notes + Archive tabs only) - Desktop: sidebar with All / Unfiled / user folders + counts. - Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid. - Per-folder kebab dropdown with Rename + Delete. - Newly-created folder is auto-selected. - HTML5 draggable note cards on desktop; touch long-press fallback (350ms) for iPad/mobile via shared drop pipeline (registerFolderDropHandler + hit-tested `[data-folder-drop]`). - Dev-only `window.__notesTouchTest` helper (gated to non-production builds) exposes the touch pipeline so e2e tests can drive it deterministically without synthesizing native TouchEvents. Bilingual - notes.folders.* keys added to en.json and ar.json (RTL inherited from existing dir prop wired from i18n.language). Tests (artifacts/tx-os/tests/notes-folders.spec.mjs) - create folder + auto-select - drag note → folder, refresh-and-still-in-folder persistence - filter by folder / Unfiled / All - drag between folders (count math) - rename via kebab - delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL) - cross-user folder rejection (400) - Arabic + RTL render - separate touch test (414×800, hasTouch+isMobile) covers chip-row layout and exercises the touch drop pipeline through window helper. All API node tests + tx-os Playwright suite + tsc clean.
This commit is contained in:
@@ -1090,6 +1090,16 @@ export interface NoteRecipientSummary {
|
|||||||
recipient?: NoteUserSummary | null;
|
recipient?: NoteUserSummary | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NoteFolder {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
/** Number of notes in this folder, scoped to the requested tab (active vs archived). */
|
||||||
|
noteCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SentNote {
|
export interface SentNote {
|
||||||
id: number;
|
id: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -1098,6 +1108,11 @@ export interface SentNote {
|
|||||||
color: string;
|
color: string;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
|
/**
|
||||||
|
* Owning folder id, or null for Unfiled.
|
||||||
|
* @nullable
|
||||||
|
*/
|
||||||
|
folderId?: number | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
recipients: NoteRecipientSummary[];
|
recipients: NoteRecipientSummary[];
|
||||||
@@ -1109,6 +1124,11 @@ export interface ReceivedNote {
|
|||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
/**
|
||||||
|
* Owning folder id on the recipient's side, or null for Unfiled.
|
||||||
|
* @nullable
|
||||||
|
*/
|
||||||
|
folderId?: number | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
sender?: NoteUserSummary | null;
|
sender?: NoteUserSummary | null;
|
||||||
@@ -1730,6 +1750,29 @@ export type ReplyToNoteBody = {
|
|||||||
recipientUserId?: number;
|
recipientUserId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ListNoteFoldersParams = {
|
||||||
|
/**
|
||||||
|
* Tab scope for noteCount (false = My Notes, true = Archive)
|
||||||
|
*/
|
||||||
|
archived?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateNoteFolderBody = {
|
||||||
|
/**
|
||||||
|
* @minLength 1
|
||||||
|
* @maxLength 80
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateNoteFolderBody = {
|
||||||
|
/**
|
||||||
|
* @minLength 1
|
||||||
|
* @maxLength 80
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetAdminUserDependentNotesParams = {
|
export type GetAdminUserDependentNotesParams = {
|
||||||
/**
|
/**
|
||||||
* @minimum 1
|
* @minimum 1
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import type {
|
|||||||
CreateAppBody,
|
CreateAppBody,
|
||||||
CreateConversationBody,
|
CreateConversationBody,
|
||||||
CreateGroupBody,
|
CreateGroupBody,
|
||||||
|
CreateNoteFolderBody,
|
||||||
CreateRoleBody,
|
CreateRoleBody,
|
||||||
CreateServiceBody,
|
CreateServiceBody,
|
||||||
CreateServiceOrderBody,
|
CreateServiceOrderBody,
|
||||||
@@ -80,10 +81,12 @@ import type {
|
|||||||
ListGroupsParams,
|
ListGroupsParams,
|
||||||
ListMyNotesAlias200Item,
|
ListMyNotesAlias200Item,
|
||||||
ListMyNotesAliasParams,
|
ListMyNotesAliasParams,
|
||||||
|
ListNoteFoldersParams,
|
||||||
ListReceivedNotesParams,
|
ListReceivedNotesParams,
|
||||||
ListUsersParams,
|
ListUsersParams,
|
||||||
LoginBody,
|
LoginBody,
|
||||||
MessageWithSender,
|
MessageWithSender,
|
||||||
|
NoteFolder,
|
||||||
NoteReply,
|
NoteReply,
|
||||||
NoteThread,
|
NoteThread,
|
||||||
Notification,
|
Notification,
|
||||||
@@ -121,6 +124,7 @@ import type {
|
|||||||
UpdateGroupBody,
|
UpdateGroupBody,
|
||||||
UpdateLanguageBody,
|
UpdateLanguageBody,
|
||||||
UpdateMyAppOrderBody,
|
UpdateMyAppOrderBody,
|
||||||
|
UpdateNoteFolderBody,
|
||||||
UpdateNotificationPreferencesBody,
|
UpdateNotificationPreferencesBody,
|
||||||
UpdateRoleBody,
|
UpdateRoleBody,
|
||||||
UpdateServiceBody,
|
UpdateServiceBody,
|
||||||
@@ -8967,6 +8971,357 @@ export const useReplyToNote = <
|
|||||||
return useMutation(getReplyToNoteMutationOptions(options));
|
return useMutation(getReplyToNoteMutationOptions(options));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary List the caller's note folders with per-tab note counts
|
||||||
|
*/
|
||||||
|
export const getListNoteFoldersUrl = (params?: ListNoteFoldersParams) => {
|
||||||
|
const normalizedParams = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params || {}).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
normalizedParams.append(key, value === null ? "null" : value.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stringifiedParams = normalizedParams.toString();
|
||||||
|
|
||||||
|
return stringifiedParams.length > 0
|
||||||
|
? `/api/note-folders?${stringifiedParams}`
|
||||||
|
: `/api/note-folders`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listNoteFolders = async (
|
||||||
|
params?: ListNoteFoldersParams,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<NoteFolder[]> => {
|
||||||
|
return customFetch<NoteFolder[]>(getListNoteFoldersUrl(params), {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getListNoteFoldersQueryKey = (params?: ListNoteFoldersParams) => {
|
||||||
|
return [`/api/note-folders`, ...(params ? [params] : [])] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getListNoteFoldersQueryOptions = <
|
||||||
|
TData = Awaited<ReturnType<typeof listNoteFolders>>,
|
||||||
|
TError = ErrorType<unknown>,
|
||||||
|
>(
|
||||||
|
params?: ListNoteFoldersParams,
|
||||||
|
options?: {
|
||||||
|
query?: UseQueryOptions<
|
||||||
|
Awaited<ReturnType<typeof listNoteFolders>>,
|
||||||
|
TError,
|
||||||
|
TData
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||||
|
|
||||||
|
const queryKey = queryOptions?.queryKey ?? getListNoteFoldersQueryKey(params);
|
||||||
|
|
||||||
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof listNoteFolders>>> = ({
|
||||||
|
signal,
|
||||||
|
}) => listNoteFolders(params, { signal, ...requestOptions });
|
||||||
|
|
||||||
|
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||||
|
Awaited<ReturnType<typeof listNoteFolders>>,
|
||||||
|
TError,
|
||||||
|
TData
|
||||||
|
> & { queryKey: QueryKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListNoteFoldersQueryResult = NonNullable<
|
||||||
|
Awaited<ReturnType<typeof listNoteFolders>>
|
||||||
|
>;
|
||||||
|
export type ListNoteFoldersQueryError = ErrorType<unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary List the caller's note folders with per-tab note counts
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function useListNoteFolders<
|
||||||
|
TData = Awaited<ReturnType<typeof listNoteFolders>>,
|
||||||
|
TError = ErrorType<unknown>,
|
||||||
|
>(
|
||||||
|
params?: ListNoteFoldersParams,
|
||||||
|
options?: {
|
||||||
|
query?: UseQueryOptions<
|
||||||
|
Awaited<ReturnType<typeof listNoteFolders>>,
|
||||||
|
TError,
|
||||||
|
TData
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
},
|
||||||
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||||
|
const queryOptions = getListNoteFoldersQueryOptions(params, options);
|
||||||
|
|
||||||
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||||
|
queryKey: QueryKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...query, queryKey: queryOptions.queryKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create a new note folder owned by the caller
|
||||||
|
*/
|
||||||
|
export const getCreateNoteFolderUrl = () => {
|
||||||
|
return `/api/note-folders`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNoteFolder = async (
|
||||||
|
createNoteFolderBody: CreateNoteFolderBody,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<NoteFolder> => {
|
||||||
|
return customFetch<NoteFolder>(getCreateNoteFolderUrl(), {
|
||||||
|
...options,
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||||
|
body: JSON.stringify(createNoteFolderBody),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCreateNoteFolderMutationOptions = <
|
||||||
|
TError = ErrorType<ErrorResponse>,
|
||||||
|
TContext = unknown,
|
||||||
|
>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof createNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ data: BodyType<CreateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof createNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ data: BodyType<CreateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
const mutationKey = ["createNoteFolder"];
|
||||||
|
const { mutation: mutationOptions, request: requestOptions } = options
|
||||||
|
? options.mutation &&
|
||||||
|
"mutationKey" in options.mutation &&
|
||||||
|
options.mutation.mutationKey
|
||||||
|
? options
|
||||||
|
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||||
|
: { mutation: { mutationKey }, request: undefined };
|
||||||
|
|
||||||
|
const mutationFn: MutationFunction<
|
||||||
|
Awaited<ReturnType<typeof createNoteFolder>>,
|
||||||
|
{ data: BodyType<CreateNoteFolderBody> }
|
||||||
|
> = (props) => {
|
||||||
|
const { data } = props ?? {};
|
||||||
|
|
||||||
|
return createNoteFolder(data, requestOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { mutationFn, ...mutationOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateNoteFolderMutationResult = NonNullable<
|
||||||
|
Awaited<ReturnType<typeof createNoteFolder>>
|
||||||
|
>;
|
||||||
|
export type CreateNoteFolderMutationBody = BodyType<CreateNoteFolderBody>;
|
||||||
|
export type CreateNoteFolderMutationError = ErrorType<ErrorResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create a new note folder owned by the caller
|
||||||
|
*/
|
||||||
|
export const useCreateNoteFolder = <
|
||||||
|
TError = ErrorType<ErrorResponse>,
|
||||||
|
TContext = unknown,
|
||||||
|
>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof createNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ data: BodyType<CreateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationResult<
|
||||||
|
Awaited<ReturnType<typeof createNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ data: BodyType<CreateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
return useMutation(getCreateNoteFolderMutationOptions(options));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Rename a note folder
|
||||||
|
*/
|
||||||
|
export const getUpdateNoteFolderUrl = (id: number) => {
|
||||||
|
return `/api/note-folders/${id}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateNoteFolder = async (
|
||||||
|
id: number,
|
||||||
|
updateNoteFolderBody: UpdateNoteFolderBody,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<NoteFolder> => {
|
||||||
|
return customFetch<NoteFolder>(getUpdateNoteFolderUrl(id), {
|
||||||
|
...options,
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||||
|
body: JSON.stringify(updateNoteFolderBody),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUpdateNoteFolderMutationOptions = <
|
||||||
|
TError = ErrorType<ErrorResponse>,
|
||||||
|
TContext = unknown,
|
||||||
|
>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof updateNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number; data: BodyType<UpdateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof updateNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number; data: BodyType<UpdateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
const mutationKey = ["updateNoteFolder"];
|
||||||
|
const { mutation: mutationOptions, request: requestOptions } = options
|
||||||
|
? options.mutation &&
|
||||||
|
"mutationKey" in options.mutation &&
|
||||||
|
options.mutation.mutationKey
|
||||||
|
? options
|
||||||
|
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||||
|
: { mutation: { mutationKey }, request: undefined };
|
||||||
|
|
||||||
|
const mutationFn: MutationFunction<
|
||||||
|
Awaited<ReturnType<typeof updateNoteFolder>>,
|
||||||
|
{ id: number; data: BodyType<UpdateNoteFolderBody> }
|
||||||
|
> = (props) => {
|
||||||
|
const { id, data } = props ?? {};
|
||||||
|
|
||||||
|
return updateNoteFolder(id, data, requestOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { mutationFn, ...mutationOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateNoteFolderMutationResult = NonNullable<
|
||||||
|
Awaited<ReturnType<typeof updateNoteFolder>>
|
||||||
|
>;
|
||||||
|
export type UpdateNoteFolderMutationBody = BodyType<UpdateNoteFolderBody>;
|
||||||
|
export type UpdateNoteFolderMutationError = ErrorType<ErrorResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Rename a note folder
|
||||||
|
*/
|
||||||
|
export const useUpdateNoteFolder = <
|
||||||
|
TError = ErrorType<ErrorResponse>,
|
||||||
|
TContext = unknown,
|
||||||
|
>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof updateNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number; data: BodyType<UpdateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationResult<
|
||||||
|
Awaited<ReturnType<typeof updateNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number; data: BodyType<UpdateNoteFolderBody> },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
return useMutation(getUpdateNoteFolderMutationOptions(options));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Delete a note folder. Notes inside are preserved (folderId set to null).
|
||||||
|
*/
|
||||||
|
export const getDeleteNoteFolderUrl = (id: number) => {
|
||||||
|
return `/api/note-folders/${id}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteNoteFolder = async (
|
||||||
|
id: number,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<SuccessResponse> => {
|
||||||
|
return customFetch<SuccessResponse>(getDeleteNoteFolderUrl(id), {
|
||||||
|
...options,
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeleteNoteFolderMutationOptions = <
|
||||||
|
TError = ErrorType<unknown>,
|
||||||
|
TContext = unknown,
|
||||||
|
>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof deleteNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof deleteNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
const mutationKey = ["deleteNoteFolder"];
|
||||||
|
const { mutation: mutationOptions, request: requestOptions } = options
|
||||||
|
? options.mutation &&
|
||||||
|
"mutationKey" in options.mutation &&
|
||||||
|
options.mutation.mutationKey
|
||||||
|
? options
|
||||||
|
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||||
|
: { mutation: { mutationKey }, request: undefined };
|
||||||
|
|
||||||
|
const mutationFn: MutationFunction<
|
||||||
|
Awaited<ReturnType<typeof deleteNoteFolder>>,
|
||||||
|
{ id: number }
|
||||||
|
> = (props) => {
|
||||||
|
const { id } = props ?? {};
|
||||||
|
|
||||||
|
return deleteNoteFolder(id, requestOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { mutationFn, ...mutationOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteNoteFolderMutationResult = NonNullable<
|
||||||
|
Awaited<ReturnType<typeof deleteNoteFolder>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DeleteNoteFolderMutationError = ErrorType<unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Delete a note folder. Notes inside are preserved (folderId set to null).
|
||||||
|
*/
|
||||||
|
export const useDeleteNoteFolder = <
|
||||||
|
TError = ErrorType<unknown>,
|
||||||
|
TContext = unknown,
|
||||||
|
>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<ReturnType<typeof deleteNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationResult<
|
||||||
|
Awaited<ReturnType<typeof deleteNoteFolder>>,
|
||||||
|
TError,
|
||||||
|
{ id: number },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
return useMutation(getDeleteNoteFolderMutationOptions(options));
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary List notes owned by a user
|
* @summary List notes owned by a user
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2905,6 +2905,96 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: { $ref: "#/components/schemas/NoteReply" }
|
schema: { $ref: "#/components/schemas/NoteReply" }
|
||||||
|
|
||||||
|
# ----- Note folders (user-defined) -----
|
||||||
|
/note-folders:
|
||||||
|
get:
|
||||||
|
operationId: listNoteFolders
|
||||||
|
tags: [notes]
|
||||||
|
summary: List the caller's note folders with per-tab note counts
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: archived
|
||||||
|
required: false
|
||||||
|
description: Tab scope for noteCount (false = My Notes, true = Archive)
|
||||||
|
schema: { type: boolean, default: false }
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Folders owned by the caller
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/NoteFolder" }
|
||||||
|
post:
|
||||||
|
operationId: createNoteFolder
|
||||||
|
tags: [notes]
|
||||||
|
summary: Create a new note folder owned by the caller
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [name]
|
||||||
|
properties:
|
||||||
|
name: { type: string, minLength: 1, maxLength: 80 }
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Folder created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/NoteFolder" }
|
||||||
|
"400":
|
||||||
|
description: Invalid name or duplicate per-user folder name
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||||
|
/note-folders/{id}:
|
||||||
|
patch:
|
||||||
|
operationId: updateNoteFolder
|
||||||
|
tags: [notes]
|
||||||
|
summary: Rename a note folder
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema: { type: integer }
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [name]
|
||||||
|
properties:
|
||||||
|
name: { type: string, minLength: 1, maxLength: 80 }
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Folder updated
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/NoteFolder" }
|
||||||
|
"404":
|
||||||
|
description: Folder not found or not owned by caller
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/ErrorResponse" }
|
||||||
|
delete:
|
||||||
|
operationId: deleteNoteFolder
|
||||||
|
tags: [notes]
|
||||||
|
summary: Delete a note folder. Notes inside are preserved (folderId set to null).
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema: { type: integer }
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Folder deleted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/SuccessResponse" }
|
||||||
|
|
||||||
/admin/users/{id}/dependents/notes:
|
/admin/users/{id}/dependents/notes:
|
||||||
get:
|
get:
|
||||||
operationId: getAdminUserDependentNotes
|
operationId: getAdminUserDependentNotes
|
||||||
@@ -4900,6 +4990,19 @@ components:
|
|||||||
required:
|
required:
|
||||||
[id, recipientUserId, status, sentAt, readAt, archivedAt]
|
[id, recipientUserId, status, sentAt, readAt, archivedAt]
|
||||||
|
|
||||||
|
NoteFolder:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: integer }
|
||||||
|
userId: { type: integer }
|
||||||
|
name: { type: string }
|
||||||
|
createdAt: { type: string, format: date-time }
|
||||||
|
updatedAt: { type: string, format: date-time }
|
||||||
|
noteCount:
|
||||||
|
type: integer
|
||||||
|
description: Number of notes in this folder, scoped to the requested tab (active vs archived).
|
||||||
|
required: [id, userId, name, createdAt, updatedAt, noteCount]
|
||||||
|
|
||||||
SentNote:
|
SentNote:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -4910,6 +5013,9 @@ components:
|
|||||||
color: { type: string }
|
color: { type: string }
|
||||||
isPinned: { type: boolean }
|
isPinned: { type: boolean }
|
||||||
isArchived: { type: boolean }
|
isArchived: { type: boolean }
|
||||||
|
folderId:
|
||||||
|
type: ["integer", "null"]
|
||||||
|
description: Owning folder id, or null for Unfiled.
|
||||||
createdAt: { type: string, format: date-time }
|
createdAt: { type: string, format: date-time }
|
||||||
updatedAt: { type: string, format: date-time }
|
updatedAt: { type: string, format: date-time }
|
||||||
recipients:
|
recipients:
|
||||||
@@ -4926,6 +5032,9 @@ components:
|
|||||||
title: { type: string }
|
title: { type: string }
|
||||||
content: { type: string }
|
content: { type: string }
|
||||||
color: { type: string }
|
color: { type: string }
|
||||||
|
folderId:
|
||||||
|
type: ["integer", "null"]
|
||||||
|
description: Owning folder id on the recipient's side, or null for Unfiled.
|
||||||
createdAt: { type: string, format: date-time }
|
createdAt: { type: string, format: date-time }
|
||||||
updatedAt: { type: string, format: date-time }
|
updatedAt: { type: string, format: date-time }
|
||||||
sender:
|
sender:
|
||||||
|
|||||||
@@ -3431,6 +3431,10 @@ export const ListSentNotesResponseItem = zod.object({
|
|||||||
color: zod.string(),
|
color: zod.string(),
|
||||||
isPinned: zod.boolean().optional(),
|
isPinned: zod.boolean().optional(),
|
||||||
isArchived: zod.boolean().optional(),
|
isArchived: zod.boolean().optional(),
|
||||||
|
folderId: zod
|
||||||
|
.number()
|
||||||
|
.nullish()
|
||||||
|
.describe("Owning folder id, or null for Unfiled."),
|
||||||
createdAt: zod.coerce.date(),
|
createdAt: zod.coerce.date(),
|
||||||
updatedAt: zod.coerce.date(),
|
updatedAt: zod.coerce.date(),
|
||||||
recipients: zod.array(
|
recipients: zod.array(
|
||||||
@@ -3475,6 +3479,10 @@ export const ListReceivedNotesResponseItem = zod.object({
|
|||||||
title: zod.string(),
|
title: zod.string(),
|
||||||
content: zod.string(),
|
content: zod.string(),
|
||||||
color: zod.string(),
|
color: zod.string(),
|
||||||
|
folderId: zod
|
||||||
|
.number()
|
||||||
|
.nullish()
|
||||||
|
.describe("Owning folder id on the recipient's side, or null for Unfiled."),
|
||||||
createdAt: zod.coerce.date(),
|
createdAt: zod.coerce.date(),
|
||||||
updatedAt: zod.coerce.date(),
|
updatedAt: zod.coerce.date(),
|
||||||
sender: zod
|
sender: zod
|
||||||
@@ -3642,6 +3650,78 @@ export const ReplyToNoteBody = zod.object({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary List the caller's note folders with per-tab note counts
|
||||||
|
*/
|
||||||
|
export const listNoteFoldersQueryArchivedDefault = false;
|
||||||
|
|
||||||
|
export const ListNoteFoldersQueryParams = zod.object({
|
||||||
|
archived: zod.coerce
|
||||||
|
.boolean()
|
||||||
|
.default(listNoteFoldersQueryArchivedDefault)
|
||||||
|
.describe("Tab scope for noteCount (false = My Notes, true = Archive)"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ListNoteFoldersResponseItem = zod.object({
|
||||||
|
id: zod.number(),
|
||||||
|
userId: zod.number(),
|
||||||
|
name: zod.string(),
|
||||||
|
createdAt: zod.coerce.date(),
|
||||||
|
updatedAt: zod.coerce.date(),
|
||||||
|
noteCount: zod
|
||||||
|
.number()
|
||||||
|
.describe(
|
||||||
|
"Number of notes in this folder, scoped to the requested tab (active vs archived).",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
export const ListNoteFoldersResponse = zod.array(ListNoteFoldersResponseItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create a new note folder owned by the caller
|
||||||
|
*/
|
||||||
|
export const createNoteFolderBodyNameMax = 80;
|
||||||
|
|
||||||
|
export const CreateNoteFolderBody = zod.object({
|
||||||
|
name: zod.string().min(1).max(createNoteFolderBodyNameMax),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Rename a note folder
|
||||||
|
*/
|
||||||
|
export const UpdateNoteFolderParams = zod.object({
|
||||||
|
id: zod.coerce.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateNoteFolderBodyNameMax = 80;
|
||||||
|
|
||||||
|
export const UpdateNoteFolderBody = zod.object({
|
||||||
|
name: zod.string().min(1).max(updateNoteFolderBodyNameMax),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateNoteFolderResponse = zod.object({
|
||||||
|
id: zod.number(),
|
||||||
|
userId: zod.number(),
|
||||||
|
name: zod.string(),
|
||||||
|
createdAt: zod.coerce.date(),
|
||||||
|
updatedAt: zod.coerce.date(),
|
||||||
|
noteCount: zod
|
||||||
|
.number()
|
||||||
|
.describe(
|
||||||
|
"Number of notes in this folder, scoped to the requested tab (active vs archived).",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Delete a note folder. Notes inside are preserved (folderId set to null).
|
||||||
|
*/
|
||||||
|
export const DeleteNoteFolderParams = zod.object({
|
||||||
|
id: zod.coerce.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DeleteNoteFolderResponse = zod.object({
|
||||||
|
success: zod.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary List notes owned by a user
|
* @summary List notes owned by a user
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user