Remove the closed people pages, keep their user form in settings
This commit is contained in:
@@ -54,7 +54,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
|
|||||||
### Auth
|
### Auth
|
||||||
|
|
||||||
- **Auth guard:** `src/pages/__root.server.ts` - server function that runs on every request.
|
- **Auth guard:** `src/pages/__root.server.ts` - server function that runs on every request.
|
||||||
- No public pages: anything outside the live admin sections (`AdminSections` in `src/dict/admin-sections-dict.ts`: `/admin/bots`, `/admin/workspaces`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`) and `/auth` redirects to `AdminHomePath` (`/admin/bots`). `/admin` itself only redirects there. People exists in code but is closed until added back to `AdminSections`
|
- No public pages: anything outside the live admin sections (`AdminSections` in `src/dict/admin-sections-dict.ts`: `/admin/bots`, `/admin/workspaces`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`) and `/auth` redirects to `AdminHomePath` (`/admin/bots`). `/admin` itself only redirects there
|
||||||
- No users at all: everything redirects to `/auth/signup` (first-run super admin setup, `grabHasUsers`); once a user exists `/auth/signup` redirects away
|
- No users at all: everything redirects to `/auth/signup` (first-run super admin setup, `grabHasUsers`); once a user exists `/auth/signup` redirects away
|
||||||
- Redirects unauthenticated `/admin/*` → `/auth/login`
|
- Redirects unauthenticated `/admin/*` → `/auth/login`
|
||||||
- Unknown `/admin/settings/*` pages (not in `SettingsPages`, `src/utils/is-unknown-settings-path.ts`) redirect to `/admin/settings/general`
|
- Unknown `/admin/settings/*` pages (not in `SettingsPages`, `src/utils/is-unknown-settings-path.ts`) redirect to `/admin/settings/general`
|
||||||
@@ -150,16 +150,15 @@ Envs are set in Settings, under Environment (super admin only), not in `.env`. T
|
|||||||
|
|
||||||
## Admin Route Hierarchy
|
## Admin Route Hierarchy
|
||||||
|
|
||||||
Live routes under `/admin` (see `AdminSections`): `/admin/bots`, `/admin/workspaces` (the index opens the first workspace), `/admin/routines`, `/admin/logs`, `/admin/help` and `/admin/settings`. `/admin` redirects to `/admin/bots`; a workspace's dashboard is `/admin/workspaces/<id>`. The rest below are built but closed for now (redirect to `/admin/bots`):
|
Live routes under `/admin` (see `AdminSections`): `/admin/bots`, `/admin/workspaces` (the index opens the first workspace), `/admin/routines`, `/admin/logs`, `/admin/help` and `/admin/settings`. `/admin` redirects to `/admin/bots`; a workspace's dashboard is `/admin/workspaces/<id>`. The routes in detail:
|
||||||
|
|
||||||
- `/admin/people` - admin users list, plus `create/`, `[user_id]/` (profile), `[user_id]/edit/`
|
|
||||||
- `/admin/bots` (live) - the Cobble chat shell, plus `[bot_id]/`
|
- `/admin/bots` (live) - the Cobble chat shell, plus `[bot_id]/`
|
||||||
- `/admin/routines` (live) - every schedule the user's bots have (see Schedules), opened from "Routines" in the rail's user popover
|
- `/admin/routines` (live) - every schedule the user's bots have (see Schedules), opened from "Routines" in the rail's user popover
|
||||||
- `/admin/logs` (live) - Activity, the audit log: super admins only (others are redirected; the popover's "Activity" item shows to super admins only). Rendered in `SettingsShell` with `wide`
|
- `/admin/logs` (live) - Activity, the audit log: super admins only (others are redirected; the popover's "Activity" item shows to super admins only). Rendered in `SettingsShell` with `wide`
|
||||||
- `/admin/help` (live) - familiar problems and their fixes, one `SettingsGroup` per topic (`(data)/help-topics.ts`, markdown rendered with the chat's `MessageMarkdownComponents`, the topic id as the group's anchor). First topic: signing in to Google and other strict sites with Cookie-Editor over the Desktop view (`HelpSignInTopicId` in `src/dict/help-dict.ts`; `ask_user_to_take_over` points bots to it)
|
- `/admin/help` (live) - familiar problems and their fixes, one `SettingsGroup` per topic (`(data)/help-topics.ts`, markdown rendered with the chat's `MessageMarkdownComponents`, the topic id as the group's anchor). First topic: signing in to Google and other strict sites with Cookie-Editor over the Desktop view (`HelpSignInTopicId` in `src/dict/help-dict.ts`; `ask_user_to_take_over` points bots to it)
|
||||||
- Routines, Activity and Help are the `RailPages` (`src/dict/rail-pages-dict.ts`), opened from the rail's user popover (which lists them, then Settings and Sign out). On them the rail shows its own aside (`(partials)/rail-pages-list.tsx`): a back link to the bots, then those pages, like settings
|
- Routines, Activity and Help are the `RailPages` (`src/dict/rail-pages-dict.ts`), opened from the rail's user popover (which lists them, then Settings and Sign out). On them the rail shows its own aside (`(partials)/rail-pages-list.tsx`): a back link to the bots, then those pages, like settings
|
||||||
- `/admin/settings/backups` - super admin only (listed in `SettingsPages`): create/list/download database snapshots in `db/.backups`, plus `restore/` (restore a listed backup, or upload a SQLite database file which is saved to `.backups` then restored). Shells out to the `bun-sqlite` CLI (`backup`, and `import <file>` since `restore` is interactive) via `src/functions/backend/backups/`; API routes in `api/admin/backups/`
|
- `/admin/settings/backups` - super admin only (listed in `SettingsPages`): create/list/download database snapshots in `db/.backups`, plus `restore/` (restore a listed backup, or upload a SQLite database file which is saved to `.backups` then restored). Shells out to the `bun-sqlite` CLI (`backup`, and `import <file>` since `restore` is interactive) via `src/functions/backend/backups/`; API routes in `api/admin/backups/`
|
||||||
- `/admin/settings` (live) - redirects to `SettingsDefaultPath` (`/admin/settings/general`). Sub pages: `general/` (theme: System/Light/Dark per device via `useThemePreference`, stored in `localStorage[AppData.ThemeStorageKey]`, the same key `ThemeInitScript` and twui's `ColorSchemeSelector` read; applies instantly, no save bar), `profile/` (current user profile), `notifications/` (this device's Web Push switch, a test send, and every device the user subscribed), `change-password/` (set/change own password) `models/` (super admin only, `super_admin_only` in the dict hides it from the rail: model providers and which model each harness uses) and `environment/` (super admin only: app settings and the envs bots and computers use, per scope), each wrapped in `SettingsShell` (`(partials)/settings-shell.tsx`: 64px pane header like the conversation header, then one centered column). Content is built from `SettingsGroup` blocks (label + description left, controls right, hairline-separated, no cards) and ends with the sticky `SettingsSaveBar` (Save enabled only when dirty, Discard, one-line status). Saves don't reload the page. Keep save notices in component state: `useFormInit` clears `status` whenever the form changes. `bots/` (the crew: chat, edit with `BotForm`, models, delete a worker via `api/admin/bots/delete-bot`), `workspaces/` (open, create through the Add flow, delete), `computers/` (state and resources, live; super admins create, edit with `AddComputerForm`, retry, delete), `mcps/` and `tools/` (see MCPs and custom tools: super admins add, edit, refresh or try, delete; admins only set Access, `extension-access-modal.tsx`; add, edit, access and try open in modals (`AddModalShell`)), each row with a status line under it (`extension-status-line.tsx`: connected or not, last run worked or failed, when, and why), those five for `canManageCrew` only (`grab-crew-settings-server-props.ts`, `can_view` in the dict) and `users/` (for `canListUsers`: super admins and admins; the dict's `can_view` gates it in the rail; add with a password (`canCreateUser`), edit (`canUpdateUser`), both with the one `UserForm` (`admin/people/create/(partials)/user-form/`, `useFormInit`, photo included, saved in one request), revoke (adds `revoked`, `canRevokeUser`), delete (`canRevokeUser`'s rules, never yourself), pick their user types from `UserTypes` (`user-types-select.tsx`: only the ones `filterAssignableUserTypes` allows, never `UneditableUserTypes`; at least one, since login needs a type; `saveUser` checks each grant and removal with `userTypeAssignment`; the super admin's types are never shown or changed there, and they can't be revoked; the password is entered twice, `SaveUserRequestSchema` checks they match), delete with everything theirs; `api/admin/users/save-user`, `delete-user`). The lists share `SettingsList`, `SettingsListRow`, `SettingsRowAction` and `use-settings-list`. Sub pages are declared once in `src/dict/settings-pages-dict.ts`; on every `/admin/settings/*` page the rail swaps the crew for a back link plus that list. Add a sub page by adding its folder and a dict entry
|
- `/admin/settings` (live) - redirects to `SettingsDefaultPath` (`/admin/settings/general`). Sub pages: `general/` (theme: System/Light/Dark per device via `useThemePreference`, stored in `localStorage[AppData.ThemeStorageKey]`, the same key `ThemeInitScript` and twui's `ColorSchemeSelector` read; applies instantly, no save bar), `profile/` (current user profile), `notifications/` (this device's Web Push switch, a test send, and every device the user subscribed), `change-password/` (set/change own password) `models/` (super admin only, `super_admin_only` in the dict hides it from the rail: model providers and which model each harness uses) and `environment/` (super admin only: app settings and the envs bots and computers use, per scope), each wrapped in `SettingsShell` (`(partials)/settings-shell.tsx`: 64px pane header like the conversation header, then one centered column). Content is built from `SettingsGroup` blocks (label + description left, controls right, hairline-separated, no cards) and ends with the sticky `SettingsSaveBar` (Save enabled only when dirty, Discard, one-line status). Saves don't reload the page. Keep save notices in component state: `useFormInit` clears `status` whenever the form changes. `bots/` (the crew: chat, edit with `BotForm`, models, delete a worker via `api/admin/bots/delete-bot`), `workspaces/` (open, create through the Add flow, delete), `computers/` (state and resources, live; super admins create, edit with `AddComputerForm`, retry, delete), `mcps/` and `tools/` (see MCPs and custom tools: super admins add, edit, refresh or try, delete; admins only set Access, `extension-access-modal.tsx`; add, edit, access and try open in modals (`AddModalShell`)), each row with a status line under it (`extension-status-line.tsx`: connected or not, last run worked or failed, when, and why), those five for `canManageCrew` only (`grab-crew-settings-server-props.ts`, `can_view` in the dict) and `users/` (for `canListUsers`: super admins and admins; the dict's `can_view` gates it in the rail; add with a password (`canCreateUser`), edit (`canUpdateUser`), both with the one `UserForm` (`admin/settings/users/(partials)/user-form/`, `useFormInit`, photo included, saved in one request), revoke (adds `revoked`, `canRevokeUser`), delete (`canRevokeUser`'s rules, never yourself), pick their user types from `UserTypes` (`user-types-select.tsx`: only the ones `filterAssignableUserTypes` allows, never `UneditableUserTypes`; at least one, since login needs a type; `saveUser` checks each grant and removal with `userTypeAssignment`; the super admin's types are never shown or changed there, and they can't be revoked; the password is entered twice, `SaveUserRequestSchema` checks they match), delete with everything theirs; `api/admin/users/save-user`, `delete-user`). The lists share `SettingsList`, `SettingsListRow`, `SettingsRowAction` and `use-settings-list`. Sub pages are declared once in `src/dict/settings-pages-dict.ts`; on every `/admin/settings/*` page the rail swaps the crew for a back link plus that list. Add a sub page by adding its folder and a dict entry
|
||||||
|
|
||||||
Every `/admin/*` page renders in the bots layout (`src/layouts/bots/`); there is no separate admin layout or aside. The rail header's Add icon (super admins and admins only, like the rail's "+" buttons) opens the Add flow (`(partials)/add-flow.tsx`, `AddFlowContext`): a bot, a workspace or (super admins) a computer, each form in a modal stacked over the choice, so closing it goes back there (the Bots and Workspaces labels have a "+" that opens the form directly); on a workspace's pages new bots and computers join it. The rail shows the crew (`(partials)/rail-crew-list.tsx`, needs `bots` and `workspaces` in page props): the coordinator, the latest `AppData.RailRecentBots` workers (plus any open, unread or needing you, `pick-rail-bots`), "See all bots" with a few stones opening every bot in a modal, then Workspaces with a "+". On `/admin/workspaces/*` it shows the workspace's crew instead (`(partials)/rail-workspace-list.tsx`: back link, the workspace, then only the bots attached to it, "Workspace bots"; the coordinator stays in the main crew rail, and on a workspace bot's page the back link returns to the workspace page), except on `/admin/settings/*` where it shows "Back to bots" plus the settings sub pages (`(partials)/rail-settings-list.tsx`). Page heroes use the shared `AdminHero` component (`src/components/general/admin-hero.tsx`).
|
Every `/admin/*` page renders in the bots layout (`src/layouts/bots/`); there is no separate admin layout or aside. The rail header's Add icon (super admins and admins only, like the rail's "+" buttons) opens the Add flow (`(partials)/add-flow.tsx`, `AddFlowContext`): a bot, a workspace or (super admins) a computer, each form in a modal stacked over the choice, so closing it goes back there (the Bots and Workspaces labels have a "+" that opens the form directly); on a workspace's pages new bots and computers join it. The rail shows the crew (`(partials)/rail-crew-list.tsx`, needs `bots` and `workspaces` in page props): the coordinator, the latest `AppData.RailRecentBots` workers (plus any open, unread or needing you, `pick-rail-bots`), "See all bots" with a few stones opening every bot in a modal, then Workspaces with a "+". On `/admin/workspaces/*` it shows the workspace's crew instead (`(partials)/rail-workspace-list.tsx`: back link, the workspace, then only the bots attached to it, "Workspace bots"; the coordinator stays in the main crew rail, and on a workspace bot's page the back link returns to the workspace page), except on `/admin/settings/*` where it shows "Back to bots" plus the settings sub pages (`(partials)/rail-settings-list.tsx`). Page heroes use the shared `AdminHero` component (`src/components/general/admin-hero.tsx`).
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* The admin sections that are live. Any other `/admin/*` path redirects to
|
* The admin sections that are live. Any other `/admin/*` path redirects to
|
||||||
* `AdminHomePath`. People exists in code but stays closed until it is added
|
* `AdminHomePath`. Logs (Activity) is super admins only.
|
||||||
* back here. Logs (Activity) is super admins only.
|
|
||||||
*/
|
*/
|
||||||
export const AdminSections = [
|
export const AdminSections = [
|
||||||
"/admin/bots",
|
"/admin/bots",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function LogDetailsModal({
|
|||||||
{
|
{
|
||||||
label: "User",
|
label: "User",
|
||||||
value: log.user_id ? (
|
value: log.user_id ? (
|
||||||
<Link href={`/admin/people/${log.user_id}`}>
|
<Link href={`/admin/settings/users/${log.user_id}`}>
|
||||||
{user_name || "User"} #{log.user_id}
|
{user_name || "User"} #{log.user_id}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { AppData } from "@/src/data/app-data";
|
|
||||||
import { useContext, useRef } from "react";
|
|
||||||
import useStatus from "@moduletrace/twui/hooks/useStatus";
|
|
||||||
import type { ApiReqParams, UserType } from "@/src/types";
|
|
||||||
import { AppContext } from "@/src/pages/__root";
|
|
||||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
|
||||||
import EJSON from "@/src/utils/ejson";
|
|
||||||
import { useUsersGet } from "@/src/hooks/use-users-get";
|
|
||||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
users_type?: UserType | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function useAllPeopleListInit({ users_type }: Props) {
|
|
||||||
const { pageProps, query } = useContext(AppContext);
|
|
||||||
const { user_types, url } = pageProps || {};
|
|
||||||
|
|
||||||
const limit = AppData["DefaultQueryLimit"];
|
|
||||||
const can_view_users = checkUserAccess({
|
|
||||||
user_types,
|
|
||||||
includes: ["admin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
let location_query = EJSON.parse(url?.searchParams?.q || "") as
|
|
||||||
| ApiReqParams
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (query?.user_type) {
|
|
||||||
if (!location_query) {
|
|
||||||
location_query = {
|
|
||||||
target_user_type: query.user_type as any,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
location_query.target_user_type = query.user_type as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
users: people,
|
|
||||||
count,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
setUsersQuery,
|
|
||||||
setUsers: set_people,
|
|
||||||
ready,
|
|
||||||
} = useUsersGet({
|
|
||||||
query: location_query,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selected = useRef(new Set<BUN_SQLITE_COBBLE_USERS_JOIN>());
|
|
||||||
|
|
||||||
const { refresh, setRefresh } = useStatus();
|
|
||||||
|
|
||||||
return {
|
|
||||||
user_types,
|
|
||||||
limit,
|
|
||||||
can_view_users,
|
|
||||||
people,
|
|
||||||
count,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
setUsersQuery,
|
|
||||||
set_people,
|
|
||||||
selected,
|
|
||||||
refresh,
|
|
||||||
setRefresh,
|
|
||||||
users_type,
|
|
||||||
location_query,
|
|
||||||
ready,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import Search from "@moduletrace/twui/elements/Search";
|
|
||||||
import Select from "@moduletrace/twui/form/Select";
|
|
||||||
import Divider from "@moduletrace/twui/layout/Divider";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import _ from "lodash";
|
|
||||||
import twuiSlugToNormalText from "@moduletrace/twui/utils/slug-to-normal-text";
|
|
||||||
import type useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
|
|
||||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
|
||||||
import searchableFieldToQueryField from "@/src/utils/searchable-field-to-query-field";
|
|
||||||
import type { ApiReqParams } from "@/src/types";
|
|
||||||
|
|
||||||
export default function AllPeopleListSearch({
|
|
||||||
setUsersQuery,
|
|
||||||
location_query,
|
|
||||||
}: ReturnType<typeof useAllPeopleListInit>) {
|
|
||||||
const searchable_fields: (keyof BUN_SQLITE_COBBLE_USERS_JOIN)[] = [
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"user_types",
|
|
||||||
];
|
|
||||||
|
|
||||||
const initial_search_field = (Object.keys(location_query || {}).filter(
|
|
||||||
(q) => q.startsWith("search_term"),
|
|
||||||
)?.[0] ||
|
|
||||||
searchableFieldToQueryField({
|
|
||||||
field: searchable_fields[0]!,
|
|
||||||
})) as keyof ApiReqParams;
|
|
||||||
|
|
||||||
const initial_search = String(location_query?.[initial_search_field] || "");
|
|
||||||
|
|
||||||
const search_field_ref = useRef(initial_search_field);
|
|
||||||
const search_value_ref = useRef(initial_search);
|
|
||||||
const initialized_ref = useRef(false);
|
|
||||||
|
|
||||||
function appendQuery() {
|
|
||||||
setUsersQuery((prev) => {
|
|
||||||
let nq = _.cloneDeep(prev) || {};
|
|
||||||
|
|
||||||
for (let i = 0; i < searchable_fields.length; i++) {
|
|
||||||
const sf = searchableFieldToQueryField({
|
|
||||||
field: searchable_fields[i]!,
|
|
||||||
});
|
|
||||||
delete nq[sf];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search_value_ref.current.match(/./)) {
|
|
||||||
nq[search_field_ref.current] = search_value_ref.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
nq.page = 1;
|
|
||||||
|
|
||||||
if (_.isEqual(nq, prev)) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nq;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Search
|
|
||||||
placeholder="Find People"
|
|
||||||
changeHandler={(value) => {
|
|
||||||
search_value_ref.current = value || "";
|
|
||||||
|
|
||||||
if (!initialized_ref.current) {
|
|
||||||
initialized_ref.current = true;
|
|
||||||
if ((value || "") === initial_search) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appendQuery();
|
|
||||||
}}
|
|
||||||
defaultValue={initial_search}
|
|
||||||
no_search_button
|
|
||||||
inputProps={{
|
|
||||||
suffix: (
|
|
||||||
<>
|
|
||||||
<Divider vertical className="-my-4 -mr-2 h-9" />
|
|
||||||
<Select
|
|
||||||
options={searchable_fields.map((sf) => ({
|
|
||||||
value: sf,
|
|
||||||
default:
|
|
||||||
searchableFieldToQueryField({
|
|
||||||
field: sf,
|
|
||||||
}) == search_field_ref.current,
|
|
||||||
title: twuiSlugToNormalText(
|
|
||||||
sf.replace(/user_types/, "user_type"),
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
className="-my-4 min-w-[80px] xs:min-w-[150px] -mr-2 pl-4"
|
|
||||||
wrapperWrapperProps={{
|
|
||||||
className: "-mr-2",
|
|
||||||
}}
|
|
||||||
wrapperProps={{
|
|
||||||
className: "border-none!",
|
|
||||||
}}
|
|
||||||
changeHandler={(value) => {
|
|
||||||
search_field_ref.current =
|
|
||||||
searchableFieldToQueryField({
|
|
||||||
field: value,
|
|
||||||
});
|
|
||||||
appendQuery();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
autoFocus: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import Row from "@moduletrace/twui/layout/Row";
|
|
||||||
import _ from "lodash";
|
|
||||||
import Span from "@moduletrace/twui/layout/Span";
|
|
||||||
import Card from "@moduletrace/twui/elements/Card";
|
|
||||||
import type { RefObject } from "react";
|
|
||||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selected: RefObject<Set<BUN_SQLITE_COBBLE_USERS_JOIN>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AllPeopleListSelections({ selected }: Props) {
|
|
||||||
const total = Array.from(selected.current.entries()).length;
|
|
||||||
|
|
||||||
if (!total) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="py-2! px-3! w-full">
|
|
||||||
<Row>
|
|
||||||
<Span>
|
|
||||||
{Array.from(selected.current.entries()).length} items
|
|
||||||
selected
|
|
||||||
</Span>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Row from "@moduletrace/twui/layout/Row";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { AppData } from "@/src/data/app-data";
|
|
||||||
import type useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
|
|
||||||
import AllPeopleListSearch from "./all-people-list-search";
|
|
||||||
import AllPeopleListUsersTypeSelect from "./all-people-list-users-type-select";
|
|
||||||
|
|
||||||
export default function AllPeopleListTitleSection(
|
|
||||||
props: ReturnType<typeof useAllPeopleListInit>,
|
|
||||||
) {
|
|
||||||
const { users_type, people, page, count } = props;
|
|
||||||
|
|
||||||
const limit = AppData["DefaultQueryLimit"];
|
|
||||||
|
|
||||||
const range_start = people?.length ? (page - 1) * limit + 1 : 0;
|
|
||||||
const range_end = people?.length ? range_start + people.length - 1 : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row className="flex items-center justify-between gap-4 w-full">
|
|
||||||
<Row>
|
|
||||||
<AllPeopleListSearch {...props} />
|
|
||||||
{users_type ? null : (
|
|
||||||
<AllPeopleListUsersTypeSelect {...props} />
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{typeof count === "number" && count > 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Showing {range_start}-{range_end} of {count} users
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
No Results
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import type useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
|
|
||||||
import { UserTypes } from "@/src/dict/user-types-dict";
|
|
||||||
import SearchSelect from "@moduletrace/twui/form/SearchSelect";
|
|
||||||
|
|
||||||
export default function AllPeopleListUsersTypeSelect({
|
|
||||||
setUsersQuery,
|
|
||||||
location_query,
|
|
||||||
}: ReturnType<typeof useAllPeopleListInit>) {
|
|
||||||
const initial_user_type = String(location_query?.target_user_type || "");
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{ value: "N/A", label: "All Users" },
|
|
||||||
...UserTypes.map((ut) => ({
|
|
||||||
value: ut.value,
|
|
||||||
label: ut.value,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<SearchSelect
|
|
||||||
options={options}
|
|
||||||
defaultValues={[initial_user_type]}
|
|
||||||
changeHandler={(value) => {
|
|
||||||
setUsersQuery((prev) => {
|
|
||||||
const new_query = _.cloneDeep(prev) || {};
|
|
||||||
if (!value || value === "N/A") {
|
|
||||||
delete new_query?.target_user_type;
|
|
||||||
} else {
|
|
||||||
new_query.target_user_type = value as any;
|
|
||||||
}
|
|
||||||
return new_query;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import Card from "@moduletrace/twui/elements/Card";
|
|
||||||
import LucideIcon from "@moduletrace/twui/elements/lucide-icon";
|
|
||||||
import Tag from "@moduletrace/twui/elements/Tag";
|
|
||||||
import Button from "@moduletrace/twui/layout/Button";
|
|
||||||
import H3 from "@moduletrace/twui/layout/H3";
|
|
||||||
import Img from "@moduletrace/twui/layout/Img";
|
|
||||||
import Row from "@moduletrace/twui/layout/Row";
|
|
||||||
import Span from "@moduletrace/twui/layout/Span";
|
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
|
||||||
import grabMediaURL from "@/src/functions/backend/media/grab-media-url";
|
|
||||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
|
||||||
import { AppContext } from "@/src/pages/__root";
|
|
||||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import UserAvatar from "@/src/components/general/user-avatar";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
person: BUN_SQLITE_COBBLE_USERS_JOIN;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PeopleListCard({ person }: Props) {
|
|
||||||
const { pageProps } = useContext(AppContext);
|
|
||||||
const is_read_only = isReadOnlyUser({
|
|
||||||
user_types: pageProps?.user_types,
|
|
||||||
});
|
|
||||||
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
|
|
||||||
const person_profile_image = person.profile_media_id
|
|
||||||
? grabMediaURL({
|
|
||||||
media: { id: person.profile_media_id },
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const person_url = `/admin/people/${person.id}`;
|
|
||||||
const edit_person_url = `/admin/people/${person.id}/edit`;
|
|
||||||
|
|
||||||
const date_created = dayjs(Number(person.created_at));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={(e) => {
|
|
||||||
const target_el = e.target as HTMLElement;
|
|
||||||
|
|
||||||
if (target_el.closest(".cancel-link")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.ctrlKey) {
|
|
||||||
window.open(person_url, "__blank");
|
|
||||||
} else {
|
|
||||||
window.location.pathname = person_url;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Row className="w-full justify-between">
|
|
||||||
<Row>
|
|
||||||
<UserAvatar person={person} size="md" />
|
|
||||||
<Img
|
|
||||||
alt={`${person_name}`}
|
|
||||||
src={
|
|
||||||
person_profile_image?.media_url || "/icons/man.png"
|
|
||||||
}
|
|
||||||
size={60}
|
|
||||||
circle
|
|
||||||
/>
|
|
||||||
<Stack className="gap-2">
|
|
||||||
<H3>{person_name}</H3>
|
|
||||||
<Row className="gap-2!">
|
|
||||||
{person.user_types?.split(",").map((ut, i) => (
|
|
||||||
<Tag key={i} color="gray">
|
|
||||||
{ut}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
<Row className="gap-2!">
|
|
||||||
<LucideIcon
|
|
||||||
name="Clock"
|
|
||||||
size={14}
|
|
||||||
className="opacity-50"
|
|
||||||
/>
|
|
||||||
<Span variant="faded" className="text-xs">
|
|
||||||
{date_created.toString()}
|
|
||||||
</Span>
|
|
||||||
</Row>
|
|
||||||
</Stack>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row className="cancel-link">
|
|
||||||
{is_read_only ? null : (
|
|
||||||
<Button
|
|
||||||
title="Edit Person"
|
|
||||||
href={edit_person_url}
|
|
||||||
variant="ghost"
|
|
||||||
color="gray"
|
|
||||||
>
|
|
||||||
<LucideIcon name="Edit2" size={18} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import Loading from "@moduletrace/twui/elements/Loading";
|
|
||||||
import Center from "@moduletrace/twui/layout/Center";
|
|
||||||
import Span from "@moduletrace/twui/layout/Span";
|
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
|
||||||
import PeopleListCard from "./people-list-card";
|
|
||||||
import Paper from "@moduletrace/twui/elements/Paper";
|
|
||||||
import AllPeopleListSelections from "./all-people-list-selections";
|
|
||||||
import useAllPeopleListInit from "../(hooks)/use-all-people-list-init";
|
|
||||||
import AllPeopleListSearch from "./all-people-list-search";
|
|
||||||
|
|
||||||
export default function PeopleList() {
|
|
||||||
const init = useAllPeopleListInit({});
|
|
||||||
|
|
||||||
const { people } = init;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack className="w-full items-stretch">
|
|
||||||
<AllPeopleListSearch {...init} />
|
|
||||||
<AllPeopleListSelections {...init} />
|
|
||||||
|
|
||||||
{!people ? (
|
|
||||||
<Paper>
|
|
||||||
<Center className="p-20">
|
|
||||||
<Loading />
|
|
||||||
</Center>
|
|
||||||
</Paper>
|
|
||||||
) : !people[0] ? (
|
|
||||||
<Paper>
|
|
||||||
<Center className="p-20">
|
|
||||||
<Span>No People Found!</Span>
|
|
||||||
</Center>
|
|
||||||
</Paper>
|
|
||||||
) : (
|
|
||||||
people.map((person, index) => {
|
|
||||||
return <PeopleListCard person={person} key={index} />;
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import AdminHero from "@/src/components/general/admin-hero";
|
|
||||||
import Button from "@moduletrace/twui/layout/Button";
|
|
||||||
import { AppContext } from "@/src/pages/__root";
|
|
||||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function Hero() {
|
|
||||||
const { pageProps } = useContext(AppContext);
|
|
||||||
const is_read_only = isReadOnlyUser({ user_types: pageProps?.user_types });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminHero
|
|
||||||
title="People"
|
|
||||||
buttons={
|
|
||||||
<>
|
|
||||||
{is_read_only ? null : (
|
|
||||||
<Button
|
|
||||||
title="Create New Person"
|
|
||||||
href="/admin/people/create"
|
|
||||||
>
|
|
||||||
Create New Person
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import Section from "@moduletrace/twui/layout/Section";
|
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
|
||||||
import PeopleList from "../(partials)/people-list";
|
|
||||||
|
|
||||||
export default function PeopleListSection() {
|
|
||||||
return (
|
|
||||||
<Section>
|
|
||||||
<Stack className="w-full items-stretch">
|
|
||||||
<PeopleList />
|
|
||||||
</Stack>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import AdminHero from "@/src/components/general/admin-hero";
|
|
||||||
import Tag from "@moduletrace/twui/elements/Tag";
|
|
||||||
import Button from "@moduletrace/twui/layout/Button";
|
|
||||||
import Row from "@moduletrace/twui/layout/Row";
|
|
||||||
import Span from "@moduletrace/twui/layout/Span";
|
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
|
||||||
import { AppContext } from "@/src/pages/__root";
|
|
||||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function Hero() {
|
|
||||||
const { pageProps } = useContext(AppContext);
|
|
||||||
|
|
||||||
const person = pageProps?.person;
|
|
||||||
|
|
||||||
if (!person) return null;
|
|
||||||
|
|
||||||
const is_read_only = isReadOnlyUser({
|
|
||||||
user_types: pageProps?.user_types,
|
|
||||||
});
|
|
||||||
|
|
||||||
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminHero
|
|
||||||
title={person_name}
|
|
||||||
buttons={
|
|
||||||
<>
|
|
||||||
{is_read_only ? null : (
|
|
||||||
<Button
|
|
||||||
title={`Edit ${person_name} profile`}
|
|
||||||
href={`/admin/people/${person.id}/edit`}
|
|
||||||
>
|
|
||||||
Edit Person
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Stack className="gap-4">
|
|
||||||
{person.user_types?.[0] ? (
|
|
||||||
<Row className="flex-nowrap">
|
|
||||||
{person.user_types?.split(",").map((ut, i) => (
|
|
||||||
<Tag key={i} color="gray" variant="outlined">
|
|
||||||
{ut}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
) : null}
|
|
||||||
{person.bio ? (
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{ __html: person.bio }}
|
|
||||||
className="bio-subtitle"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Paper from "@moduletrace/twui/elements/Paper";
|
|
||||||
import H2 from "@moduletrace/twui/layout/H2";
|
|
||||||
import Row from "@moduletrace/twui/layout/Row";
|
|
||||||
import Section from "@moduletrace/twui/layout/Section";
|
|
||||||
import Span from "@moduletrace/twui/layout/Span";
|
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
|
||||||
import { AppContext } from "@/src/pages/__root";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function PersonInfo() {
|
|
||||||
const { pageProps } = useContext(AppContext);
|
|
||||||
|
|
||||||
const person = pageProps?.person;
|
|
||||||
|
|
||||||
if (!person) return null;
|
|
||||||
|
|
||||||
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Section>
|
|
||||||
<Row className="w-full grid xl:grid-cols-2">
|
|
||||||
<Paper>
|
|
||||||
<Stack>
|
|
||||||
<H2>Biography</H2>
|
|
||||||
{person.bio ? (
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{ __html: person.bio }}
|
|
||||||
className="bio-content"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Span>No Bio</Span>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Row>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import Section from "@moduletrace/twui/layout/Section";
|
|
||||||
import { AppContext } from "@/src/pages/__root";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import UserForm from "../../../create/(partials)/user-form";
|
|
||||||
|
|
||||||
export default function EditPersonFormSection() {
|
|
||||||
const { pageProps } = useContext(AppContext);
|
|
||||||
|
|
||||||
const person = pageProps?.person;
|
|
||||||
|
|
||||||
if (!person) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Section>
|
|
||||||
<UserForm person={person} />
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import AdminHero from "@/src/components/general/admin-hero";
|
|
||||||
import { AppContext } from "@/src/pages/__root";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function Hero() {
|
|
||||||
const { pageProps } = useContext(AppContext);
|
|
||||||
|
|
||||||
const person = pageProps?.person;
|
|
||||||
|
|
||||||
if (!person) return null;
|
|
||||||
|
|
||||||
const person_name = `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`;
|
|
||||||
|
|
||||||
return <AdminHero title={`Edit ${person_name} Profile`} />;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { server as person_server } from "../index.server";
|
|
||||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
|
||||||
import type { PagePropsType } from "@/src/types";
|
|
||||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
|
||||||
|
|
||||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
|
||||||
const user_types = params.props?.user_types;
|
|
||||||
|
|
||||||
if (isReadOnlyUser({ user_types })) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/admin/people",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return await person_server(params);
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { PagePropsType } from "@/src/types";
|
|
||||||
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
|
|
||||||
import Hero from "./(sections)/hero";
|
|
||||||
import EditPersonFormSection from "./(sections)/edit-person-form-section";
|
|
||||||
import { SiteData } from "@/src/data/site-data";
|
|
||||||
|
|
||||||
export default function EditPersonPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Hero />
|
|
||||||
<EditPersonFormSection />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const meta: BunextPageModuleMetaFn = async (params) => {
|
|
||||||
const person = (params.serverRes?.props as PagePropsType | undefined)
|
|
||||||
?.person;
|
|
||||||
|
|
||||||
const person_name = `${person?.first_name + (person?.last_name ? ` ${person.last_name}` : "")}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `Edit ${person_name} profile | ${SiteData["SiteName"]}`,
|
|
||||||
description: person?.bio || `${SiteData["SiteName"]} Personnel`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
|
||||||
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
|
|
||||||
import type { PagePropsType, PageQueryObject } from "@/src/types";
|
|
||||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
|
||||||
|
|
||||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
|
||||||
const query: PageQueryObject = params.query;
|
|
||||||
|
|
||||||
if (!query.user_id) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/admin",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const user_res = await grabUsers({ query: { user_id: query.user_id } });
|
|
||||||
|
|
||||||
if (!user_res.singleRes) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/admin",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
person: sanitizeUserBio({ user: user_res.singleRes }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { PagePropsType } from "@/src/types";
|
|
||||||
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
|
|
||||||
import Hero from "./(sections)/hero";
|
|
||||||
import PersonInfo from "./(sections)/person-info";
|
|
||||||
import { SiteData } from "@/src/data/site-data";
|
|
||||||
|
|
||||||
export default function PersonPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Hero />
|
|
||||||
<PersonInfo />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const meta: BunextPageModuleMetaFn = async (params) => {
|
|
||||||
const person = (params.serverRes?.props as PagePropsType | undefined)
|
|
||||||
?.person;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `${person?.first_name + (person?.last_name ? ` ${person.last_name}` : "")} | ${SiteData["SiteName"]}`,
|
|
||||||
description: person?.bio || `${SiteData["SiteName"]} Personnel`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import AdminHero from "@/src/components/general/admin-hero";
|
|
||||||
import Button from "@moduletrace/twui/layout/Button";
|
|
||||||
|
|
||||||
export default function Hero() {
|
|
||||||
return <AdminHero title="Create New Person" />;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import Section from "@moduletrace/twui/layout/Section";
|
|
||||||
import UserForm from "../(partials)/user-form";
|
|
||||||
|
|
||||||
export default function UserFormSection() {
|
|
||||||
return (
|
|
||||||
<Section>
|
|
||||||
<UserForm />
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { PagePropsType } from "@/src/types";
|
|
||||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
|
||||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
|
||||||
|
|
||||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
|
||||||
const user_types = params.props?.user_types;
|
|
||||||
|
|
||||||
if (isReadOnlyUser({ user_types })) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/admin/people",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
|
||||||
import Hero from "./(sections)/hero";
|
|
||||||
import UserFormSection from "./(sections)/user-form-section";
|
|
||||||
import { SiteData } from "@/src/data/site-data";
|
|
||||||
|
|
||||||
export default function AdminUsersCreatePage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Hero />
|
|
||||||
<UserFormSection />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const meta: BunextPageModuleMeta = {
|
|
||||||
title: `Create Admin User | ${SiteData["SiteName"]}`,
|
|
||||||
description: "Create a new admin user in the dashboard.",
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
|
||||||
import Hero from "./(sections)/hero";
|
|
||||||
import PeopleListSection from "./(sections)/people-list-section";
|
|
||||||
import { SiteData } from "@/src/data/site-data";
|
|
||||||
|
|
||||||
export default function AdminUsersPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Hero />
|
|
||||||
<PeopleListSection />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const meta: BunextPageModuleMeta = {
|
|
||||||
title: `Admin People | ${SiteData["SiteName"]}`,
|
|
||||||
description: "Manage admin users in the dashboard.",
|
|
||||||
};
|
|
||||||
+1
-1
@@ -59,6 +59,6 @@ export default async function submitUserForm({
|
|||||||
} else if (existing_full?.id) {
|
} else if (existing_full?.id) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
window.location.pathname = `/admin/people/${res.singleRes.id}`;
|
window.location.pathname = `/admin/settings/users/${res.singleRes.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
||||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
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 = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import { UneditableUserTypes } from "@/src/dict/user-types-dict";
|
|||||||
import type { UserFormObject } from "@/src/types";
|
import type { UserFormObject } from "@/src/types";
|
||||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||||
import submitUserForm from "../../(functions)/submit-user-form";
|
import submitUserForm from "@/src/pages/admin/settings/users/(functions)/submit-user-form";
|
||||||
import UserFormName from "./user-form-name";
|
import UserFormName from "./user-form-name";
|
||||||
import UserFormAction from "./user-form-action";
|
import UserFormAction from "./user-form-action";
|
||||||
import UserFormEmail from "./user-form-email";
|
import UserFormEmail from "./user-form-email";
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
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 SettingsGroup from "../../../(partials)/settings-group";
|
||||||
import SettingsStatus from "../../../(partials)/settings-status";
|
import SettingsStatus from "../../../(partials)/settings-status";
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ describe("isAdminSectionPath", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rejects the admin root, closed sections and lookalikes", () => {
|
test("rejects the admin root, unknown sections and lookalikes", () => {
|
||||||
expect(isAdminSectionPath({ pathname: "/admin" })).toBe(false);
|
expect(isAdminSectionPath({ pathname: "/admin" })).toBe(false);
|
||||||
expect(isAdminSectionPath({ pathname: "/admin/people" })).toBe(false);
|
|
||||||
expect(isAdminSectionPath({ pathname: "/admin/backups" })).toBe(false);
|
expect(isAdminSectionPath({ pathname: "/admin/backups" })).toBe(false);
|
||||||
expect(isAdminSectionPath({ pathname: "/admin/botsx" })).toBe(false);
|
expect(isAdminSectionPath({ pathname: "/admin/botsx" })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user