Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes a note (title/content/color), picks recipient(s), Send. Recipients get an Inbox with sender name, color, Read/Unread badge, and inline reply. Sender sees Sent Notes with per-recipient status. Sender's and recipient's copies must be INDEPENDENT, with backend access checks (admin sees everything), realtime updates, toasts, an unread badge on the Notes app tile, and full i18n + RTL. Four review rounds were addressed in this commit: Round 1 (independence): - Snapshot columns (title/content/color) on note_recipients; FKs dropped on note_recipients.note_id and note_replies.note_id so recipient threads survive the sender deleting their note. - /notes/received and /notes/:id/thread render the recipient snapshot. Round 2: - POST /notes/:id/reply: owner is now allowed to reply too. - Added GET /notes/:id as alias of /notes/:id/thread. - Added OpenAPI ops for the Notes routes; ran orval codegen. - use-notifications-socket.ts shows bilingual toasts for note_received / note_replied (suppressed during socket warmup). - home.tsx renders an unread badge on the Notes app tile. Round 3: - Archived tab now shows BOTH "My archived notes" and "Archived inbox". - Sender thread view groups replies by recipient with per-conversation header and per-reply author + localized timestamp. - Send dialog requires explicit AlertDialog confirmation + success toast. - Realtime toast strings moved off hardcoded EN/AR to i18n keys. - handleNoteDetail returns 403 (not 404) for non-participants of an existing conversation. - useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient picker for owner replies on multi-recipient notes. Round 6 (this round): - OpenAPI: replaced all `additionalProperties: true` schemas in /notes paths with concrete component refs — NoteRecipientStatus (enum), NoteUserSummary, NoteRecipientSummary, SentNote, ReceivedNote, NoteReply, NoteThread, SendNoteResult. Re-ran orval; api-zod and api-client-react regenerated cleanly. - Trimmed narrative comments in artifacts/api-server/src/routes/notes.ts (handleNoteDetail, /sent, /received, /send, /read, /reply). - Schema FK + enum policy: kept noteId as plain integer (no FK) on note_recipients/note_replies — this is required so recipient snapshots survive sender deletion. Status remains a varchar but is constrained at the API boundary by the new NoteRecipientStatus enum schema and TS string-literal union on both server and client. - Migrations: this monorepo uses `drizzle-kit push` exclusively (lib/db has no migrations directory; drizzle.config.ts is push-only; package.json defines only push/push-force scripts). No migration files committed by design. Round 5: admin authorization fix - handleNoteDetail: admin bypass is now evaluated BEFORE the "non-participant 403" branch. When the sender has deleted the live note row but recipient snapshots remain, an admin GET /notes/:id now returns 200 with thread payload assembled from a fallback recipient snapshot (instead of 403). - New regression test: "admin can read a note whose sender deleted their copy (recipient snapshot remains)". Round 4: - Added GET /notes/my as an explicit alias of GET /notes (shared handleListMyNotes handler). - Removed `as any[]` cast in /notes/sent — replaced with a precise local SentNoteOut type derived from the query result. - Added auth coverage tests: - stranger forbidden from /read, /archive, /reply, GET /notes/:id - recipient cannot PATCH the sender's note body - admin can read any note via GET /notes/:id and sees full thread - GET /notes/my matches GET /notes createUser test helper now accepts an optional role. Note on schema migrations: this monorepo uses `drizzle-kit push` (no migration files); `pnpm --filter @workspace/db push` was already run when the new columns/tables landed. Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox) all pass. tx-os typecheck is clean. Architect re-review: PASS. Pre-existing executive-meetings TS errors and the failing top-level `test` workflow are unrelated to this task.
This commit is contained in:
@@ -1049,6 +1049,106 @@ export interface UserDependentNoteItem {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type NoteRecipientStatus =
|
||||
(typeof NoteRecipientStatus)[keyof typeof NoteRecipientStatus];
|
||||
|
||||
export const NoteRecipientStatus = {
|
||||
unread: "unread",
|
||||
read: "read",
|
||||
replied: "replied",
|
||||
archived: "archived",
|
||||
} as const;
|
||||
|
||||
export interface NoteUserSummary {
|
||||
id: number;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
displayNameAr: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn: string | null;
|
||||
/** @nullable */
|
||||
avatarUrl?: string | null;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface NoteRecipientSummary {
|
||||
id: number;
|
||||
noteId?: number;
|
||||
recipientUserId: number;
|
||||
senderUserId: number;
|
||||
status: NoteRecipientStatus;
|
||||
sentAt: string;
|
||||
/** @nullable */
|
||||
readAt: string | null;
|
||||
/** @nullable */
|
||||
archivedAt: string | null;
|
||||
recipient?: NoteUserSummary | null;
|
||||
}
|
||||
|
||||
export interface SentNote {
|
||||
id: number;
|
||||
userId: number;
|
||||
title: string;
|
||||
content: string;
|
||||
color: string;
|
||||
isPinned?: boolean;
|
||||
isArchived?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
recipients: NoteRecipientSummary[];
|
||||
replyCount: number;
|
||||
}
|
||||
|
||||
export interface ReceivedNote {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
color: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
sender?: NoteUserSummary | null;
|
||||
recipientRowId: number;
|
||||
status: NoteRecipientStatus;
|
||||
sentAt: string;
|
||||
/** @nullable */
|
||||
readAt: string | null;
|
||||
/** @nullable */
|
||||
archivedAt: string | null;
|
||||
}
|
||||
|
||||
export interface NoteReply {
|
||||
id: number;
|
||||
noteId: number;
|
||||
senderUserId: number;
|
||||
recipientUserId: number;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
sender?: NoteUserSummary | null;
|
||||
}
|
||||
|
||||
export interface NoteThread {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
color: string;
|
||||
/** @nullable */
|
||||
createdAt?: string | null;
|
||||
/** @nullable */
|
||||
updatedAt?: string | null;
|
||||
senderUserId: number;
|
||||
sender?: NoteUserSummary | null;
|
||||
isOwner: boolean;
|
||||
isAdmin: boolean;
|
||||
myStatus: NoteRecipientStatus | null;
|
||||
recipients: NoteRecipientSummary[];
|
||||
replies: NoteReply[];
|
||||
}
|
||||
|
||||
export interface SendNoteResult {
|
||||
success: boolean;
|
||||
sent: number;
|
||||
}
|
||||
|
||||
export interface UserDependentNotesPage {
|
||||
items: UserDependentNoteItem[];
|
||||
totalCount: number;
|
||||
@@ -1601,38 +1701,24 @@ export type GetAdminServiceDependentOrdersParams = {
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type ListSentNotes200Item = { [key: string]: unknown };
|
||||
|
||||
export type ListReceivedNotesParams = {
|
||||
archived?: boolean;
|
||||
};
|
||||
|
||||
export type ListReceivedNotes200Item = { [key: string]: unknown };
|
||||
|
||||
export type GetNoteDetail200 = { [key: string]: unknown };
|
||||
|
||||
export type SendNoteBody = {
|
||||
recipientUserIds: number[];
|
||||
};
|
||||
|
||||
export type SendNote200 = { [key: string]: unknown };
|
||||
|
||||
export type MarkNoteRead200 = { [key: string]: unknown };
|
||||
|
||||
export type ArchiveReceivedNoteBody = {
|
||||
archived: boolean;
|
||||
};
|
||||
|
||||
export type ArchiveReceivedNote200 = { [key: string]: unknown };
|
||||
|
||||
export type ReplyToNoteBody = {
|
||||
content: string;
|
||||
/** Required when the note owner replies and there is more than one recipient. */
|
||||
recipientUserId?: number;
|
||||
};
|
||||
|
||||
export type ReplyToNote201 = { [key: string]: unknown };
|
||||
|
||||
export type GetAdminUserDependentNotesParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
|
||||
@@ -33,7 +33,6 @@ import type {
|
||||
AppPermissionsImpact,
|
||||
AppPermissionsImpactBody,
|
||||
AppSettings,
|
||||
ArchiveReceivedNote200,
|
||||
ArchiveReceivedNoteBody,
|
||||
AuditLogList,
|
||||
AuthUser,
|
||||
@@ -69,7 +68,6 @@ import type {
|
||||
GetAdminUserDependentOrdersParams,
|
||||
GetAppPermissionAuditParams,
|
||||
GetGroupPermissionAuditParams,
|
||||
GetNoteDetail200,
|
||||
GetRolePermissionAuditParams,
|
||||
GetUserPermissionAuditParams,
|
||||
Group,
|
||||
@@ -80,19 +78,18 @@ import type {
|
||||
LeaveConversationBody,
|
||||
ListAuditLogsParams,
|
||||
ListGroupsParams,
|
||||
ListReceivedNotes200Item,
|
||||
ListReceivedNotesParams,
|
||||
ListSentNotes200Item,
|
||||
ListUsersParams,
|
||||
LoginBody,
|
||||
MarkNoteRead200,
|
||||
MessageWithSender,
|
||||
NoteReply,
|
||||
NoteThread,
|
||||
Notification,
|
||||
Permission,
|
||||
PermissionAuditList,
|
||||
ReceivedNote,
|
||||
RegisterBody,
|
||||
ReplaceRolePermissionsBody,
|
||||
ReplyToNote201,
|
||||
ReplyToNoteBody,
|
||||
RequestUploadUrlBody,
|
||||
RequestUploadUrlResponse,
|
||||
@@ -104,8 +101,9 @@ import type {
|
||||
RolePermissionsImpactBody,
|
||||
RoleUsage,
|
||||
SendMessageBody,
|
||||
SendNote200,
|
||||
SendNoteBody,
|
||||
SendNoteResult,
|
||||
SentNote,
|
||||
Service,
|
||||
ServiceCategory,
|
||||
ServiceDeletionConflict,
|
||||
@@ -8272,8 +8270,8 @@ export const getListSentNotesUrl = () => {
|
||||
|
||||
export const listSentNotes = async (
|
||||
options?: RequestInit,
|
||||
): Promise<ListSentNotes200Item[]> => {
|
||||
return customFetch<ListSentNotes200Item[]>(getListSentNotesUrl(), {
|
||||
): Promise<SentNote[]> => {
|
||||
return customFetch<SentNote[]>(getListSentNotesUrl(), {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
@@ -8360,14 +8358,11 @@ export const getListReceivedNotesUrl = (params?: ListReceivedNotesParams) => {
|
||||
export const listReceivedNotes = async (
|
||||
params?: ListReceivedNotesParams,
|
||||
options?: RequestInit,
|
||||
): Promise<ListReceivedNotes200Item[]> => {
|
||||
return customFetch<ListReceivedNotes200Item[]>(
|
||||
getListReceivedNotesUrl(params),
|
||||
{
|
||||
...options,
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
): Promise<ReceivedNote[]> => {
|
||||
return customFetch<ReceivedNote[]>(getListReceivedNotesUrl(params), {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const getListReceivedNotesQueryKey = (
|
||||
@@ -8448,8 +8443,8 @@ export const getGetNoteDetailUrl = (id: number) => {
|
||||
export const getNoteDetail = async (
|
||||
id: number,
|
||||
options?: RequestInit,
|
||||
): Promise<GetNoteDetail200> => {
|
||||
return customFetch<GetNoteDetail200>(getGetNoteDetailUrl(id), {
|
||||
): Promise<NoteThread> => {
|
||||
return customFetch<NoteThread>(getGetNoteDetailUrl(id), {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
@@ -8536,8 +8531,8 @@ export const sendNote = async (
|
||||
id: number,
|
||||
sendNoteBody: SendNoteBody,
|
||||
options?: RequestInit,
|
||||
): Promise<SendNote200> => {
|
||||
return customFetch<SendNote200>(getSendNoteUrl(id), {
|
||||
): Promise<SendNoteResult> => {
|
||||
return customFetch<SendNoteResult>(getSendNoteUrl(id), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
@@ -8622,8 +8617,8 @@ export const getMarkNoteReadUrl = (id: number) => {
|
||||
export const markNoteRead = async (
|
||||
id: number,
|
||||
options?: RequestInit,
|
||||
): Promise<MarkNoteRead200> => {
|
||||
return customFetch<MarkNoteRead200>(getMarkNoteReadUrl(id), {
|
||||
): Promise<SuccessResponse> => {
|
||||
return customFetch<SuccessResponse>(getMarkNoteReadUrl(id), {
|
||||
...options,
|
||||
method: "POST",
|
||||
});
|
||||
@@ -8707,8 +8702,8 @@ export const archiveReceivedNote = async (
|
||||
id: number,
|
||||
archiveReceivedNoteBody: ArchiveReceivedNoteBody,
|
||||
options?: RequestInit,
|
||||
): Promise<ArchiveReceivedNote200> => {
|
||||
return customFetch<ArchiveReceivedNote200>(getArchiveReceivedNoteUrl(id), {
|
||||
): Promise<SuccessResponse> => {
|
||||
return customFetch<SuccessResponse>(getArchiveReceivedNoteUrl(id), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
@@ -8794,8 +8789,8 @@ export const replyToNote = async (
|
||||
id: number,
|
||||
replyToNoteBody: ReplyToNoteBody,
|
||||
options?: RequestInit,
|
||||
): Promise<ReplyToNote201> => {
|
||||
return customFetch<ReplyToNote201>(getReplyToNoteUrl(id), {
|
||||
): Promise<NoteReply> => {
|
||||
return customFetch<NoteReply>(getReplyToNoteUrl(id), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
|
||||
+134
-7
@@ -2745,7 +2745,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { type: object, additionalProperties: true }
|
||||
items: { $ref: "#/components/schemas/SentNote" }
|
||||
/notes/received:
|
||||
get:
|
||||
operationId: listReceivedNotes
|
||||
@@ -2763,7 +2763,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { type: object, additionalProperties: true }
|
||||
items: { $ref: "#/components/schemas/ReceivedNote" }
|
||||
/notes/{id}:
|
||||
get:
|
||||
operationId: getNoteDetail
|
||||
@@ -2779,7 +2779,7 @@ paths:
|
||||
description: Note thread, scoped to caller (sender, recipient, or admin)
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
schema: { $ref: "#/components/schemas/NoteThread" }
|
||||
"403":
|
||||
description: Not a participant
|
||||
content:
|
||||
@@ -2816,7 +2816,7 @@ paths:
|
||||
description: Sent (idempotent on existing recipients)
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
schema: { $ref: "#/components/schemas/SendNoteResult" }
|
||||
/notes/{id}/read:
|
||||
post:
|
||||
operationId: markNoteRead
|
||||
@@ -2832,7 +2832,7 @@ paths:
|
||||
description: Marked read
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
schema: { $ref: "#/components/schemas/SuccessResponse" }
|
||||
/notes/{id}/archive:
|
||||
post:
|
||||
operationId: archiveReceivedNote
|
||||
@@ -2857,7 +2857,7 @@ paths:
|
||||
description: Archive state updated
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
schema: { $ref: "#/components/schemas/SuccessResponse" }
|
||||
/notes/{id}/reply:
|
||||
post:
|
||||
operationId: replyToNote
|
||||
@@ -2885,7 +2885,7 @@ paths:
|
||||
description: Reply created
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
schema: { $ref: "#/components/schemas/NoteReply" }
|
||||
|
||||
/admin/users/{id}/dependents/notes:
|
||||
get:
|
||||
@@ -4835,6 +4835,133 @@ components:
|
||||
updatedAt: { type: string, format: date-time }
|
||||
required: [id, title, isPinned, isArchived, updatedAt]
|
||||
|
||||
NoteRecipientStatus:
|
||||
type: string
|
||||
enum: [unread, read, replied, archived]
|
||||
|
||||
NoteUserSummary:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
username: { type: string }
|
||||
displayNameAr: { type: ["string", "null"] }
|
||||
displayNameEn: { type: ["string", "null"] }
|
||||
avatarUrl: { type: ["string", "null"] }
|
||||
isActive: { type: boolean }
|
||||
required: [id, username, displayNameAr, displayNameEn]
|
||||
|
||||
NoteRecipientSummary:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
noteId: { type: integer }
|
||||
recipientUserId: { type: integer }
|
||||
senderUserId: { type: integer }
|
||||
status: { $ref: "#/components/schemas/NoteRecipientStatus" }
|
||||
sentAt: { type: string, format: date-time }
|
||||
readAt: { type: ["string", "null"], format: date-time }
|
||||
archivedAt: { type: ["string", "null"], format: date-time }
|
||||
recipient:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/NoteUserSummary"
|
||||
- type: "null"
|
||||
required:
|
||||
[id, recipientUserId, senderUserId, status, sentAt, readAt, archivedAt]
|
||||
|
||||
SentNote:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
userId: { type: integer }
|
||||
title: { type: string }
|
||||
content: { type: string }
|
||||
color: { type: string }
|
||||
isPinned: { type: boolean }
|
||||
isArchived: { type: boolean }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
recipients:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/NoteRecipientSummary" }
|
||||
replyCount: { type: integer }
|
||||
required:
|
||||
[id, userId, title, content, color, createdAt, updatedAt, recipients, replyCount]
|
||||
|
||||
ReceivedNote:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
title: { type: string }
|
||||
content: { type: string }
|
||||
color: { type: string }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
sender:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/NoteUserSummary"
|
||||
- type: "null"
|
||||
recipientRowId: { type: integer }
|
||||
status: { $ref: "#/components/schemas/NoteRecipientStatus" }
|
||||
sentAt: { type: string, format: date-time }
|
||||
readAt: { type: ["string", "null"], format: date-time }
|
||||
archivedAt: { type: ["string", "null"], format: date-time }
|
||||
required:
|
||||
[id, title, content, color, createdAt, updatedAt, recipientRowId, status,
|
||||
sentAt, readAt, archivedAt]
|
||||
|
||||
NoteReply:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
noteId: { type: integer }
|
||||
senderUserId: { type: integer }
|
||||
recipientUserId: { type: integer }
|
||||
content: { type: string }
|
||||
createdAt: { type: string, format: date-time }
|
||||
sender:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/NoteUserSummary"
|
||||
- type: "null"
|
||||
required:
|
||||
[id, noteId, senderUserId, recipientUserId, content, createdAt]
|
||||
|
||||
NoteThread:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
title: { type: string }
|
||||
content: { type: string }
|
||||
color: { type: string }
|
||||
createdAt: { type: ["string", "null"], format: date-time }
|
||||
updatedAt: { type: ["string", "null"], format: date-time }
|
||||
senderUserId: { type: integer }
|
||||
sender:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/NoteUserSummary"
|
||||
- type: "null"
|
||||
isOwner: { type: boolean }
|
||||
isAdmin: { type: boolean }
|
||||
myStatus:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/NoteRecipientStatus"
|
||||
- type: "null"
|
||||
recipients:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/NoteRecipientSummary" }
|
||||
replies:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/NoteReply" }
|
||||
required:
|
||||
[id, title, content, color, senderUserId, isOwner, isAdmin, myStatus,
|
||||
recipients, replies]
|
||||
|
||||
SendNoteResult:
|
||||
type: object
|
||||
properties:
|
||||
success: { type: boolean }
|
||||
sent: { type: integer }
|
||||
required: [success, sent]
|
||||
|
||||
UserDependentNotesPage:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -3331,10 +3331,43 @@ export const GetAdminServiceDependentOrdersResponse = zod.object({
|
||||
/**
|
||||
* @summary List notes the caller has sent
|
||||
*/
|
||||
export const ListSentNotesResponseItem = zod.record(
|
||||
zod.string(),
|
||||
zod.unknown(),
|
||||
);
|
||||
export const ListSentNotesResponseItem = zod.object({
|
||||
id: zod.number(),
|
||||
userId: zod.number(),
|
||||
title: zod.string(),
|
||||
content: zod.string(),
|
||||
color: zod.string(),
|
||||
isPinned: zod.boolean().optional(),
|
||||
isArchived: zod.boolean().optional(),
|
||||
createdAt: zod.coerce.date(),
|
||||
updatedAt: zod.coerce.date(),
|
||||
recipients: zod.array(
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
noteId: zod.number().optional(),
|
||||
recipientUserId: zod.number(),
|
||||
senderUserId: zod.number(),
|
||||
status: zod.enum(["unread", "read", "replied", "archived"]),
|
||||
sentAt: zod.coerce.date(),
|
||||
readAt: zod.coerce.date().nullable(),
|
||||
archivedAt: zod.coerce.date().nullable(),
|
||||
recipient: zod
|
||||
.union([
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
username: zod.string(),
|
||||
displayNameAr: zod.string().nullable(),
|
||||
displayNameEn: zod.string().nullable(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean().optional(),
|
||||
}),
|
||||
zod.null(),
|
||||
])
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
replyCount: zod.number(),
|
||||
});
|
||||
export const ListSentNotesResponse = zod.array(ListSentNotesResponseItem);
|
||||
|
||||
/**
|
||||
@@ -3346,10 +3379,32 @@ export const ListReceivedNotesQueryParams = zod.object({
|
||||
archived: zod.coerce.boolean().default(listReceivedNotesQueryArchivedDefault),
|
||||
});
|
||||
|
||||
export const ListReceivedNotesResponseItem = zod.record(
|
||||
zod.string(),
|
||||
zod.unknown(),
|
||||
);
|
||||
export const ListReceivedNotesResponseItem = zod.object({
|
||||
id: zod.number(),
|
||||
title: zod.string(),
|
||||
content: zod.string(),
|
||||
color: zod.string(),
|
||||
createdAt: zod.coerce.date(),
|
||||
updatedAt: zod.coerce.date(),
|
||||
sender: zod
|
||||
.union([
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
username: zod.string(),
|
||||
displayNameAr: zod.string().nullable(),
|
||||
displayNameEn: zod.string().nullable(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean().optional(),
|
||||
}),
|
||||
zod.null(),
|
||||
])
|
||||
.optional(),
|
||||
recipientRowId: zod.number(),
|
||||
status: zod.enum(["unread", "read", "replied", "archived"]),
|
||||
sentAt: zod.coerce.date(),
|
||||
readAt: zod.coerce.date().nullable(),
|
||||
archivedAt: zod.coerce.date().nullable(),
|
||||
});
|
||||
export const ListReceivedNotesResponse = zod.array(
|
||||
ListReceivedNotesResponseItem,
|
||||
);
|
||||
@@ -3361,7 +3416,82 @@ export const GetNoteDetailParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
export const GetNoteDetailResponse = zod.record(zod.string(), zod.unknown());
|
||||
export const GetNoteDetailResponse = zod.object({
|
||||
id: zod.number(),
|
||||
title: zod.string(),
|
||||
content: zod.string(),
|
||||
color: zod.string(),
|
||||
createdAt: zod.coerce.date().nullish(),
|
||||
updatedAt: zod.coerce.date().nullish(),
|
||||
senderUserId: zod.number(),
|
||||
sender: zod
|
||||
.union([
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
username: zod.string(),
|
||||
displayNameAr: zod.string().nullable(),
|
||||
displayNameEn: zod.string().nullable(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean().optional(),
|
||||
}),
|
||||
zod.null(),
|
||||
])
|
||||
.optional(),
|
||||
isOwner: zod.boolean(),
|
||||
isAdmin: zod.boolean(),
|
||||
myStatus: zod.union([
|
||||
zod.enum(["unread", "read", "replied", "archived"]),
|
||||
zod.null(),
|
||||
]),
|
||||
recipients: zod.array(
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
noteId: zod.number().optional(),
|
||||
recipientUserId: zod.number(),
|
||||
senderUserId: zod.number(),
|
||||
status: zod.enum(["unread", "read", "replied", "archived"]),
|
||||
sentAt: zod.coerce.date(),
|
||||
readAt: zod.coerce.date().nullable(),
|
||||
archivedAt: zod.coerce.date().nullable(),
|
||||
recipient: zod
|
||||
.union([
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
username: zod.string(),
|
||||
displayNameAr: zod.string().nullable(),
|
||||
displayNameEn: zod.string().nullable(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean().optional(),
|
||||
}),
|
||||
zod.null(),
|
||||
])
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
replies: zod.array(
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
noteId: zod.number(),
|
||||
senderUserId: zod.number(),
|
||||
recipientUserId: zod.number(),
|
||||
content: zod.string(),
|
||||
createdAt: zod.coerce.date(),
|
||||
sender: zod
|
||||
.union([
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
username: zod.string(),
|
||||
displayNameAr: zod.string().nullable(),
|
||||
displayNameEn: zod.string().nullable(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean().optional(),
|
||||
}),
|
||||
zod.null(),
|
||||
])
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Owner sends an existing note to a list of recipients
|
||||
@@ -3374,7 +3504,10 @@ export const SendNoteBody = zod.object({
|
||||
recipientUserIds: zod.array(zod.number()),
|
||||
});
|
||||
|
||||
export const SendNoteResponse = zod.record(zod.string(), zod.unknown());
|
||||
export const SendNoteResponse = zod.object({
|
||||
success: zod.boolean(),
|
||||
sent: zod.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Recipient marks their copy of a note read
|
||||
@@ -3383,7 +3516,9 @@ export const MarkNoteReadParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
export const MarkNoteReadResponse = zod.record(zod.string(), zod.unknown());
|
||||
export const MarkNoteReadResponse = zod.object({
|
||||
success: zod.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Recipient archives or unarchives their copy of a note
|
||||
@@ -3396,10 +3531,9 @@ export const ArchiveReceivedNoteBody = zod.object({
|
||||
archived: zod.boolean(),
|
||||
});
|
||||
|
||||
export const ArchiveReceivedNoteResponse = zod.record(
|
||||
zod.string(),
|
||||
zod.unknown(),
|
||||
);
|
||||
export const ArchiveReceivedNoteResponse = zod.object({
|
||||
success: zod.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Sender or recipient adds a reply to a note's thread
|
||||
|
||||
Reference in New Issue
Block a user