Remove the closed people pages, keep their user form in settings
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
|
||||
type Params = ReturnType<typeof useFormInit<UserFormObject>> & {
|
||||
onSaved?: (params: { person: BUN_SQLITE_COBBLE_USERS_JOIN; msg: string }) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the user, with a newly picked photo, through `users/save-user`,
|
||||
* which checks every field and user type. Hands the saved user to
|
||||
* `onSaved`, else reloads or opens their page.
|
||||
*/
|
||||
export default async function submitUserForm({
|
||||
form,
|
||||
image,
|
||||
existing_full,
|
||||
loading,
|
||||
setLoading,
|
||||
setStatus,
|
||||
onSaved,
|
||||
}: Params) {
|
||||
if (loading) return;
|
||||
|
||||
if ((form.password || "") != (form.confirm_password || "")) {
|
||||
setStatus({ error: true, msg: "The passwords don't match." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(existing_full?.id ? `Save changes to ${form.first_name || "this user"}?` : `Add ${form.first_name || "this user"}?`)) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await postAdminApi<BUN_SQLITE_COBBLE_USERS_JOIN>({
|
||||
path: "users/save-user",
|
||||
body: {
|
||||
user_id: existing_full?.id || null,
|
||||
first_name: form.first_name,
|
||||
last_name: form.last_name,
|
||||
email: form.email,
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
confirm_password: form.confirm_password,
|
||||
user_types: form.user_types,
|
||||
image: image?.imageBase64Full,
|
||||
},
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!res.success || !res.singleRes?.id) {
|
||||
setStatus({ error: true, msg: res.msg || "Couldn't save the user." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (onSaved) {
|
||||
onSaved({ person: res.singleRes, msg: res.msg || "Saved." });
|
||||
} else if (existing_full?.id) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
window.location.pathname = `/admin/settings/users/${res.singleRes.id}`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import UserForm from "@/src/pages/admin/people/create/(partials)/user-form";
|
||||
import UserForm from "./user-form";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { UneditableUserTypes } from "@/src/dict/user-types-dict";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||
import submitUserForm from "@/src/pages/admin/settings/users/(functions)/submit-user-form";
|
||||
import UserFormName from "./user-form-name";
|
||||
import UserFormAction from "./user-form-action";
|
||||
import UserFormEmail from "./user-form-email";
|
||||
import UserFormUsername from "./user-form-username";
|
||||
import UserFormUserTypes from "./user-form-user-types";
|
||||
import UserFormImage from "./user-form-image";
|
||||
import UserFormPassword from "./user-form-password";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Edits this user; adds one when not given.
|
||||
*/
|
||||
person?: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
/**
|
||||
* Called with the saved user instead of reloading the page.
|
||||
*/
|
||||
onSaved?: (params: { person: BUN_SQLITE_COBBLE_USERS_JOIN; msg: string }) => void;
|
||||
onCancel?: () => void;
|
||||
/**
|
||||
* On a settings page: no Cancel, and the save updates in place.
|
||||
*/
|
||||
embedded?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A user's photo, name, email, username, user types and password, saved
|
||||
* in one go through `users/save-user`. What they reach is set on their
|
||||
* settings page.
|
||||
*/
|
||||
export default function UserForm({
|
||||
person,
|
||||
onSaved,
|
||||
onCancel,
|
||||
embedded,
|
||||
}: Props) {
|
||||
const init = useFormInit<UserFormObject>({
|
||||
existing: person
|
||||
? {
|
||||
first_name: person.first_name,
|
||||
last_name: person.last_name,
|
||||
email: person.email,
|
||||
username: person.username,
|
||||
user_types: parseUserTypes({ user_types: person.user_types }).filter(
|
||||
(type) => !UneditableUserTypes.includes(type),
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
existing_full: person ? { id: person.id } : undefined,
|
||||
default: { user_types: ["member"] },
|
||||
});
|
||||
|
||||
return (
|
||||
<Form className="w-full gap-4! items-stretch pt-4" onSubmit={() => submitUserForm({ ...init, onSaved })}>
|
||||
<UserFormImage {...init} person={person} />
|
||||
<UserFormName {...init} />
|
||||
<Row className="w-full grid sm:grid-cols-2 gap-4!">
|
||||
<UserFormEmail {...init} />
|
||||
<UserFormUsername {...init} />
|
||||
</Row>
|
||||
<UserFormUserTypes {...init} person={person} />
|
||||
<UserFormPassword {...init} />
|
||||
<UserFormAction
|
||||
{...init}
|
||||
onCancel={onCancel}
|
||||
hide_cancel={embedded}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import AddFormActions from "@/src/layouts/bots/(partials)/add-form-actions";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
|
||||
type Props = ReturnType<typeof useFormInit<UserFormObject>> & {
|
||||
onCancel?: () => void;
|
||||
/**
|
||||
* On a settings page, where there is nothing to cancel into.
|
||||
*/
|
||||
hide_cancel?: boolean;
|
||||
};
|
||||
|
||||
export default function UserFormAction({
|
||||
existing_full,
|
||||
loading,
|
||||
status,
|
||||
onCancel,
|
||||
hide_cancel,
|
||||
}: Props) {
|
||||
return (
|
||||
<AddFormActions
|
||||
submit_label={existing_full?.id ? "Save" : "Add user"}
|
||||
loading={loading}
|
||||
error={status?.error ? status.msg : undefined}
|
||||
onCancel={
|
||||
hide_cancel
|
||||
? undefined
|
||||
: onCancel || (() => window.history.back())
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { BUN_SQLITE_COBBLE_USERS } from "@/db/types/db";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
|
||||
export default function UserFormEmail({
|
||||
setForm,
|
||||
form,
|
||||
}: ReturnType<typeof useFormInit<BUN_SQLITE_COBBLE_USERS>>) {
|
||||
return (
|
||||
<Input
|
||||
id="user-form-email"
|
||||
title="Email"
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
email: e.target.value,
|
||||
}));
|
||||
}}
|
||||
defaultValue={form.email}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import ImageUpload from "@moduletrace/twui/form/ImageUpload";
|
||||
import grabMediaURL from "@/src/functions/backend/media/grab-media-url";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
|
||||
type Props = ReturnType<typeof useFormInit<UserFormObject>> & {
|
||||
person?: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
};
|
||||
|
||||
/**
|
||||
* Their profile photo, saved with the rest of the form.
|
||||
*/
|
||||
export default function UserFormImage({ person, setImage }: Props) {
|
||||
const user_image_url = person?.profile_media_id
|
||||
? grabMediaURL({ media: { id: person.profile_media_id } }).media_url
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ImageUpload
|
||||
label="Photo"
|
||||
existingImageUrl={user_image_url}
|
||||
onChangeHandler={(imgData) => setImage(imgData)}
|
||||
restoreImageFn={() => setImage(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { BUN_SQLITE_COBBLE_USERS } from "@/db/types/db";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
|
||||
export default function UserFormName({
|
||||
setForm,
|
||||
form,
|
||||
}: ReturnType<typeof useFormInit<BUN_SQLITE_COBBLE_USERS>>) {
|
||||
return (
|
||||
<Row className="w-full grid sm:grid-cols-2 gap-4!">
|
||||
<Input
|
||||
id="user-form-first-name"
|
||||
title="First name"
|
||||
label="First name"
|
||||
placeholder="Ada"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
first_name: e.target.value,
|
||||
}));
|
||||
}}
|
||||
defaultValue={form.first_name}
|
||||
autoComplete="off"
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="user-form-last-name"
|
||||
title="Last name"
|
||||
label="Last name"
|
||||
placeholder="Lovelace"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
last_name: e.target.value,
|
||||
}));
|
||||
}}
|
||||
defaultValue={form.last_name}
|
||||
autoComplete="off"
|
||||
showLabel
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
|
||||
/**
|
||||
* The password, then the same again to confirm it. Editing someone, both
|
||||
* may stay empty to keep theirs.
|
||||
*/
|
||||
export default function UserFormPassword({ setForm, existing_full }: ReturnType<typeof useFormInit<UserFormObject>>) {
|
||||
const optional = Boolean(existing_full?.id);
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-3! items-stretch pt-2 border-0 border-t border-solid border-hairline">
|
||||
<Stack className="gap-0.5!">
|
||||
<Span className="text-sm font-bold leading-5 text-ink">{optional ? "New password" : "Password"}</Span>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{`At least ${AppData.PasswordMinLength} characters.${optional ? " Leave both empty to keep theirs." : " They sign in with it."}`}
|
||||
</Span>
|
||||
</Stack>
|
||||
<Input
|
||||
id="user-form-password"
|
||||
title="Password"
|
||||
label="Password"
|
||||
showLabel
|
||||
type="password"
|
||||
placeholder={optional ? "Leave empty to keep it" : "A strong password"}
|
||||
autoComplete="new-password"
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
id="user-form-confirm-password"
|
||||
title="Confirm password"
|
||||
label="Confirm password"
|
||||
showLabel
|
||||
type="password"
|
||||
placeholder="The same password again"
|
||||
autoComplete="new-password"
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, confirm_password: e.target.value }))}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import SearchSelect from "@moduletrace/twui/form/SearchSelect";
|
||||
import { UneditableUserTypes, UserTypes } from "@/src/dict/user-types-dict";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { UserFormObject, UserType } from "@/src/types";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||
import filterAssignableUserTypes from "@/src/utils/rbac/filter-assignable-user-types";
|
||||
|
||||
type Props = ReturnType<typeof useFormInit<UserFormObject>> & {
|
||||
person?: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
};
|
||||
|
||||
/**
|
||||
* Picks a user's types, offering only those the session user may grant
|
||||
* (never `super_admin`). The super admin's types aren't shown: they never
|
||||
* change here.
|
||||
*/
|
||||
export default function UserFormUserTypes({
|
||||
setForm,
|
||||
form,
|
||||
person,
|
||||
app_context,
|
||||
}: Props) {
|
||||
if (parseUserTypes({ user_types: person?.user_types }).includes("super_admin")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assignable = filterAssignableUserTypes({
|
||||
actor_user_types: app_context.pageProps?.user_types,
|
||||
user_types: UserTypes.map((type) => type.value).filter((type) => !UneditableUserTypes.includes(type)),
|
||||
});
|
||||
|
||||
return (
|
||||
<SearchSelect
|
||||
id="user-form-user-types"
|
||||
title="User types"
|
||||
label="User types"
|
||||
showLabel
|
||||
multiple
|
||||
placeholder="Pick their user types"
|
||||
options={UserTypes.filter((type) => assignable.includes(type.value)).map((type) => ({
|
||||
value: type.value,
|
||||
title: type.title,
|
||||
}))}
|
||||
defaultValues={form.user_types || []}
|
||||
info="At least one, or they can't sign in."
|
||||
changeHandler={(value) => {
|
||||
const user_types = (Array.isArray(value) ? value : value ? [value] : []) as UserType[];
|
||||
setForm((prev) => ({ ...prev, user_types }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { BUN_SQLITE_COBBLE_USERS } from "@/db/types/db";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
|
||||
export default function UserFormUsername({
|
||||
setForm,
|
||||
form,
|
||||
}: ReturnType<typeof useFormInit<BUN_SQLITE_COBBLE_USERS>>) {
|
||||
return (
|
||||
<Input
|
||||
id="user-form-username"
|
||||
title="Username"
|
||||
label="Username"
|
||||
placeholder="ada"
|
||||
autoComplete="off"
|
||||
info="Lowercase letters, numbers, dots, dashes or underscores."
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
username: e.target.value,
|
||||
}));
|
||||
}}
|
||||
defaultValue={form.username}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import UserForm from "@/src/pages/admin/people/create/(partials)/user-form";
|
||||
import UserForm from "../../(partials)/user-form";
|
||||
import SettingsGroup from "../../../(partials)/settings-group";
|
||||
import SettingsStatus from "../../../(partials)/settings-status";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user