Files
klz-cables.com/.pnpm-store/v10/files/9e/6eaf5469e377232a269d59ad30dfcd9490248b4c60ae6b47635ca7edf7803ecaad4dd4288eaa5bd9c0847a737c5c2dd804b3347c1217a2a3c4fc5232ee13da
Marc Mintel 5397309103
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
fix(products): fix breadcrumbs and product filtering (backport from main)
2026-02-24 16:04:21 +01:00

609 lines
22 KiB
Plaintext

'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useModal } from '@faceless-ui/modal';
import { formatAdminURL } from 'payload/shared';
import * as qs from 'qs-esm';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useBulkUpload } from '../../elements/BulkUpload/index.js';
import { Button } from '../../elements/Button/index.js';
import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js';
import { Dropzone } from '../../elements/Dropzone/index.js';
import { useListDrawer } from '../../elements/ListDrawer/index.js';
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js';
import { ShimmerEffect } from '../../elements/ShimmerEffect/index.js';
import { FieldDescription } from '../../fields/FieldDescription/index.js';
import { FieldError } from '../../fields/FieldError/index.js';
import { FieldLabel } from '../../fields/FieldLabel/index.js';
import { useAuth } from '../../providers/Auth/index.js';
import { useLocale } from '../../providers/Locale/index.js';
import { useTranslation } from '../../providers/Translation/index.js';
import { normalizeRelationshipValue } from '../../utilities/normalizeRelationshipValue.js';
import { fieldBaseClass } from '../shared/index.js';
import { UploadComponentHasMany } from './HasMany/index.js';
import './index.scss';
import { UploadComponentHasOne } from './HasOne/index.js';
export const baseClass = 'upload';
export function UploadInput(props) {
const {
AfterInput,
allowCreate,
api,
BeforeInput,
className,
Description,
description,
displayPreview,
Error,
filterOptions: filterOptionsFromProps,
hasMany,
isSortable,
Label,
label,
localized,
maxRows,
onChange: onChangeFromProps,
path,
readOnly,
relationTo,
required,
serverURL,
showError,
style,
value
} = props;
const [populatedDocs, setPopulatedDocs] = React.useState();
const [activeRelationTo] = React.useState(Array.isArray(relationTo) ? relationTo[0] : relationTo);
const {
openModal
} = useModal();
const {
drawerSlug,
setCollectionSlug,
setInitialFiles,
setMaxFiles,
setOnSuccess,
setSelectableCollections
} = useBulkUpload();
const {
permissions
} = useAuth();
const {
code
} = useLocale();
const {
i18n,
t
} = useTranslation();
// This will be used by the bulk upload to allow you to select only collections you have create permissions for
const collectionSlugsWithCreatePermission = useMemo(() => {
if (Array.isArray(relationTo)) {
return relationTo.filter(relation => permissions?.collections && permissions.collections?.[relation]?.create);
}
return [];
}, [relationTo, permissions]);
const filterOptions = useMemo(() => {
const isPoly = Array.isArray(relationTo);
if (!value) {
return filterOptionsFromProps;
}
// Group existing IDs by relation
const existingIdsByRelation = {};
const values = Array.isArray(value) ? value : [value];
for (const val of values) {
if (isPoly && typeof val === 'object' && 'relationTo' in val) {
// Poly upload - group by relationTo
if (!existingIdsByRelation[val.relationTo]) {
existingIdsByRelation[val.relationTo] = [];
}
existingIdsByRelation[val.relationTo].push(val.value);
} else if (!isPoly) {
// Non-poly upload - all IDs belong to the single collection
const collection = relationTo;
if (!existingIdsByRelation[collection]) {
existingIdsByRelation[collection] = [];
}
const id = typeof val === 'object' && 'value' in val ? val.value : val;
if (typeof id === 'string' || typeof id === 'number') {
existingIdsByRelation[collection].push(id);
}
}
}
// Build filter options for each collection
const newFilterOptions = {
...filterOptionsFromProps
};
const relations = isPoly ? relationTo : [relationTo];
relations.forEach(relation_0 => {
const existingIds = existingIdsByRelation[relation_0] || [];
newFilterOptions[relation_0] = {
...(filterOptionsFromProps?.[relation_0] || {}),
id: {
...(filterOptionsFromProps?.[relation_0]?.id || {}),
not_in: (filterOptionsFromProps?.[relation_0]?.id?.not_in || []).concat(existingIds)
}
};
});
return newFilterOptions;
}, [value, filterOptionsFromProps, relationTo]);
const [ListDrawer,, {
closeDrawer: closeListDrawer,
openDrawer: openListDrawer
}] = useListDrawer({
collectionSlugs: typeof relationTo === 'string' ? [relationTo] : relationTo,
filterOptions
});
const [CreateDocDrawer,, {
closeDrawer: closeCreateDocDrawer,
openDrawer: openCreateDocDrawer
}] = useDocumentDrawer({
collectionSlug: activeRelationTo
});
/**
* Track the last loaded value to prevent unnecessary reloads
*/
const loadedValueRef = React.useRef(null);
const canCreate = useMemo(() => {
if (!allowCreate) {
return false;
}
if (typeof activeRelationTo === 'string') {
if (permissions?.collections && permissions.collections?.[activeRelationTo]?.create) {
return true;
}
}
return false;
}, [activeRelationTo, permissions, allowCreate]);
const onChange = React.useCallback(newValue => {
if (typeof onChangeFromProps === 'function') {
onChangeFromProps(newValue);
}
}, [onChangeFromProps]);
const populateDocs = React.useCallback(async items => {
if (!items?.length) {
return [];
}
// 1. Group IDs by collection
const grouped = {};
items.forEach(({
relationTo: relationTo_0,
value: value_0
}) => {
if (!grouped[relationTo_0]) {
grouped[relationTo_0] = [];
}
// Ensure we extract the actual ID value, not an object
let idValue = value_0;
if (value_0 && typeof value_0 === 'object' && 'value' in value_0) {
idValue = value_0.value;
}
grouped[relationTo_0].push(idValue);
});
// 2. Fetch per collection
const fetches = Object.entries(grouped).map(async ([collection_0, ids]) => {
const query = {
depth: 0,
draft: true,
limit: ids.length,
locale: code,
where: {
and: [{
id: {
in: ids
}
}]
}
};
const response = await fetch(formatAdminURL({
apiRoute: api,
path: `/${collection_0}`
}), {
body: qs.stringify(query),
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/x-www-form-urlencoded',
'X-Payload-HTTP-Method-Override': 'GET'
},
method: 'POST'
});
let docs = [];
if (response.ok) {
const data = await response.json();
docs = data.docs;
}
// Map docs by ID for fast lookup
const docsById = docs.reduce((acc, doc) => {
acc[doc.id] = doc;
return acc;
}, {});
return {
collection: collection_0,
docsById
};
});
const results = await Promise.all(fetches);
// 3. Build lookup
const lookup = {};
results.forEach(({
collection: collection_1,
docsById: docsById_0
}) => {
lookup[collection_1] = docsById_0;
});
// 4. Reconstruct in input order, add placeholders if missing
const sortedDocs = items.map(({
relationTo: relationTo_1,
value: value_1
}) => {
const doc_0 = lookup[relationTo_1]?.[value_1] || {
id: value_1,
filename: `${t('general:untitled')} - ID: ${value_1}`,
isPlaceholder: true
};
return {
relationTo: relationTo_1,
value: doc_0
};
});
return sortedDocs;
}, [api, code, i18n.language, t]);
const normalizeValue = useCallback(value_2 => normalizeRelationshipValue(value_2, relationTo), [relationTo]);
const onUploadSuccess = useCallback(uploadedForms => {
const isPoly_0 = Array.isArray(relationTo);
if (hasMany) {
const newValues = uploadedForms.map(form => isPoly_0 ? {
relationTo: form.collectionSlug,
value: form.doc.id
} : form.doc.id);
// Normalize existing values before merging
const normalizedExisting = Array.isArray(value) ? value.map(normalizeValue) : [];
const mergedValue = [...normalizedExisting, ...newValues];
onChange(mergedValue);
setPopulatedDocs(currentDocs => [...(currentDocs || []), ...uploadedForms.map(form_0 => ({
relationTo: form_0.collectionSlug,
value: form_0.doc
}))]);
} else {
const firstDoc = uploadedForms[0];
const newValue_0 = isPoly_0 ? {
relationTo: firstDoc.collectionSlug,
value: firstDoc.doc.id
} : firstDoc.doc.id;
onChange(newValue_0);
setPopulatedDocs([{
relationTo: firstDoc.collectionSlug,
value: firstDoc.doc
}]);
}
}, [value, onChange, hasMany, relationTo, normalizeValue]);
const onLocalFileSelection = React.useCallback(fileList => {
let fileListToUse = fileList;
if (!hasMany && fileList && fileList.length > 1) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(fileList[0]);
fileListToUse = dataTransfer.files;
}
if (fileListToUse) {
setInitialFiles(fileListToUse);
}
// Use activeRelationTo for poly uploads, or relationTo as string for single collection
const collectionToUse = Array.isArray(relationTo) ? activeRelationTo : relationTo;
setCollectionSlug(collectionToUse);
if (Array.isArray(collectionSlugsWithCreatePermission)) {
setSelectableCollections(collectionSlugsWithCreatePermission);
}
if (typeof maxRows === 'number') {
setMaxFiles(maxRows);
}
openModal(drawerSlug);
}, [hasMany, relationTo, activeRelationTo, setCollectionSlug, collectionSlugsWithCreatePermission, maxRows, openModal, drawerSlug, setInitialFiles, setSelectableCollections, setMaxFiles]);
// only hasMany can bulk select
const onListBulkSelect = React.useCallback(async docs_0 => {
const isPoly_1 = Array.isArray(relationTo);
const selectedDocIDs = [];
for (const [id_0, isSelected] of docs_0) {
if (isSelected) {
selectedDocIDs.push(id_0);
}
}
const itemsToLoad = selectedDocIDs.map(id_1 => ({
relationTo: activeRelationTo,
value: id_1
}));
const loadedDocs = await populateDocs(itemsToLoad);
if (loadedDocs) {
setPopulatedDocs(currentDocs_0 => [...(currentDocs_0 || []), ...loadedDocs]);
}
const newValues_0 = selectedDocIDs.map(id_2 => isPoly_1 ? {
relationTo: activeRelationTo,
value: id_2
} : id_2);
// Normalize existing values before merging
const normalizedExisting_0 = Array.isArray(value) ? value.map(normalizeValue) : [];
onChange([...normalizedExisting_0, ...newValues_0]);
closeListDrawer();
}, [activeRelationTo, closeListDrawer, onChange, populateDocs, value, relationTo, normalizeValue]);
const onDocCreate = React.useCallback(data_0 => {
const isPoly_2 = Array.isArray(relationTo);
if (data_0.doc) {
setPopulatedDocs(currentDocs_1 => [...(currentDocs_1 || []), {
relationTo: activeRelationTo,
value: data_0.doc
}]);
const newValue_1 = isPoly_2 ? {
relationTo: activeRelationTo,
value: data_0.doc.id
} : data_0.doc.id;
onChange(newValue_1);
}
closeCreateDocDrawer();
}, [closeCreateDocDrawer, activeRelationTo, onChange, relationTo]);
const onListSelect = useCallback(async ({
collectionSlug,
doc: doc_1
}) => {
const isPoly_3 = Array.isArray(relationTo);
const loadedDocs_0 = await populateDocs([{
relationTo: collectionSlug,
value: doc_1.id
}]);
const selectedDoc = loadedDocs_0?.[0] || null;
setPopulatedDocs(currentDocs_2 => {
if (selectedDoc) {
if (hasMany) {
return [...(currentDocs_2 || []), selectedDoc];
}
return [selectedDoc];
}
return currentDocs_2;
});
if (hasMany) {
const newValue_2 = isPoly_3 ? {
relationTo: collectionSlug,
value: doc_1.id
} : doc_1.id;
// Normalize existing values before merging
const normalizedExisting_1 = Array.isArray(value) ? value.map(normalizeValue) : [];
const valueToUse = [...normalizedExisting_1, newValue_2];
onChange(valueToUse);
} else {
const valueToUse_0 = isPoly_3 ? {
relationTo: collectionSlug,
value: doc_1.id
} : doc_1.id;
onChange(valueToUse_0);
}
closeListDrawer();
}, [closeListDrawer, hasMany, populateDocs, onChange, value, relationTo, normalizeValue]);
const reloadDoc = React.useCallback(async (docID, collectionSlug_0) => {
const docs_1 = await populateDocs([{
relationTo: collectionSlug_0,
value: docID
}]);
if (docs_1[0]) {
let updatedDocsToPropogate = [];
setPopulatedDocs(currentDocs_3 => {
const existingDocIndex = currentDocs_3?.findIndex(doc_2 => {
const hasExisting = doc_2.value?.id === docs_1[0].value.id || doc_2.value?.isPlaceholder;
return hasExisting && doc_2.relationTo === collectionSlug_0;
});
if (existingDocIndex > -1) {
const updatedDocs = [...currentDocs_3];
updatedDocs[existingDocIndex] = docs_1[0];
updatedDocsToPropogate = updatedDocs;
return updatedDocs;
}
return currentDocs_3;
});
if (updatedDocsToPropogate.length && hasMany) {
onChange(updatedDocsToPropogate.map(doc_3 => doc_3.value?.id));
}
}
}, [populateDocs, onChange, hasMany]);
// only hasMany can reorder
const onReorder = React.useCallback(newValue_3 => {
const isPoly_4 = Array.isArray(relationTo);
const newValueToSave = newValue_3.map(({
relationTo: rel,
value: value_3
}) => isPoly_4 ? {
relationTo: rel,
value: value_3.id
} : value_3.id);
onChange(newValueToSave);
setPopulatedDocs(newValue_3);
}, [onChange, relationTo]);
const onRemove = React.useCallback(newValue_4 => {
const isPoly_5 = Array.isArray(relationTo);
if (!newValue_4 || newValue_4.length === 0) {
onChange(hasMany ? [] : null);
setPopulatedDocs(hasMany ? [] : null);
return;
}
const newValueToSave_0 = newValue_4.map(({
relationTo: rel_0,
value: value_4
}) => isPoly_5 ? {
relationTo: rel_0,
value: value_4.id
} : value_4.id);
onChange(hasMany ? newValueToSave_0 : newValueToSave_0[0]);
setPopulatedDocs(newValue_4);
}, [onChange, hasMany, relationTo]);
useEffect(() => {
async function loadInitialDocs() {
if (value) {
let itemsToLoad_0 = [];
if (Array.isArray(relationTo) && (typeof value === 'object' && 'relationTo' in value || Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && 'relationTo' in value[0])) {
// For poly uploads, value should already be in the format { relationTo, value }
const values_0 = Array.isArray(value) ? value : [value];
itemsToLoad_0 = values_0.filter(v => typeof v === 'object' && 'relationTo' in v).map(v_0 => {
// Ensure the value property is a simple ID, not nested
let idValue_0 = v_0.value;
while (idValue_0 && typeof idValue_0 === 'object' && idValue_0 !== null && 'value' in idValue_0) {
idValue_0 = idValue_0.value;
}
return {
relationTo: v_0.relationTo,
value: idValue_0
};
});
} else {
// This check is here to satisfy TypeScript that relationTo is a string
if (!Array.isArray(relationTo)) {
// For single collection uploads, we need to wrap the IDs
const ids_0 = Array.isArray(value) ? value : [value];
itemsToLoad_0 = ids_0.map(id_3 => {
// Extract the actual ID, handling nested objects
let idValue_1 = id_3;
while (idValue_1 && typeof idValue_1 === 'object' && idValue_1 !== null && 'value' in idValue_1) {
idValue_1 = idValue_1.value;
}
return {
relationTo,
value: idValue_1
};
});
}
}
if (itemsToLoad_0.length > 0) {
const loadedDocs_1 = await populateDocs(itemsToLoad_0);
if (loadedDocs_1) {
setPopulatedDocs(loadedDocs_1);
loadedValueRef.current = value;
}
}
} else {
// Clear populated docs when value is cleared
setPopulatedDocs([]);
loadedValueRef.current = null;
}
}
// Only load if value has changed from what we last loaded
const valueChanged = loadedValueRef.current !== value;
if (valueChanged) {
void loadInitialDocs();
}
}, [populateDocs, value, relationTo]);
useEffect(() => {
setOnSuccess(onUploadSuccess);
}, [value, path, onUploadSuccess, setOnSuccess]);
const showDropzone = !value || hasMany && Array.isArray(value) && (typeof maxRows !== 'number' || value.length < maxRows) || !hasMany && populatedDocs?.[0] && typeof populatedDocs[0].value === 'undefined';
return /*#__PURE__*/_jsxs("div", {
className: [fieldBaseClass, baseClass, className, showError && 'error', readOnly && 'read-only'].filter(Boolean).join(' '),
id: `field-${path?.replace(/\./g, '__')}`,
style: style,
children: [/*#__PURE__*/_jsx(RenderCustomComponent, {
CustomComponent: Label,
Fallback: /*#__PURE__*/_jsx(FieldLabel, {
label: label,
localized: localized,
path: path,
required: required
})
}), /*#__PURE__*/_jsx("div", {
className: `${baseClass}__wrap`,
children: /*#__PURE__*/_jsx(RenderCustomComponent, {
CustomComponent: Error,
Fallback: /*#__PURE__*/_jsx(FieldError, {
path: path,
showError: showError
})
})
}), BeforeInput, /*#__PURE__*/_jsxs("div", {
className: `${baseClass}__dropzoneAndUpload`,
children: [hasMany && Array.isArray(value) && value.length > 0 ? /*#__PURE__*/_jsx(_Fragment, {
children: populatedDocs && populatedDocs?.length > 0 ? /*#__PURE__*/_jsx(UploadComponentHasMany, {
displayPreview: displayPreview,
fileDocs: populatedDocs,
isSortable: isSortable && !readOnly,
onRemove: onRemove,
onReorder: onReorder,
readonly: readOnly,
reloadDoc: reloadDoc,
serverURL: serverURL,
showCollectionSlug: Array.isArray(relationTo)
}) : /*#__PURE__*/_jsx("div", {
className: `${baseClass}__loadingRows`,
children: value.map(id_4 => /*#__PURE__*/_jsx(ShimmerEffect, {
height: "40px"
}, typeof id_4 === 'object' ? id_4.value : id_4))
})
}) : null, !hasMany && value ? /*#__PURE__*/_jsx(_Fragment, {
children: populatedDocs && populatedDocs?.length > 0 && populatedDocs[0].value ? /*#__PURE__*/_jsx(UploadComponentHasOne, {
displayPreview: displayPreview,
fileDoc: populatedDocs[0],
onRemove: onRemove,
readonly: readOnly,
reloadDoc: reloadDoc,
serverURL: serverURL,
showCollectionSlug: Array.isArray(relationTo)
}) : populatedDocs && value && !populatedDocs?.[0]?.value ? /*#__PURE__*/_jsxs(_Fragment, {
children: [t('general:untitled'), " - ID: ", value]
}) : /*#__PURE__*/_jsx(ShimmerEffect, {
height: "62px"
})
}) : null, showDropzone ? /*#__PURE__*/_jsx(Dropzone, {
disabled: readOnly || !canCreate,
multipleFiles: hasMany,
onChange: onLocalFileSelection,
children: /*#__PURE__*/_jsxs("div", {
className: `${baseClass}__dropzoneContent`,
children: [/*#__PURE__*/_jsxs("div", {
className: `${baseClass}__dropzoneContent__buttons`,
children: [canCreate && /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(Button, {
buttonStyle: "pill",
className: `${baseClass}__createNewToggler`,
disabled: readOnly || !canCreate,
onClick: () => {
if (!readOnly) {
if (hasMany) {
onLocalFileSelection();
} else {
openCreateDocDrawer();
}
}
},
size: "small",
children: t('general:createNew')
}), /*#__PURE__*/_jsx("span", {
className: `${baseClass}__dropzoneContent__orText`,
children: t('general:or')
})]
}), /*#__PURE__*/_jsx(Button, {
buttonStyle: "pill",
className: `${baseClass}__listToggler`,
disabled: readOnly,
onClick: openListDrawer,
size: "small",
children: t('fields:chooseFromExisting')
}), /*#__PURE__*/_jsx(CreateDocDrawer, {
onSave: onDocCreate
}), /*#__PURE__*/_jsx(ListDrawer, {
allowCreate: canCreate,
enableRowSelections: hasMany,
onBulkSelect: onListBulkSelect,
onSelect: onListSelect
})]
}), canCreate && !readOnly && /*#__PURE__*/_jsxs("p", {
className: `${baseClass}__dragAndDropText`,
children: [t('general:or'), " ", t('upload:dragAndDrop')]
})]
})
}) : /*#__PURE__*/_jsx(_Fragment, {
children: !readOnly && !populatedDocs && (!value || typeof maxRows !== 'number' || Array.isArray(value) && value.length < maxRows) ? /*#__PURE__*/_jsx(ShimmerEffect, {
height: "40px"
}) : null
})]
}), AfterInput, /*#__PURE__*/_jsx(RenderCustomComponent, {
CustomComponent: Description,
Fallback: /*#__PURE__*/_jsx(FieldDescription, {
description: description,
path: path
})
})]
});
}
//# sourceMappingURL=Input.js.map