feat: added optin to add delegant for executor and change delegant
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { APPROVAL_API_CLIENT_ID, DICTIONARY_API_BASE_URL } from '../config';
|
||||
import { IDepartment } from './models/IDepartment';
|
||||
|
||||
// GET /Dictionary/Departments — справочник отделов по всем компаниям, ответ —
|
||||
// плоский массив (без обёртки {data: ...}, в отличие от approval-route API).
|
||||
async function fetchDepartments(context: WebPartContext): Promise<IDepartment[]> {
|
||||
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||
const response: HttpClientResponse = await client.get(
|
||||
`${DICTIONARY_API_BASE_URL}/Dictionary/Departments`,
|
||||
AadHttpClient.configurations.v1
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Dictionary/Departments: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Справочник большой (сотни записей) и в рамках сессии не меняется — грузим
|
||||
// один раз и отдаём тот же промис повторным вызовам (иначе каждое открытие
|
||||
// формы/пикера заново тянуло бы весь список).
|
||||
let cache: Promise<IDepartment[]> | undefined;
|
||||
export function getDepartments(context: WebPartContext): Promise<IDepartment[]> {
|
||||
if (!cache) {
|
||||
cache = fetchDepartments(context).catch((e) => {
|
||||
cache = undefined;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { getDepartments } from './departments.service';
|
||||
export type { IDepartment } from './models/IDepartment';
|
||||
export { DepartmentPicker } from './ui/DepartmentPicker';
|
||||
export type { IDepartmentPickerProps } from './ui/DepartmentPicker';
|
||||
@@ -0,0 +1,7 @@
|
||||
// GET /Dictionary/Departments (api.tnsu.kz) → IDepartment[]
|
||||
export interface IDepartment {
|
||||
Id: string;
|
||||
Name: string;
|
||||
CompanyId: string;
|
||||
Company: string;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@import '../../../../../styles/design-tokens';
|
||||
|
||||
.wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 11px 13px;
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: $r-sm;
|
||||
background: $white;
|
||||
font-family: $font-sans;
|
||||
font-size: 13.5px;
|
||||
color: $gray-900;
|
||||
transition: .18s;
|
||||
|
||||
@include input-focus-gold;
|
||||
@include readonly-field(':read-only');
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: $sp-1;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-sm;
|
||||
box-shadow: $sh-2;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
font-family: $font-sans;
|
||||
font-size: $fs-sm;
|
||||
color: $gray-900;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $navy-50;
|
||||
}
|
||||
}
|
||||
|
||||
.company {
|
||||
font-size: $fs-caption;
|
||||
color: $gray-500;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as React from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './DepartmentPicker.module.scss';
|
||||
import { useFormReadOnly } from '../../ui/FormMode';
|
||||
import { getDepartments } from '../departments.service';
|
||||
import { IDepartment } from '../models/IDepartment';
|
||||
|
||||
export interface IDepartmentPickerProps {
|
||||
context: WebPartContext;
|
||||
id?: string;
|
||||
/** Текущее название отдела (то, что видно в поле). */
|
||||
value: string;
|
||||
/** name — введённый/выбранный текст; id — Id из справочника ('' если текст введён вручную и не выбран из списка). */
|
||||
onChange: (name: string, id: string) => void;
|
||||
placeholder?: string;
|
||||
/** Только чтение (режим согласующего). */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const MAX_SUGGESTIONS = 30;
|
||||
|
||||
/** Поле «Отдел / участок» со справочником (Dictionary/Departments): ввод текста фильтрует список, выбор проставляет Id. */
|
||||
export const DepartmentPicker: React.FC<IDepartmentPickerProps> = ({
|
||||
context,
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
readOnly
|
||||
}) => {
|
||||
const contextReadOnly = useFormReadOnly();
|
||||
const effectiveReadOnly = readOnly ?? contextReadOnly;
|
||||
|
||||
const [departments, setDepartments] = React.useState<IDepartment[]>([]);
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
let alive = true;
|
||||
getDepartments(context)
|
||||
.then((list) => {
|
||||
if (alive) setDepartments(list);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [context]);
|
||||
|
||||
const suggestions = React.useMemo(() => {
|
||||
const query = value.trim().toLowerCase();
|
||||
if (!query) return [];
|
||||
return departments.filter((d) => d.Name.toLowerCase().indexOf(query) !== -1).slice(0, MAX_SUGGESTIONS);
|
||||
}, [departments, value]);
|
||||
|
||||
const handlePick = (dept: IDepartment): void => {
|
||||
onChange(dept.Name, dept.Id);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<input
|
||||
id={id}
|
||||
className={styles.input}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
readOnly={effectiveReadOnly}
|
||||
onChange={(e) => onChange(e.target.value, '')}
|
||||
onFocus={() => setOpen(true)}
|
||||
onBlur={() => setOpen(false)}
|
||||
/>
|
||||
{!effectiveReadOnly && open && suggestions.length > 0 && (
|
||||
<div className={styles.list}>
|
||||
{suggestions.map((d) => (
|
||||
<button
|
||||
key={d.Id}
|
||||
type="button"
|
||||
className={styles.item}
|
||||
// preventDefault на mousedown — иначе input теряет фокус (onBlur) раньше onClick, и список закрывается до выбора.
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => handlePick(d)}
|
||||
>
|
||||
{d.Name}
|
||||
<span className={styles.company}>{d.Company}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user