mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add admin UI control for chat auto-archive days (#24704)
Relates to #24642 Adds admin UI controls for managing chat auto-archive (days) under "Lifecycle". Also adds a "Days" label to the right of the pre-existing unitless numeric input for consistency. Exemplary screenshot below. More screens available in Storybook. <img width="847" height="585" alt="Screenshot 2026-04-24 at 16 48 59" src="https://github.com/user-attachments/assets/d38de5f8-d379-4b06-b175-ac399f31e578" />
This commit is contained in:
+2
-2
@@ -4360,8 +4360,8 @@ func (api *API) getChatAutoArchiveDays(rw http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// Upper bound for the auto-archive window. Update the frontend
|
||||
// constant if a settings UI is added.
|
||||
// Upper bound for the auto-archive window. Keep in sync with
|
||||
// the validation schema in site/src/pages/AgentsPage/components/AutoArchiveSettings.tsx.
|
||||
const autoArchiveDaysMaximum = 3650 // ~10 years
|
||||
|
||||
// putChatAutoArchiveDays updates the deployment-wide auto-archive
|
||||
|
||||
@@ -3379,6 +3379,24 @@ class ExperimentalApiMethods {
|
||||
await this.axios.put("/api/experimental/chats/config/retention-days", req);
|
||||
};
|
||||
|
||||
getChatAutoArchiveDays =
|
||||
async (): Promise<TypesGen.ChatAutoArchiveDaysResponse> => {
|
||||
const response =
|
||||
await this.axios.get<TypesGen.ChatAutoArchiveDaysResponse>(
|
||||
"/api/experimental/chats/config/auto-archive-days",
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateChatAutoArchiveDays = async (
|
||||
req: TypesGen.UpdateChatAutoArchiveDaysRequest,
|
||||
): Promise<void> => {
|
||||
await this.axios.put(
|
||||
"/api/experimental/chats/config/auto-archive-days",
|
||||
req,
|
||||
);
|
||||
};
|
||||
|
||||
updateChatTemplateAllowlist = async (
|
||||
req: TypesGen.ChatTemplateAllowlist,
|
||||
): Promise<void> => {
|
||||
|
||||
@@ -1351,6 +1351,22 @@ export const updateChatRetentionDays = (queryClient: QueryClient) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const chatAutoArchiveDaysKey = ["chat-auto-archive-days"] as const;
|
||||
|
||||
export const chatAutoArchiveDays = () => ({
|
||||
queryKey: chatAutoArchiveDaysKey,
|
||||
queryFn: () => API.experimental.getChatAutoArchiveDays(),
|
||||
});
|
||||
|
||||
export const updateChatAutoArchiveDays = (queryClient: QueryClient) => ({
|
||||
mutationFn: API.experimental.updateChatAutoArchiveDays,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: chatAutoArchiveDaysKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const chatTemplateAllowlistKey = ["chat-template-allowlist"] as const;
|
||||
|
||||
export const chatTemplateAllowlist = () => ({
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { FC } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
chatAutoArchiveDays,
|
||||
chatRetentionDays,
|
||||
chatWorkspaceTTL,
|
||||
updateChatAutoArchiveDays,
|
||||
updateChatRetentionDays,
|
||||
updateChatWorkspaceTTL,
|
||||
} from "#/api/queries/chats";
|
||||
@@ -21,12 +23,19 @@ const AgentSettingsLifecyclePage: FC = () => {
|
||||
...chatRetentionDays(),
|
||||
enabled: permissions.editDeploymentConfig,
|
||||
});
|
||||
const autoArchiveDaysQuery = useQuery({
|
||||
...chatAutoArchiveDays(),
|
||||
enabled: permissions.editDeploymentConfig,
|
||||
});
|
||||
const saveWorkspaceTTLMutation = useMutation(
|
||||
updateChatWorkspaceTTL(queryClient),
|
||||
);
|
||||
const saveRetentionDaysMutation = useMutation(
|
||||
updateChatRetentionDays(queryClient),
|
||||
);
|
||||
const saveAutoArchiveDaysMutation = useMutation(
|
||||
updateChatAutoArchiveDays(queryClient),
|
||||
);
|
||||
|
||||
return (
|
||||
<RequirePermission isFeatureVisible={permissions.editDeploymentConfig}>
|
||||
@@ -43,6 +52,12 @@ const AgentSettingsLifecyclePage: FC = () => {
|
||||
onSaveRetentionDays={saveRetentionDaysMutation.mutate}
|
||||
isSavingRetentionDays={saveRetentionDaysMutation.isPending}
|
||||
isSaveRetentionDaysError={saveRetentionDaysMutation.isError}
|
||||
autoArchiveDaysData={autoArchiveDaysQuery.data}
|
||||
isAutoArchiveDaysLoading={autoArchiveDaysQuery.isLoading}
|
||||
isAutoArchiveDaysLoadError={autoArchiveDaysQuery.isError}
|
||||
onSaveAutoArchiveDays={saveAutoArchiveDaysMutation.mutate}
|
||||
isSavingAutoArchiveDays={saveAutoArchiveDaysMutation.isPending}
|
||||
isSaveAutoArchiveDaysError={saveAutoArchiveDaysMutation.isError}
|
||||
/>
|
||||
</RequirePermission>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,12 @@ const baseArgs: AgentSettingsLifecyclePageViewProps = {
|
||||
onSaveRetentionDays: fn(),
|
||||
isSavingRetentionDays: false,
|
||||
isSaveRetentionDaysError: false,
|
||||
autoArchiveDaysData: { auto_archive_days: 0 },
|
||||
isAutoArchiveDaysLoading: false,
|
||||
isAutoArchiveDaysLoadError: false,
|
||||
onSaveAutoArchiveDays: fn(),
|
||||
isSavingAutoArchiveDays: false,
|
||||
isSaveAutoArchiveDaysError: false,
|
||||
};
|
||||
|
||||
const meta = {
|
||||
@@ -233,6 +239,231 @@ export const DefaultAutostopToggleOffFailure: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
// --- Auto-Archive Stories ---
|
||||
|
||||
export const AutoArchiveDefault: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const toggle = await canvas.findByRole("switch", {
|
||||
name: "Enable auto-archive",
|
||||
});
|
||||
expect(toggle).not.toBeChecked();
|
||||
expect(canvas.queryByLabelText("Auto-archive period in days")).toBeNull();
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveEnabled: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 90 },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const toggle = await canvas.findByRole("switch", {
|
||||
name: "Enable auto-archive",
|
||||
});
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
const input = await canvas.findByLabelText("Auto-archive period in days");
|
||||
expect(input).toHaveValue(90);
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveSaveDisabled: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 90 },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = await canvas.findByLabelText("Auto-archive period in days");
|
||||
const archiveForm = input.closest("form");
|
||||
if (!(archiveForm instanceof HTMLFormElement)) {
|
||||
throw new Error("Expected auto-archive input to live inside a form.");
|
||||
}
|
||||
expect(
|
||||
within(archiveForm).queryByRole("button", { name: "Save" }),
|
||||
).toBeNull();
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveToggleOnSavesDefault: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 0 },
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const toggle = await canvas.findByRole("switch", {
|
||||
name: "Enable auto-archive",
|
||||
});
|
||||
expect(toggle).not.toBeChecked();
|
||||
|
||||
await userEvent.click(toggle);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onSaveAutoArchiveDays).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ auto_archive_days: 90 },
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
const archiveForm = toggle.closest("form");
|
||||
if (!(archiveForm instanceof HTMLFormElement)) {
|
||||
throw new Error("Expected auto-archive toggle to live inside a form.");
|
||||
}
|
||||
|
||||
const input = await within(archiveForm).findByLabelText(
|
||||
"Auto-archive period in days",
|
||||
);
|
||||
expect(input).toHaveValue(90);
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveToggleOffSavesDisabled: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 90 },
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const toggle = await canvas.findByRole("switch", {
|
||||
name: "Enable auto-archive",
|
||||
});
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
await userEvent.click(toggle);
|
||||
await waitFor(() => {
|
||||
expect(args.onSaveAutoArchiveDays).toHaveBeenCalledWith(
|
||||
{ auto_archive_days: 0 },
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveEditDaysAndSave: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 90 },
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = await canvas.findByLabelText("Auto-archive period in days");
|
||||
const archiveForm = input.closest("form");
|
||||
if (!(archiveForm instanceof HTMLFormElement)) {
|
||||
throw new Error("Expected auto-archive input to live inside a form.");
|
||||
}
|
||||
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "120");
|
||||
|
||||
const saveButton = within(archiveForm).getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onSaveAutoArchiveDays).toHaveBeenCalledWith(
|
||||
{ auto_archive_days: 120 },
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveExceedsMax: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 90 },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = await canvas.findByLabelText("Auto-archive period in days");
|
||||
const archiveForm = input.closest("form");
|
||||
if (!(archiveForm instanceof HTMLFormElement)) {
|
||||
throw new Error("Expected auto-archive input to live inside a form.");
|
||||
}
|
||||
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "9999");
|
||||
|
||||
const saveButton = within(archiveForm).getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(input).toBeInvalid();
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
await userEvent.tab();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
canvas.getByText(/must not exceed 3650 days/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveBelowMin: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 90 },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = await canvas.findByLabelText("Auto-archive period in days");
|
||||
const archiveForm = input.closest("form");
|
||||
if (!(archiveForm instanceof HTMLFormElement)) {
|
||||
throw new Error("Expected auto-archive input to live inside a form.");
|
||||
}
|
||||
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "0");
|
||||
|
||||
const saveButton = within(archiveForm).getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(input).toBeInvalid();
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
await userEvent.tab();
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByText(/at least 1 day/i)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveSaveError: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: { auto_archive_days: 90 },
|
||||
isSaveAutoArchiveDaysError: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(
|
||||
await canvas.findByText("Failed to save auto-archive setting."),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const AutoArchiveLoadError: Story = {
|
||||
args: {
|
||||
autoArchiveDaysData: undefined,
|
||||
isAutoArchiveDaysLoadError: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const toggle = await canvas.findByRole("switch", {
|
||||
name: "Enable auto-archive",
|
||||
});
|
||||
expect(toggle).not.toBeChecked();
|
||||
expect(canvas.queryByLabelText("Auto-archive period in days")).toBeNull();
|
||||
expect(
|
||||
await canvas.findByText("Failed to load auto-archive setting."),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
// --- Retention Stories ---
|
||||
|
||||
export const RetentionToggleOnSavesDefault: Story = {
|
||||
args: {
|
||||
retentionDaysData: { retention_days: 0 },
|
||||
@@ -352,5 +583,44 @@ export const RetentionExceedsMax: Story = {
|
||||
expect(retentionInput).toBeInvalid();
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
await userEvent.tab();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
canvas.getByText(/must not exceed 3650 days/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const RetentionBelowMin: Story = {
|
||||
args: {
|
||||
retentionDaysData: { retention_days: 30 },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const retentionInput = await canvas.findByLabelText(
|
||||
"Conversation retention period in days",
|
||||
);
|
||||
const retentionForm = retentionInput.closest("form");
|
||||
if (!(retentionForm instanceof HTMLFormElement)) {
|
||||
throw new Error("Expected retention period input to live inside a form.");
|
||||
}
|
||||
|
||||
await userEvent.clear(retentionInput);
|
||||
await userEvent.type(retentionInput, "0");
|
||||
|
||||
const saveButton = within(retentionForm).getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(retentionInput).toBeInvalid();
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
await userEvent.tab();
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByText(/at least 1 day/i)).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FC } from "react";
|
||||
import type { UseMutateFunction } from "react-query";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { AutoArchiveSettings } from "./components/AutoArchiveSettings";
|
||||
import { RetentionPeriodSettings } from "./components/RetentionPeriodSettings";
|
||||
import { SectionHeader } from "./components/SectionHeader";
|
||||
import { WorkspaceAutostopSettings } from "./components/WorkspaceAutostopSettings";
|
||||
@@ -28,6 +29,17 @@ export interface AgentSettingsLifecyclePageViewProps {
|
||||
>;
|
||||
isSavingRetentionDays: boolean;
|
||||
isSaveRetentionDaysError: boolean;
|
||||
autoArchiveDaysData: TypesGen.ChatAutoArchiveDaysResponse | undefined;
|
||||
isAutoArchiveDaysLoading: boolean;
|
||||
isAutoArchiveDaysLoadError: boolean;
|
||||
onSaveAutoArchiveDays: UseMutateFunction<
|
||||
void,
|
||||
Error,
|
||||
TypesGen.UpdateChatAutoArchiveDaysRequest,
|
||||
unknown
|
||||
>;
|
||||
isSavingAutoArchiveDays: boolean;
|
||||
isSaveAutoArchiveDaysError: boolean;
|
||||
}
|
||||
|
||||
export const AgentSettingsLifecyclePageView: FC<
|
||||
@@ -45,6 +57,12 @@ export const AgentSettingsLifecyclePageView: FC<
|
||||
onSaveRetentionDays,
|
||||
isSavingRetentionDays,
|
||||
isSaveRetentionDaysError,
|
||||
autoArchiveDaysData,
|
||||
isAutoArchiveDaysLoading,
|
||||
isAutoArchiveDaysLoadError,
|
||||
onSaveAutoArchiveDays,
|
||||
isSavingAutoArchiveDays,
|
||||
isSaveAutoArchiveDaysError,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -60,6 +78,14 @@ export const AgentSettingsLifecyclePageView: FC<
|
||||
isSavingWorkspaceTTL={isSavingWorkspaceTTL}
|
||||
isSaveWorkspaceTTLError={isSaveWorkspaceTTLError}
|
||||
/>
|
||||
<AutoArchiveSettings
|
||||
autoArchiveDaysData={autoArchiveDaysData}
|
||||
isAutoArchiveDaysLoading={isAutoArchiveDaysLoading}
|
||||
isAutoArchiveDaysLoadError={isAutoArchiveDaysLoadError}
|
||||
onSaveAutoArchiveDays={onSaveAutoArchiveDays}
|
||||
isSavingAutoArchiveDays={isSavingAutoArchiveDays}
|
||||
isSaveAutoArchiveDaysError={isSaveAutoArchiveDaysError}
|
||||
/>
|
||||
<RetentionPeriodSettings
|
||||
retentionDaysData={retentionDaysData}
|
||||
isRetentionDaysLoading={isRetentionDaysLoading}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import * as Yup from "yup";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { DefaultChatAutoArchiveDays } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { Switch } from "#/components/Switch/Switch";
|
||||
import {
|
||||
TemporarySavedState,
|
||||
useTemporarySavedState,
|
||||
} from "./TemporarySavedState";
|
||||
|
||||
interface MutationCallbacks {
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
interface AutoArchiveSettingsProps {
|
||||
autoArchiveDaysData: TypesGen.ChatAutoArchiveDaysResponse | undefined;
|
||||
isAutoArchiveDaysLoading: boolean;
|
||||
isAutoArchiveDaysLoadError: boolean;
|
||||
onSaveAutoArchiveDays: (
|
||||
req: TypesGen.UpdateChatAutoArchiveDaysRequest,
|
||||
options?: MutationCallbacks,
|
||||
) => void;
|
||||
isSavingAutoArchiveDays: boolean;
|
||||
isSaveAutoArchiveDaysError: boolean;
|
||||
}
|
||||
|
||||
// Keep in sync with autoArchiveDaysMaximum in coderd/exp_chats.go.
|
||||
const validationSchema = Yup.object({
|
||||
auto_archive_days: Yup.number()
|
||||
.integer("Auto-archive days must be a whole number.")
|
||||
.min(1, "Auto-archive period must be at least 1 day.")
|
||||
.max(3650, "Must not exceed 3650 days (~10 years).")
|
||||
.required("Auto-archive days is required."),
|
||||
});
|
||||
|
||||
// Sensible default offered when an admin enables auto-archive for
|
||||
// the first time. Distinct from the server default (0 = disabled).
|
||||
const ENABLE_DEFAULT_DAYS = 90;
|
||||
|
||||
export const AutoArchiveSettings: FC<AutoArchiveSettingsProps> = ({
|
||||
autoArchiveDaysData,
|
||||
isAutoArchiveDaysLoading,
|
||||
isAutoArchiveDaysLoadError,
|
||||
onSaveAutoArchiveDays,
|
||||
isSavingAutoArchiveDays,
|
||||
isSaveAutoArchiveDaysError,
|
||||
}) => {
|
||||
const [archiveToggled, setArchiveToggled] = useState<boolean | null>(null);
|
||||
const { isSavedVisible, showSavedState } = useTemporarySavedState();
|
||||
|
||||
const serverAutoArchiveDays =
|
||||
autoArchiveDaysData?.auto_archive_days ?? DefaultChatAutoArchiveDays;
|
||||
const isAutoArchiveEnabled = archiveToggled ?? serverAutoArchiveDays > 0;
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: { auto_archive_days: serverAutoArchiveDays },
|
||||
enableReinitialize: true,
|
||||
validationSchema,
|
||||
onSubmit: (values, helpers) => {
|
||||
onSaveAutoArchiveDays(
|
||||
{ auto_archive_days: values.auto_archive_days },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSavedState();
|
||||
setArchiveToggled(null);
|
||||
helpers.resetForm();
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const resetArchiveState = () => {
|
||||
setArchiveToggled(null);
|
||||
form.resetForm();
|
||||
};
|
||||
|
||||
const handleToggleAutoArchive = (checked: boolean) => {
|
||||
if (checked) {
|
||||
const days =
|
||||
serverAutoArchiveDays > 0 ? serverAutoArchiveDays : ENABLE_DEFAULT_DAYS;
|
||||
setArchiveToggled(true);
|
||||
void form.setFieldValue("auto_archive_days", days);
|
||||
onSaveAutoArchiveDays(
|
||||
{ auto_archive_days: days },
|
||||
{
|
||||
onSuccess: resetArchiveState,
|
||||
onError: resetArchiveState,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setArchiveToggled(false);
|
||||
void form.setFieldValue("auto_archive_days", 0);
|
||||
onSaveAutoArchiveDays(
|
||||
{ auto_archive_days: 0 },
|
||||
{
|
||||
onSuccess: resetArchiveState,
|
||||
onError: resetArchiveState,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-2" onSubmit={form.handleSubmit}>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="m-0 text-sm font-semibold text-content-primary">
|
||||
Auto-Archive Inactive Conversations
|
||||
</h3>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isAutoArchiveEnabled}
|
||||
onCheckedChange={handleToggleAutoArchive}
|
||||
aria-label="Enable auto-archive"
|
||||
disabled={isSavingAutoArchiveDays || isAutoArchiveDaysLoading}
|
||||
/>
|
||||
</div>
|
||||
<p className="!mt-0.5 m-0 flex-1 text-xs text-content-secondary">
|
||||
Inactive conversations are automatically archived after this period.
|
||||
Pinned conversations are exempt.
|
||||
</p>
|
||||
{isAutoArchiveEnabled && (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
name="auto_archive_days"
|
||||
min={1}
|
||||
max={3650}
|
||||
step={1}
|
||||
aria-label="Auto-archive period in days"
|
||||
value={form.values.auto_archive_days}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
aria-invalid={Boolean(form.errors.auto_archive_days)}
|
||||
disabled={isSavingAutoArchiveDays || isAutoArchiveDaysLoading}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="flex h-10 w-[120px] items-center px-3 text-sm text-content-secondary">
|
||||
Days
|
||||
</span>
|
||||
</div>
|
||||
{form.errors.auto_archive_days && form.touched.auto_archive_days && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
{form.errors.auto_archive_days}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 flex min-h-6 justify-end">
|
||||
{(form.dirty || isSavedVisible || isSavingAutoArchiveDays) &&
|
||||
(isSavedVisible ? (
|
||||
<TemporarySavedState />
|
||||
) : (
|
||||
<Button
|
||||
size="xs"
|
||||
type="submit"
|
||||
disabled={
|
||||
isSavingAutoArchiveDays ||
|
||||
!form.dirty ||
|
||||
Boolean(form.errors.auto_archive_days)
|
||||
}
|
||||
>
|
||||
{isSavingAutoArchiveDays && (
|
||||
<Spinner loading className="h-4 w-4" />
|
||||
)}
|
||||
Save
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isSaveAutoArchiveDaysError && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
Failed to save auto-archive setting.
|
||||
</p>
|
||||
)}
|
||||
{isAutoArchiveDaysLoadError && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
Failed to load auto-archive setting.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -123,18 +123,25 @@ export const RetentionPeriodSettings: FC<RetentionPeriodSettingsProps> = ({
|
||||
</p>
|
||||
{isRetentionEnabled && (
|
||||
<>
|
||||
<Input
|
||||
type="number"
|
||||
name="retention_days"
|
||||
min={1}
|
||||
max={3650}
|
||||
step={1}
|
||||
aria-label="Conversation retention period in days"
|
||||
value={form.values.retention_days}
|
||||
onChange={form.handleChange}
|
||||
disabled={isSavingRetentionDays || isRetentionDaysLoading}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
name="retention_days"
|
||||
min={1}
|
||||
max={3650}
|
||||
step={1}
|
||||
aria-label="Conversation retention period in days"
|
||||
value={form.values.retention_days}
|
||||
onChange={form.handleChange}
|
||||
onBlur={form.handleBlur}
|
||||
aria-invalid={Boolean(form.errors.retention_days)}
|
||||
disabled={isSavingRetentionDays || isRetentionDaysLoading}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="flex h-10 w-[120px] items-center px-3 text-sm text-content-secondary">
|
||||
Days
|
||||
</span>
|
||||
</div>
|
||||
{form.errors.retention_days && form.touched.retention_days && (
|
||||
<p className="m-0 text-xs text-content-destructive">
|
||||
{form.errors.retention_days}
|
||||
|
||||
Reference in New Issue
Block a user