refactor: подписанные вложения — общий модуль shared/attachments
Подпись вложения префиксом в имени файла и поле одного слота жили внутри «Заявки на приём», а теперь нужны и другим формам. Перенесены в shared/attachments без изменения поведения: приём реэкспортирует общее, его форма и просмотр не тронуты. useAttachmentEditor принимает новую, ещё не сохранённую заявку (itemId пустой) и явное «куда грузить» — id появляется только после первого сохранения. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { AttachmentList, FileDropzone, FormField } from '../../shared/ui';
|
|
||||||
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
|
||||||
import { attachmentsOfCategory, categoryPrefix, uncategorizedAttachments } from './attachmentCategories';
|
|
||||||
import styles from './HireAttachmentField.module.scss';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
label: string;
|
|
||||||
editor: IAttachmentEditor;
|
|
||||||
editable: boolean;
|
|
||||||
/** Группа новых файлов в editor.pending. */
|
|
||||||
groupKey: string;
|
|
||||||
/** Категория в имени файла; не задана — «прочие» вложения без префикса. */
|
|
||||||
category?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Поле одной категории вложений: прикреплённые файлы + (в правке) выбор новых. */
|
|
||||||
export const HireAttachmentField: React.FC<Props> = ({ label, editor, editable, groupKey, category }) => {
|
|
||||||
const items = category ? attachmentsOfCategory(editor.items, category) : uncategorizedAttachments(editor.items);
|
|
||||||
// В списке имена показаны без префикса категории — для удаления возвращаем его.
|
|
||||||
const removeWithPrefix = (name: string): void => editor.remove(category ? categoryPrefix(category) + name : name);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormField label={label} full>
|
|
||||||
{items.length > 0 && <AttachmentList items={items} onRemove={editable ? removeWithPrefix : undefined} />}
|
|
||||||
{items.length === 0 && !editable && <span className={styles.empty}>—</span>}
|
|
||||||
{editable && (
|
|
||||||
<FileDropzone files={editor.pending[groupKey] || []} onChange={(files) => editor.setPending(groupKey, files)} />
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Alert, CollapseSection } from '../../shared/ui';
|
import { Alert, CollapseSection } from '../../shared/ui';
|
||||||
import { IAttachmentEditor, PrepareFiles } from '../../shared/attachments/useAttachmentEditor';
|
import { IAttachmentEditor, PrepareFiles } from '../../shared/attachments/useAttachmentEditor';
|
||||||
|
import { AttachmentSlotField } from '../../shared/attachments/AttachmentSlotField';
|
||||||
import { ATT_CATEGORY_ID, ATT_CATEGORY_RESUME, prefixFiles, uncategorizedAttachments } from './attachmentCategories';
|
import { ATT_CATEGORY_ID, ATT_CATEGORY_RESUME, prefixFiles, uncategorizedAttachments } from './attachmentCategories';
|
||||||
import { HireAttachmentField } from './HireAttachmentField';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Группы новых файлов → категория в имени файла (см. attachmentCategories).
|
* Группы новых файлов → категория в имени файла (см. attachmentCategories).
|
||||||
@@ -37,9 +37,16 @@ export const HireAttachments: React.FC<Props> = ({ editor, editable }) => {
|
|||||||
return (
|
return (
|
||||||
<CollapseSection title="Вложения" icon="upload">
|
<CollapseSection title="Вложения" icon="upload">
|
||||||
{editor.error && <Alert variant="err">{editor.error}</Alert>}
|
{editor.error && <Alert variant="err">{editor.error}</Alert>}
|
||||||
<HireAttachmentField label="Удостоверение" groupKey={GROUP_ID} category={ATT_CATEGORY_ID} {...fieldProps} />
|
<AttachmentSlotField label="Удостоверение" groupKey={GROUP_ID} category={ATT_CATEGORY_ID} {...fieldProps} />
|
||||||
<HireAttachmentField label="Резюме" groupKey={GROUP_RESUME} category={ATT_CATEGORY_RESUME} {...fieldProps} />
|
<AttachmentSlotField label="Резюме" groupKey={GROUP_RESUME} category={ATT_CATEGORY_RESUME} {...fieldProps} />
|
||||||
{showOther && <HireAttachmentField label="Прочие вложения" groupKey={GROUP_OTHER} {...fieldProps} />}
|
{showOther && (
|
||||||
|
<AttachmentSlotField
|
||||||
|
label="Прочие вложения"
|
||||||
|
groupKey={GROUP_OTHER}
|
||||||
|
knownCategories={[ATT_CATEGORY_ID, ATT_CATEGORY_RESUME]}
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CollapseSection>
|
</CollapseSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,50 +1,24 @@
|
|||||||
import { IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
|
import { attachmentsWithoutCategories } from '../../shared/attachments/attachmentCategories';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Категории вложений заявки на прием. Вложения SharePoint — плоская коллекция
|
* Категории вложений заявки на прием. Механизм подписи префиксом в имени файла —
|
||||||
* файлов на элементе списка, «категории» у них нет. Поэтому категорию кодируем
|
* общий (shared/attachments/attachmentCategories); здесь только категории этой
|
||||||
* ПРЕФИКСОМ в имени файла: «<Категория> — <исходное имя>». При загрузке имя
|
* формы. Общие функции реэкспортируются, чтобы импорты формы не менялись.
|
||||||
* файла переписываем, при показе — группируем по префиксу и префикс срезаем.
|
|
||||||
*/
|
*/
|
||||||
export const ATT_CATEGORY_ID = 'Удостоверение';
|
export const ATT_CATEGORY_ID = 'Удостоверение';
|
||||||
export const ATT_CATEGORY_RESUME = 'Резюме';
|
export const ATT_CATEGORY_RESUME = 'Резюме';
|
||||||
|
|
||||||
const SEP = ' — ';
|
export {
|
||||||
|
categoryPrefix,
|
||||||
/** Префикс имени файла для категории: «Удостоверение — ». */
|
prefixFiles,
|
||||||
export function categoryPrefix(category: string): string {
|
attachmentsOfCategory
|
||||||
return `${category}${SEP}`;
|
} from '../../shared/attachments/attachmentCategories';
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Копии File с префиксом категории в имени — для загрузки. File.name неизменяемо,
|
|
||||||
* поэтому создаём новый File с тем же содержимым и типом, но другим именем.
|
|
||||||
*/
|
|
||||||
export function prefixFiles(files: File[], category: string): File[] {
|
|
||||||
const prefix = categoryPrefix(category);
|
|
||||||
return files.map((f) => new File([f], prefix + f.name, { type: f.type }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Вложения указанной категории, с ОБРЕЗАННЫМ префиксом в имени (для показа).
|
|
||||||
* url сохраняется как есть; чтобы удалить такой файл, префикс надо вернуть
|
|
||||||
* обратно (см. categoryPrefix в обработчике удаления формы).
|
|
||||||
*/
|
|
||||||
export function attachmentsOfCategory(items: IAttachmentInfo[], category: string): IAttachmentInfo[] {
|
|
||||||
const prefix = categoryPrefix(category);
|
|
||||||
return items
|
|
||||||
.filter((a) => a.fileName.indexOf(prefix) === 0)
|
|
||||||
.map((a) => ({ url: a.url, fileName: a.fileName.slice(prefix.length) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вложения без известной категории (старые заявки, загруженные до разделения).
|
* Вложения без известной категории (старые заявки, загруженные до разделения).
|
||||||
* Показываем как есть, чтобы файлы не «исчезали» с экрана.
|
* Показываем как есть, чтобы файлы не «исчезали» с экрана.
|
||||||
*/
|
*/
|
||||||
export function uncategorizedAttachments(items: IAttachmentInfo[]): IAttachmentInfo[] {
|
export function uncategorizedAttachments(items: IAttachmentInfo[]): IAttachmentInfo[] {
|
||||||
const idPrefix = categoryPrefix(ATT_CATEGORY_ID);
|
return attachmentsWithoutCategories(items, [ATT_CATEGORY_ID, ATT_CATEGORY_RESUME]);
|
||||||
const resumePrefix = categoryPrefix(ATT_CATEGORY_RESUME);
|
|
||||||
return items.filter(
|
|
||||||
(a) => a.fileName.indexOf(idPrefix) !== 0 && a.fileName.indexOf(resumePrefix) !== 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -1,5 +1,6 @@
|
|||||||
@import '../../../../styles/design-tokens';
|
@import '../../../../styles/design-tokens';
|
||||||
|
|
||||||
|
// Прочерк вместо пустого слота в режиме чтения.
|
||||||
.empty {
|
.empty {
|
||||||
font-family: $font-sans;
|
font-family: $font-sans;
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { AttachmentList, FileDropzone, FormField } from '../ui';
|
||||||
|
import { IAttachmentEditor } from './useAttachmentEditor';
|
||||||
|
import { attachmentsOfCategory, attachmentsWithoutCategories, categoryPrefix } from './attachmentCategories';
|
||||||
|
import styles from './AttachmentSlotField.module.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Подпись поля — что за документ. */
|
||||||
|
label: string;
|
||||||
|
editor: IAttachmentEditor;
|
||||||
|
/** Можно добавлять и удалять файлы (форма подачи / согласующий с полным правом). */
|
||||||
|
editable: boolean;
|
||||||
|
/** Группа новых файлов в editor.pending. */
|
||||||
|
groupKey: string;
|
||||||
|
/** Категория в имени файла (см. attachmentCategories); не задана — «прочие» вложения. */
|
||||||
|
category?: string;
|
||||||
|
/** Для «прочих» (category не задана): категории формы, файлы которых сюда НЕ попадают. */
|
||||||
|
knownCategories?: string[];
|
||||||
|
/** Звёздочка «обязательно» у подписи; саму проверку делает форма. */
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поле одного подписанного слота вложений: уже прикреплённые файлы + (в правке)
|
||||||
|
* выбор новых. Новые файлы копятся в editor.pending[groupKey] и загружаются
|
||||||
|
* формой при сохранении (editor.uploadPending), с префиксом категории в имени.
|
||||||
|
*/
|
||||||
|
export const AttachmentSlotField: React.FC<Props> = ({
|
||||||
|
label,
|
||||||
|
editor,
|
||||||
|
editable,
|
||||||
|
groupKey,
|
||||||
|
category,
|
||||||
|
knownCategories = [],
|
||||||
|
required
|
||||||
|
}) => {
|
||||||
|
const items = category
|
||||||
|
? attachmentsOfCategory(editor.items, category)
|
||||||
|
: attachmentsWithoutCategories(editor.items, knownCategories);
|
||||||
|
// В списке имена показаны без префикса категории — для удаления возвращаем его.
|
||||||
|
const removeWithPrefix = (name: string): void => editor.remove(category ? categoryPrefix(category) + name : name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField label={label} required={required} full>
|
||||||
|
{items.length > 0 && <AttachmentList items={items} onRemove={editable ? removeWithPrefix : undefined} />}
|
||||||
|
{items.length === 0 && !editable && <span className={styles.empty}>—</span>}
|
||||||
|
{editable && (
|
||||||
|
<FileDropzone files={editor.pending[groupKey] || []} onChange={(files) => editor.setPending(groupKey, files)} />
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
categoryPrefix,
|
||||||
|
attachmentsOfCategory,
|
||||||
|
attachmentsWithoutCategories
|
||||||
|
} from './attachmentCategories';
|
||||||
|
|
||||||
|
const file = (fileName: string): { fileName: string; url: string } => ({ fileName, url: `/att/${fileName}` });
|
||||||
|
|
||||||
|
describe('attachmentCategories', () => {
|
||||||
|
const items = [file('Резюме — cv.pdf'), file('Удостоверение — id.jpg'), file('старый.docx')];
|
||||||
|
|
||||||
|
it('префикс — подпись и длинное тире с пробелами', () => {
|
||||||
|
expect(categoryPrefix('Резюме')).toBe('Резюме — ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('файлы категории — с обрезанным префиксом и прежней ссылкой', () => {
|
||||||
|
expect(attachmentsOfCategory(items, 'Резюме')).toEqual([{ fileName: 'cv.pdf', url: '/att/Резюме — cv.pdf' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('без категорий — только файлы, не попавшие ни в одну из известных', () => {
|
||||||
|
expect(attachmentsWithoutCategories(items, ['Резюме', 'Удостоверение'])).toEqual([file('старый.docx')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('без известных категорий «прочие» — это все файлы', () => {
|
||||||
|
expect(attachmentsWithoutCategories(items, [])).toEqual(items);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { IAttachmentInfo } from './attachments.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подписанные вложения («категории»). Вложения SharePoint — плоская коллекция
|
||||||
|
* файлов на элементе списка, подписи у них нет. Поэтому подпись (что за
|
||||||
|
* документ) кодируем ПРЕФИКСОМ в имени файла: «<Подпись> — <исходное имя>».
|
||||||
|
* При загрузке имя файла переписываем, при показе — группируем по префиксу и
|
||||||
|
* префикс срезаем. Вынесено из «Заявки на приём», где появилось первым.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SEP = ' — ';
|
||||||
|
|
||||||
|
/** Префикс имени файла для категории: «Удостоверение — ». */
|
||||||
|
export function categoryPrefix(category: string): string {
|
||||||
|
return `${category}${SEP}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Копии File с префиксом категории в имени — для загрузки. File.name неизменяемо,
|
||||||
|
* поэтому создаём новый File с тем же содержимым и типом, но другим именем.
|
||||||
|
*/
|
||||||
|
export function prefixFiles(files: File[], category: string): File[] {
|
||||||
|
const prefix = categoryPrefix(category);
|
||||||
|
return files.map((f) => new File([f], prefix + f.name, { type: f.type }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вложения указанной категории, с ОБРЕЗАННЫМ префиксом в имени (для показа).
|
||||||
|
* url сохраняется как есть; чтобы удалить такой файл, префикс надо вернуть
|
||||||
|
* обратно (categoryPrefix + показанное имя).
|
||||||
|
*/
|
||||||
|
export function attachmentsOfCategory(items: IAttachmentInfo[], category: string): IAttachmentInfo[] {
|
||||||
|
const prefix = categoryPrefix(category);
|
||||||
|
return items
|
||||||
|
.filter((a) => a.fileName.indexOf(prefix) === 0)
|
||||||
|
.map((a) => ({ url: a.url, fileName: a.fileName.slice(prefix.length) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вложения, не относящиеся ни к одной из известных категорий формы (старые
|
||||||
|
* заявки, «прочие»). Показываем как есть, чтобы файлы не «исчезали» с экрана.
|
||||||
|
*/
|
||||||
|
export function attachmentsWithoutCategories(items: IAttachmentInfo[], categories: string[]): IAttachmentInfo[] {
|
||||||
|
const prefixes = categories.map(categoryPrefix);
|
||||||
|
return items.filter((a) => !prefixes.some((prefix) => a.fileName.indexOf(prefix) === 0));
|
||||||
|
}
|
||||||
@@ -4,10 +4,16 @@ import { friendlyErrorMessage } from '../diagnostics';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Подготовка новых файлов группы к загрузке — напр. префикс категории в имени
|
* Подготовка новых файлов группы к загрузке — напр. префикс категории в имени
|
||||||
* (см. forms/RecruitingHire/attachmentCategories). По умолчанию — как есть.
|
* (см. attachmentCategories). По умолчанию — как есть.
|
||||||
*/
|
*/
|
||||||
export type PrepareFiles = (groupKey: string, files: File[]) => File[];
|
export type PrepareFiles = (groupKey: string, files: File[]) => File[];
|
||||||
|
|
||||||
|
/** Куда грузить: список (Title текущей партиции или url архивной) + id элемента. */
|
||||||
|
export interface IAttachmentTarget {
|
||||||
|
listRef: string;
|
||||||
|
itemId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAttachmentEditor {
|
export interface IAttachmentEditor {
|
||||||
/** Вложения, уже прикреплённые к заявке. */
|
/** Вложения, уже прикреплённые к заявке. */
|
||||||
items: IAttachmentInfo[];
|
items: IAttachmentInfo[];
|
||||||
@@ -17,8 +23,12 @@ export interface IAttachmentEditor {
|
|||||||
setPending: (groupKey: string, files: File[]) => void;
|
setPending: (groupKey: string, files: File[]) => void;
|
||||||
/** Удалить прикреплённый файл сразу из заявки (по полному имени файла в SP). */
|
/** Удалить прикреплённый файл сразу из заявки (по полному имени файла в SP). */
|
||||||
remove: (fileName: string) => void;
|
remove: (fileName: string) => void;
|
||||||
/** Загрузить все выбранные файлы и перечитать список вложений. */
|
/**
|
||||||
uploadPending: (prepare?: PrepareFiles) => Promise<void>;
|
* Загрузить все выбранные файлы и перечитать список вложений. target — когда
|
||||||
|
* элемент только что создан и его id ещё не дошёл до хука (сохранение новой
|
||||||
|
* заявки); не задан — грузим в listRef/itemId хука.
|
||||||
|
*/
|
||||||
|
uploadPending: (prepare?: PrepareFiles, target?: IAttachmentTarget) => Promise<void>;
|
||||||
/** Ошибка последнего удаления — показать рядом со списком. */
|
/** Ошибка последнего удаления — показать рядом со списком. */
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
}
|
}
|
||||||
@@ -26,14 +36,16 @@ export interface IAttachmentEditor {
|
|||||||
const keepAsIs: PrepareFiles = (_groupKey, files) => files;
|
const keepAsIs: PrepareFiles = (_groupKey, files) => files;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Правка вложений уже поданной заявки — для согласующего с полным правом
|
* Правка вложений заявки: удаление — сразу, новые файлы копятся и загружаются
|
||||||
* (см. shared/approval/fullEditRights.ts). Удаление — сразу, как в форме
|
* вместе с сохранением полей (чтобы не было второй кнопки сохранения).
|
||||||
* автора; новые файлы копятся и загружаются вместе с сохранением полей
|
* Используется в просмотре согласующего с полным правом (см. shared/approval/
|
||||||
* (кнопка «Сохранить» или решение), чтобы не было второй кнопки сохранения.
|
* fullEditRights.ts) и в формах подачи.
|
||||||
*
|
*
|
||||||
* listRef — Title текущей партиции или url архивной (см. attachments.service).
|
* listRef — Title текущей партиции или url архивной (см. attachments.service).
|
||||||
|
* itemId — undefined, пока новая заявка не сохранена: удалять ещё нечего, а
|
||||||
|
* загрузка идёт с явным target.
|
||||||
*/
|
*/
|
||||||
export function useAttachmentEditor(listRef: string, itemId: number): IAttachmentEditor {
|
export function useAttachmentEditor(listRef: string, itemId: number | undefined): IAttachmentEditor {
|
||||||
const [items, setItems] = useState<IAttachmentInfo[]>([]);
|
const [items, setItems] = useState<IAttachmentInfo[]>([]);
|
||||||
const [pending, setPendingState] = useState<Record<string, File[]>>({});
|
const [pending, setPendingState] = useState<Record<string, File[]>>({});
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
@@ -42,22 +54,27 @@ export function useAttachmentEditor(listRef: string, itemId: number): IAttachmen
|
|||||||
setPendingState((prev) => ({ ...prev, [groupKey]: files }));
|
setPendingState((prev) => ({ ...prev, [groupKey]: files }));
|
||||||
|
|
||||||
const remove = (fileName: string): void => {
|
const remove = (fileName: string): void => {
|
||||||
|
if (itemId === undefined) return;
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
deleteAttachment(listRef, itemId, fileName)
|
deleteAttachment(listRef, itemId, fileName)
|
||||||
.then(() => setItems((prev) => prev.filter((a) => a.fileName !== fileName)))
|
.then(() => setItems((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||||
.catch((e) => setError(`Не удалось удалить файл «${fileName}»: ${friendlyErrorMessage(e)}`));
|
.catch((e) => setError(`Не удалось удалить файл «${fileName}»: ${friendlyErrorMessage(e)}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadPending = async (prepare: PrepareFiles = keepAsIs): Promise<void> => {
|
const uploadPending = async (prepare: PrepareFiles = keepAsIs, target?: IAttachmentTarget): Promise<void> => {
|
||||||
const files = Object.keys(pending).reduce<File[]>(
|
const files = Object.keys(pending).reduce<File[]>(
|
||||||
(acc, groupKey) => acc.concat(prepare(groupKey, pending[groupKey])),
|
(acc, groupKey) => acc.concat(prepare(groupKey, pending[groupKey])),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
await uploadAttachments(listRef, itemId, files);
|
const to = target ?? (itemId === undefined ? undefined : { listRef, itemId });
|
||||||
|
if (!to) {
|
||||||
|
throw new Error('Не удалось прикрепить файлы: заявка ещё не сохранена.');
|
||||||
|
}
|
||||||
|
await uploadAttachments(to.listRef, to.itemId, files);
|
||||||
// Загруженные убираем из выбора, иначе повторное сохранение продублирует их.
|
// Загруженные убираем из выбора, иначе повторное сохранение продублирует их.
|
||||||
setPendingState({});
|
setPendingState({});
|
||||||
setItems(await getAttachments(listRef, itemId));
|
setItems(await getAttachments(to.listRef, to.itemId));
|
||||||
};
|
};
|
||||||
|
|
||||||
return { items, setItems, pending, setPending, remove, uploadPending, error };
|
return { items, setItems, pending, setPending, remove, uploadPending, error };
|
||||||
|
|||||||
Reference in New Issue
Block a user