fix(frontend): stabilize department add-member modal
This commit is contained in:
parent
c1840fe1d1
commit
852ac367c0
@ -8,7 +8,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"test": "node --experimental-strip-types --test src/**/*.test.ts",
|
||||
"test": "node --experimental-strip-types --test src/**/*.test.js",
|
||||
"postinstall": "cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs || true"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -18,6 +18,11 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import {
|
||||
getAddMemberHelperText,
|
||||
getAddMemberPlaceholder,
|
||||
isAddMemberSelectDisabled,
|
||||
} from "@/components/departments/add-member-modal-state";
|
||||
import {
|
||||
DEPARTMENT_MEMBERS_EMPTY_STATE_CLASSNAME,
|
||||
getDepartmentMembersListClassName,
|
||||
@ -389,6 +394,7 @@ function AddMemberModal({
|
||||
isOpen,
|
||||
items,
|
||||
loading,
|
||||
optionsLoading,
|
||||
onClose,
|
||||
onSave,
|
||||
onSelect,
|
||||
@ -397,6 +403,7 @@ function AddMemberModal({
|
||||
isOpen: boolean;
|
||||
items: Array<{ id: number; name: string }>;
|
||||
loading: boolean;
|
||||
optionsLoading: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
onSelect: (id: number | null) => void;
|
||||
@ -408,6 +415,7 @@ function AddMemberModal({
|
||||
onClose={onClose}
|
||||
title="Добавить участника"
|
||||
size="md"
|
||||
className="overflow-visible"
|
||||
footer={
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
@ -434,12 +442,12 @@ function AddMemberModal({
|
||||
items={items}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
placeholder="Выберите сотрудника"
|
||||
disabled={!items.length}
|
||||
placeholder={getAddMemberPlaceholder(optionsLoading)}
|
||||
disabled={isAddMemberSelectDisabled(optionsLoading, items.length)}
|
||||
/>
|
||||
{!items.length ? (
|
||||
{getAddMemberHelperText(optionsLoading, items.length) ? (
|
||||
<p className="app-text-muted mt-3 text-sm">
|
||||
В директории нет доступных сотрудников для добавления.
|
||||
{getAddMemberHelperText(optionsLoading, items.length)}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
@ -770,6 +778,7 @@ export default function DepartmentDetailPage() {
|
||||
isOpen={h.addMemberOpen}
|
||||
items={h.selectableEmployees}
|
||||
loading={h.pendingKey === "member"}
|
||||
optionsLoading={h.employeesDirectoryLoading}
|
||||
onClose={h.closeAddMember}
|
||||
onSave={h.submitAddMember}
|
||||
onSelect={h.setSelectedMemberId}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
getAddMemberHelperText,
|
||||
getAddMemberPlaceholder,
|
||||
isAddMemberSelectDisabled,
|
||||
} from "./add-member-modal-state.ts";
|
||||
|
||||
test("add member modal shows loading placeholder while directory loads", () => {
|
||||
assert.equal(getAddMemberPlaceholder(true), "Загружаем сотрудников...");
|
||||
assert.equal(isAddMemberSelectDisabled(true, 3), true);
|
||||
assert.equal(
|
||||
getAddMemberHelperText(true, 3),
|
||||
"Загружаем доступных сотрудников...",
|
||||
);
|
||||
});
|
||||
|
||||
test("add member modal shows empty state when there are no selectable employees", () => {
|
||||
assert.equal(getAddMemberPlaceholder(false), "Выберите сотрудника");
|
||||
assert.equal(isAddMemberSelectDisabled(false, 0), true);
|
||||
assert.equal(
|
||||
getAddMemberHelperText(false, 0),
|
||||
"В директории нет доступных сотрудников для добавления.",
|
||||
);
|
||||
});
|
||||
|
||||
test("add member modal stays enabled when options are ready", () => {
|
||||
assert.equal(isAddMemberSelectDisabled(false, 2), false);
|
||||
assert.equal(getAddMemberHelperText(false, 2), null);
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
export function getAddMemberPlaceholder(optionsLoading: boolean) {
|
||||
return optionsLoading ? "Загружаем сотрудников..." : "Выберите сотрудника";
|
||||
}
|
||||
|
||||
export function isAddMemberSelectDisabled(
|
||||
optionsLoading: boolean,
|
||||
itemsCount: number,
|
||||
) {
|
||||
return optionsLoading || itemsCount === 0;
|
||||
}
|
||||
|
||||
export function getAddMemberHelperText(
|
||||
optionsLoading: boolean,
|
||||
itemsCount: number,
|
||||
) {
|
||||
if (optionsLoading) {
|
||||
return "Загружаем доступных сотрудников...";
|
||||
}
|
||||
if (itemsCount === 0) {
|
||||
return "В директории нет доступных сотрудников для добавления.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
22
frontend/src/components/departments/layout.test.js
Normal file
22
frontend/src/components/departments/layout.test.js
Normal file
@ -0,0 +1,22 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
DEPARTMENT_MEMBERS_EMPTY_STATE_CLASSNAME,
|
||||
getDepartmentMembersListClassName,
|
||||
} from "./layout.ts";
|
||||
|
||||
test("showcase members layout keeps bubble flow", () => {
|
||||
assert.equal(
|
||||
getDepartmentMembersListClassName(false),
|
||||
"flex flex-wrap items-center gap-2",
|
||||
);
|
||||
});
|
||||
|
||||
test("management members layout keeps stacked editing rows", () => {
|
||||
assert.equal(getDepartmentMembersListClassName(true), "space-y-3");
|
||||
});
|
||||
|
||||
test("department empty state spans full section width", () => {
|
||||
assert.match(DEPARTMENT_MEMBERS_EMPTY_STATE_CLASSNAME, /\bw-full\b/);
|
||||
});
|
||||
@ -41,6 +41,7 @@ export type DepartmentPageController = {
|
||||
error: string | null;
|
||||
filteredMembers: DepartmentMemberLink[];
|
||||
headCandidates: Array<{ id: number; name: string }>;
|
||||
employeesDirectoryLoading: boolean;
|
||||
loading: boolean;
|
||||
members: DepartmentMemberLink[];
|
||||
pendingKey: SavingKey | null;
|
||||
@ -126,6 +127,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
const [roles, setRoles] = useState<DepartmentRole[]>([]);
|
||||
const [userPerms, setUserPerms] = useState<DepartmentUserPermissions | null>(null);
|
||||
const [allEmployees, setAllEmployees] = useState<User[]>([]);
|
||||
const [employeesDirectoryLoading, setEmployeesDirectoryLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pendingKey, setPendingKey] = useState<SavingKey | null>(null);
|
||||
@ -153,8 +155,13 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
}, []);
|
||||
|
||||
const loadEmployeesDirectory = useCallback(async () => {
|
||||
const employees = await loadAllPages<User>((params) => apiClient.getEmployees(params));
|
||||
setAllEmployees(sortEmployeesByName(employees.filter((employee) => employee.is_active)));
|
||||
setEmployeesDirectoryLoading(true);
|
||||
try {
|
||||
const employees = await loadAllPages<User>((params) => apiClient.getEmployees(params));
|
||||
setAllEmployees(sortEmployeesByName(employees.filter((employee) => employee.is_active)));
|
||||
} finally {
|
||||
setEmployeesDirectoryLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadCore = useCallback(async () => {
|
||||
@ -311,7 +318,12 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
const openAddMember = useCallback(() => {
|
||||
setSelectedMemberId(null);
|
||||
setAddMemberOpen(true);
|
||||
}, []);
|
||||
if (allEmployees.length === 0) {
|
||||
void loadEmployeesDirectory().catch((loadError) => {
|
||||
toast.error(getErrorMessage(loadError, "Не удалось загрузить сотрудников"));
|
||||
});
|
||||
}
|
||||
}, [allEmployees.length, loadEmployeesDirectory]);
|
||||
|
||||
const closeAddMember = useCallback(() => {
|
||||
setSelectedMemberId(null);
|
||||
@ -522,6 +534,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
departmentDraft,
|
||||
departmentId,
|
||||
editDepartmentOpen,
|
||||
employeesDirectoryLoading,
|
||||
error,
|
||||
filteredMembers,
|
||||
headCandidates,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user