Let leaving admins choose who takes over the group (task #46)
Original ask: when the only admin leaves a group, let them pick a
specific successor instead of always auto-promoting the longest-tenured
member. Keep "auto" available, with bilingual labels.
Changes:
- lib/api-spec/openapi.yaml: extend POST /conversations/{id}/leave with
an optional JSON body { successorId?: number | null }.
- Regenerated lib/api-client-react and lib/api-zod via api-spec codegen.
- artifacts/api-server/src/routes/conversations.ts: parse the new body,
and when the leaver is the only admin, promote the requested successor
if one is provided and is a current member; otherwise fall back to the
existing oldest-member auto-promotion. Returns 400 if successorId is
not a remaining member.
- artifacts/teaboy-os/src/pages/chat.tsx:
- Track a successorChoice state ("auto" | userId).
- In the leave confirmation dialog, when the user is the sole admin
and there are other members, render a radio-list chooser with an
"Auto" option (default) plus each remaining member.
- Pass { successorId } to the leave mutation when a specific member
is chosen; pass {} for auto.
- Reset choice on success.
- artifacts/teaboy-os/src/locales/{en,ar}.json: added successorTitle,
successorHelp, successorAuto strings.
Verification:
- pnpm -w run typecheck passes for libs and all artifacts.
- Attempted an end-to-end browser test; the run was interrupted before
completion. Backend logic and UI wiring were validated by reading
through the code paths and type system.
Replit-Task-Id: c9065bc1-ab4e-4a3b-a865-81754f5c2e5a
This commit is contained in:
@@ -562,6 +562,14 @@ export interface AdminAppOpensByUser {
|
||||
opens: AppOpenByUserEntry[];
|
||||
}
|
||||
|
||||
export type LeaveConversationBody = {
|
||||
/** When the leaver is the only admin, optionally name a specific
|
||||
remaining member to promote. Omit (or send null) to keep the
|
||||
automatic behavior of promoting the longest-tenured member.
|
||||
*/
|
||||
successorId?: number | null;
|
||||
};
|
||||
|
||||
export type GetAdminStatsParams = {
|
||||
/**
|
||||
* Time range for trend stats. Use "custom" with from/to.
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
GetAdminStatsParams,
|
||||
HealthStatus,
|
||||
HomeStats,
|
||||
LeaveConversationBody,
|
||||
LoginBody,
|
||||
MessageWithSender,
|
||||
Notification,
|
||||
@@ -2988,11 +2989,14 @@ export const getLeaveConversationUrl = (id: number) => {
|
||||
|
||||
export const leaveConversation = async (
|
||||
id: number,
|
||||
leaveConversationBody?: LeaveConversationBody,
|
||||
options?: RequestInit,
|
||||
): Promise<SuccessResponse> => {
|
||||
return customFetch<SuccessResponse>(getLeaveConversationUrl(id), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(leaveConversationBody),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3003,14 +3007,14 @@ export const getLeaveConversationMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof leaveConversation>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
{ id: number; data: BodyType<LeaveConversationBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof leaveConversation>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
{ id: number; data: BodyType<LeaveConversationBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["leaveConversation"];
|
||||
@@ -3024,11 +3028,11 @@ export const getLeaveConversationMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof leaveConversation>>,
|
||||
{ id: number }
|
||||
{ id: number; data: BodyType<LeaveConversationBody> }
|
||||
> = (props) => {
|
||||
const { id } = props ?? {};
|
||||
const { id, data } = props ?? {};
|
||||
|
||||
return leaveConversation(id, requestOptions);
|
||||
return leaveConversation(id, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
@@ -3037,7 +3041,7 @@ export const getLeaveConversationMutationOptions = <
|
||||
export type LeaveConversationMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof leaveConversation>>
|
||||
>;
|
||||
|
||||
export type LeaveConversationMutationBody = BodyType<LeaveConversationBody>;
|
||||
export type LeaveConversationMutationError = ErrorType<unknown>;
|
||||
|
||||
/**
|
||||
@@ -3050,14 +3054,14 @@ export const useLeaveConversation = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof leaveConversation>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
{ id: number; data: BodyType<LeaveConversationBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof leaveConversation>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
{ id: number; data: BodyType<LeaveConversationBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getLeaveConversationMutationOptions(options));
|
||||
|
||||
@@ -765,6 +765,20 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
successorId:
|
||||
type: integer
|
||||
description: |
|
||||
When the leaver is the only admin, optionally name a specific
|
||||
remaining member to promote. Omit (or send null) to keep the
|
||||
automatic behavior of promoting the longest-tenured member.
|
||||
nullable: true
|
||||
responses:
|
||||
"200":
|
||||
description: Left conversation
|
||||
|
||||
@@ -946,6 +946,15 @@ export const LeaveConversationParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
export const LeaveConversationBody = zod.object({
|
||||
successorId: zod
|
||||
.number()
|
||||
.nullish()
|
||||
.describe(
|
||||
"When the leaver is the only admin, optionally name a specific\nremaining member to promote. Omit (or send null) to keep the\nautomatic behavior of promoting the longest-tenured member.\n",
|
||||
),
|
||||
});
|
||||
|
||||
export const LeaveConversationResponse = zod.object({
|
||||
success: zod.boolean(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user