Files
klz-cables.com/.pnpm-store/v10/files/34/b52e27e2562db15e1fdc3883f6ab451ef3c9801de3bc34d7c22d7c1a1a1bdb39f2ffbbe2979f98a63a8d351fd6ff3b344abb66befba26f9b42ec5b65e88816
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

794 lines
26 KiB
Plaintext

'use client';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { dequal } from 'dequal/lite';
import { formatAdminURL, wordBoundariesRegex } from 'payload/shared';
import * as qs from 'qs-esm';
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { AddNewRelation } from '../../elements/AddNewRelation/index.js';
import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js';
import { useListDrawer } from '../../elements/ListDrawer/index.js';
import { ReactSelect } from '../../elements/ReactSelect/index.js';
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js';
import { FieldDescription } from '../../fields/FieldDescription/index.js';
import { FieldError } from '../../fields/FieldError/index.js';
import { FieldLabel } from '../../fields/FieldLabel/index.js';
import { useDebouncedCallback } from '../../hooks/useDebouncedCallback.js';
import { useEffectEvent } from '../../hooks/useEffectEvent.js';
import { useQueue } from '../../hooks/useQueue.js';
import { useAuth } from '../../providers/Auth/index.js';
import { useConfig } from '../../providers/Config/index.js';
import { useDocumentEvents } from '../../providers/DocumentEvents/index.js';
import { useLocale } from '../../providers/Locale/index.js';
import { useTranslation } from '../../providers/Translation/index.js';
import { sanitizeFilterOptionsQuery } from '../../utilities/sanitizeFilterOptionsQuery.js';
import { fieldBaseClass } from '../shared/index.js';
import { createRelationMap } from './createRelationMap.js';
import { findOptionsByValue } from './findOptionsByValue.js';
import { optionsReducer } from './optionsReducer.js';
import './index.scss';
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js';
import { SingleValue } from './select-components/SingleValue/index.js';
const baseClass = 'relationship';
export const RelationshipInput = props => {
const {
AfterInput,
allowCreate = true,
allowEdit = true,
appearance = 'select',
BeforeInput,
className,
description,
Description,
Error,
filterOptions,
formatDisplayedOptions,
hasMany,
initialValue,
isSortable = true,
label,
Label,
localized,
maxResultsPerRequest = 10,
onChange,
path,
placeholder,
readOnly,
relationTo,
required,
showError,
sortOptions,
style,
value
} = props;
const {
config,
getEntityConfig
} = useConfig();
const {
routes: {
api
},
serverURL
} = config;
const {
i18n,
t
} = useTranslation();
const {
permissions
} = useAuth();
const {
code: locale
} = useLocale();
const [currentlyOpenRelationship, setCurrentlyOpenRelationship] = useState({
id: undefined,
collectionSlug: undefined,
hasReadPermission: false
});
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1);
const [lastLoadedPage, setLastLoadedPage] = useState({});
const [errorLoading, setErrorLoading] = useState('');
const [search, setSearch] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [enableWordBoundarySearch, setEnableWordBoundarySearch] = useState(false);
const [menuIsOpen, setMenuIsOpen] = useState(false);
const hasLoadedFirstPageRef = useRef(false);
const {
queueTask
} = useQueue();
const [options, dispatchOptions] = useReducer(optionsReducer, []);
const valueRef = useRef(value);
const [DocumentDrawer,, {
drawerSlug,
isDrawerOpen,
openDrawer
}] = useDocumentDrawer({
id: currentlyOpenRelationship.id,
collectionSlug: currentlyOpenRelationship.collectionSlug
});
// Filter selected values from displaying in the list drawer
const listDrawerFilterOptions = useMemo(() => {
let newFilterOptions = filterOptions;
if (value) {
const valuesByRelation = (hasMany === false ? [value] : value).reduce((acc, val) => {
if (!acc[val.relationTo]) {
acc[val.relationTo] = [];
}
acc[val.relationTo].push(val.value);
return acc;
}, {});
(Array.isArray(relationTo) ? relationTo : [relationTo]).forEach(relation => {
newFilterOptions = {
...(newFilterOptions || {}),
[relation]: {
...(typeof filterOptions?.[relation] === 'object' ? filterOptions[relation] : {}),
...(valuesByRelation[relation] ? {
id: {
not_in: valuesByRelation[relation]
}
} : {})
}
};
});
}
return newFilterOptions;
}, [filterOptions, value, hasMany, relationTo]);
const [ListDrawer,, {
closeDrawer: closeListDrawer,
isDrawerOpen: isListDrawerOpen,
openDrawer: openListDrawer
}] = useListDrawer({
collectionSlugs: relationTo,
filterOptions: listDrawerFilterOptions
});
const onListSelect = useCallback(({
collectionSlug,
doc
}) => {
if (hasMany) {
onChange([...(Array.isArray(value) ? value : []), {
relationTo: collectionSlug,
value: doc.id
}]);
} else if (hasMany === false) {
onChange({
relationTo: collectionSlug,
value: doc.id
});
}
closeListDrawer();
}, [hasMany, onChange, closeListDrawer, value]);
const openDrawerWhenRelationChanges = useRef(false);
const updateResults = useCallback(({
filterOptions: filterOptions_0,
hasMany: hasManyArg,
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
lastLoadedPage: lastLoadedPageArg,
onSuccess,
search: searchArg,
sort,
value: valueArg
}) => {
if (!permissions) {
return;
}
queueTask(async () => {
const lastFullyLoadedRelationToUse = typeof lastFullyLoadedRelationArg !== 'undefined' ? lastFullyLoadedRelationArg : -1;
const relations = Array.isArray(relationTo) ? relationTo : [relationTo];
const relationsToFetch = lastFullyLoadedRelationToUse === -1 ? relations : relations.slice(lastFullyLoadedRelationToUse + 1);
let resultsFetched = 0;
const relationMap = createRelationMap(hasManyArg === true ? {
hasMany: true,
relationTo,
value: valueArg
} : {
hasMany: false,
relationTo,
value: valueArg
});
if (!errorLoading) {
await relationsToFetch.reduce(async (priorRelation, relation_0) => {
const relationFilterOption = filterOptions_0?.[relation_0];
let lastLoadedPageToUse;
if (search !== searchArg) {
lastLoadedPageToUse = 1;
} else {
lastLoadedPageToUse = (lastLoadedPageArg[relation_0] || 0) + 1;
}
await priorRelation;
if (relationFilterOption === false) {
setLastFullyLoadedRelation(relations.indexOf(relation_0));
return Promise.resolve();
}
if (resultsFetched < 10) {
const collection = getEntityConfig({
collectionSlug: relation_0
});
const fieldToSearch = collection?.admin?.useAsTitle || 'id';
let fieldToSort = collection?.defaultSort || 'id';
if (typeof sortOptions === 'string') {
fieldToSort = sortOptions;
} else if (sortOptions?.[relation_0]) {
fieldToSort = sortOptions[relation_0];
}
const query = {
depth: 0,
draft: true,
limit: maxResultsPerRequest,
locale,
page: lastLoadedPageToUse,
select: {
[fieldToSearch]: true
},
sort: fieldToSort,
where: {
and: [{
id: {
not_in: relationMap[relation_0]
}
}]
}
};
if (searchArg) {
query.where.and.push({
[fieldToSearch]: {
like: searchArg
}
});
}
if (relationFilterOption && typeof relationFilterOption !== 'boolean') {
query.where.and.push(relationFilterOption);
}
sanitizeFilterOptionsQuery(query.where);
const response = await fetch(formatAdminURL({
apiRoute: api,
path: `/${relation_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'
});
if (response.ok) {
const data = await response.json();
setLastLoadedPage(prevState => {
return {
...prevState,
[relation_0]: lastLoadedPageToUse
};
});
if (!data.nextPage) {
setLastFullyLoadedRelation(relations.indexOf(relation_0));
}
if (data.docs.length > 0) {
resultsFetched += data.docs.length;
dispatchOptions({
type: 'ADD',
collection,
config,
docs: data.docs,
i18n,
sort
});
}
} else if (response.status === 403) {
setLastFullyLoadedRelation(relations.indexOf(relation_0));
dispatchOptions({
type: 'ADD',
collection,
config,
docs: [],
i18n,
ids: relationMap[relation_0],
sort
});
} else {
setErrorLoading(t('error:unspecific'));
}
}
}, Promise.resolve());
if (typeof onSuccess === 'function') {
onSuccess();
}
}
});
}, [permissions, queueTask, relationTo, errorLoading, search, getEntityConfig, sortOptions, maxResultsPerRequest, locale, api, i18n, config, t]);
const updateSearch = useDebouncedCallback(({
hasMany: hasManyArg_0,
search: searchArg_0,
value: value_0
}) => {
updateResultsEffectEvent({
filterOptions,
lastLoadedPage: {},
onSuccess: () => {
setIsLoading(false);
},
search: searchArg_0,
sort: true,
...(hasManyArg_0 === true ? {
hasMany: hasManyArg_0,
value: value_0
} : {
hasMany: hasManyArg_0,
value: value_0
})
});
setSearch(searchArg_0);
}, 300);
const handleInputChange = useCallback(options_0 => {
if (search !== options_0.search) {
setIsLoading(true);
setLastLoadedPage({});
updateSearch(options_0);
}
}, [search, updateSearch]);
const handleValueChange = useEffectEvent(({
hasMany: hasManyArg_1,
value: value_1
}) => {
const relationMap_0 = createRelationMap(hasManyArg_1 === true ? {
hasMany: hasManyArg_1,
relationTo,
value: value_1
} : {
hasMany: hasManyArg_1,
relationTo,
value: value_1
});
void Object.entries(relationMap_0).reduce(async (priorRelation_0, [relation_1, ids]) => {
await priorRelation_0;
const idsToLoad = ids.filter(id => {
return !options.find(optionGroup => optionGroup?.options?.find(option => option.value === id && option.relationTo === relation_1));
});
if (idsToLoad.length > 0) {
const collection_0 = getEntityConfig({
collectionSlug: relation_1
});
const fieldToSelect = collection_0?.admin?.useAsTitle || 'id';
const query_0 = {
depth: 0,
draft: true,
limit: idsToLoad.length,
locale,
select: {
[fieldToSelect]: true
},
where: {
id: {
in: idsToLoad
}
}
};
if (!errorLoading) {
const response_0 = await fetch(formatAdminURL({
apiRoute: api,
path: `/${relation_1}`
}), {
body: qs.stringify(query_0),
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_0.ok) {
const data_0 = await response_0.json();
docs = data_0.docs;
}
dispatchOptions({
type: 'ADD',
collection: collection_0,
config,
docs,
i18n,
ids: idsToLoad,
sort: true
});
}
}
}, Promise.resolve());
});
const {
mostRecentUpdate
} = useDocumentEvents();
const handleDocumentUpdateEvent = useEffectEvent(mostRecentUpdate_0 => {
if (!value) {
return false;
}
const docID = mostRecentUpdate_0.doc.id;
let isMatchingUpdate = false;
if (mostRecentUpdate_0.operation === 'update') {
if (hasMany === true) {
const currentValue = Array.isArray(value) ? value : [value];
isMatchingUpdate = currentValue.some(option_0 => {
return option_0.value === docID && option_0.relationTo === mostRecentUpdate_0.entitySlug;
});
} else if (hasMany === false) {
isMatchingUpdate = value?.value === docID && value?.relationTo === mostRecentUpdate_0.entitySlug;
}
} else if (mostRecentUpdate_0.operation === 'create') {
// "Create New" drawer operations on the same level as this drawer should
// set the value to the newly created document.
// See test "should create document within document drawer > has one"
isMatchingUpdate = mostRecentUpdate_0.drawerSlug === drawerSlug;
}
if (!isMatchingUpdate) {
return;
}
const collectionConfig = getEntityConfig({
collectionSlug: mostRecentUpdate_0.entitySlug
});
dispatchOptions({
type: 'UPDATE',
collection: collectionConfig,
config,
doc: mostRecentUpdate_0.doc,
i18n
});
if (hasMany) {
const currentValue_0 = value ? Array.isArray(value) ? value : [value] : [];
const valuesToSet = currentValue_0.map(option_1 => {
return {
relationTo: option_1.value === docID ? mostRecentUpdate_0.entitySlug : option_1.relationTo,
value: option_1.value
};
});
onChange(valuesToSet);
} else if (hasMany === false) {
onChange({
relationTo: mostRecentUpdate_0.entitySlug,
value: docID
});
}
});
/**
* Listen to document update events. If you edit a related document from a drawer and save it, this event
* will be triggered. We then need up update the label of this relationship input, as the useAsLabel field could have changed.
*
* We listen to this event instead of using the onSave callback on the document drawer, as the onSave callback is not triggered
* when you save a document from a drawer opened by a *different* relationship (or any other) field.
*/
useEffect(() => {
if (mostRecentUpdate) {
handleDocumentUpdateEvent(mostRecentUpdate);
}
}, [mostRecentUpdate]);
const onDuplicate = useCallback(args => {
dispatchOptions({
type: 'ADD',
collection: args.collectionConfig,
config,
docs: [args.doc],
i18n,
sort: true
});
if (hasMany) {
onChange(value ? value.concat({
relationTo: args.collectionConfig.slug,
value: args.doc.id
}) : null);
} else if (hasMany === false) {
onChange({
relationTo: args.collectionConfig.slug,
value: args.doc.id
});
}
}, [i18n, config, hasMany, onChange, value]);
const onDelete = useCallback(args_0 => {
dispatchOptions({
id: args_0.id,
type: 'REMOVE',
collection: args_0.collectionConfig,
config,
i18n
});
if (hasMany) {
onChange(value ? value.filter(option_2 => {
return option_2.value !== args_0.id;
}) : null);
} else {
onChange(null);
}
return;
}, [i18n, config, hasMany, onChange, value]);
const filterOption = useCallback((item, searchFilter) => {
if (!searchFilter) {
return true;
}
const r = wordBoundariesRegex(searchFilter || '');
// breaking the labels to search into smaller parts increases performance
const breakApartThreshold = 250;
let labelString = String(item.label);
// strings less than breakApartThreshold length won't be chunked
while (labelString.length > breakApartThreshold) {
// slicing by the next space after the length of the search input prevents slicing the string up by partial words
const indexOfSpace = labelString.indexOf(' ', searchFilter.length);
if (r.test(labelString.slice(0, indexOfSpace === -1 ? searchFilter.length : indexOfSpace + 1))) {
return true;
}
labelString = labelString.slice(indexOfSpace === -1 ? searchFilter.length : indexOfSpace + 1);
}
return r.test(labelString.slice(-breakApartThreshold));
}, []);
const onDocumentOpen = useCallback(({
id: id_0,
collectionSlug: collectionSlug_0,
hasReadPermission,
openInNewTab
}) => {
if (openInNewTab) {
if (hasReadPermission && id_0 && collectionSlug_0) {
const docUrl = formatAdminURL({
adminRoute: config.routes.admin,
path: `/collections/${collectionSlug_0}/${id_0}`,
serverURL
});
window.open(docUrl, '_blank');
}
} else {
openDrawerWhenRelationChanges.current = true;
setCurrentlyOpenRelationship({
id: id_0,
collectionSlug: collectionSlug_0,
hasReadPermission
});
}
}, [config.routes.admin, serverURL]);
const updateResultsEffectEvent = useEffectEvent(args_1 => {
return updateResults(args_1);
});
// When (`relationTo` || `filterOptions` || `locale`) changes, reset component
// Note - effect should not run on first run
useEffect(() => {
// If the menu is open while filterOptions changes
// due to latency of form state and fast clicking into this field,
// re-fetch options
if (hasLoadedFirstPageRef.current && menuIsOpen) {
setIsLoading(true);
void updateResultsEffectEvent({
filterOptions,
lastLoadedPage: {},
onSuccess: () => {
hasLoadedFirstPageRef.current = true;
setIsLoading(false);
},
...(hasMany === true ? {
hasMany,
value: valueRef.current
} : {
hasMany,
value: valueRef.current
})
});
}
// If the menu is not open, still reset the field state
// because we need to get new options next time the menu opens
dispatchOptions({
type: 'CLEAR',
exemptValues: valueRef.current
});
setLastFullyLoadedRelation(-1);
setLastLoadedPage({});
}, [relationTo, filterOptions, locale, path, menuIsOpen, hasMany]);
const prevValue = useRef(value);
const isFirstRenderRef = useRef(true);
// ///////////////////////////////////
// Ensure we have an option for each value
// ///////////////////////////////////
useEffect(() => {
if (isFirstRenderRef.current || !dequal(value, prevValue.current)) {
handleValueChange(hasMany === true ? {
hasMany,
value
} : {
hasMany,
value
});
}
isFirstRenderRef.current = false;
prevValue.current = value;
}, [value, hasMany]);
// Determine if we should switch to word boundary search
useEffect(() => {
const relations_0 = Array.isArray(relationTo) ? relationTo : [relationTo];
const isIdOnly = relations_0.reduce((idOnly, relation_2) => {
const collection_1 = getEntityConfig({
collectionSlug: relation_2
});
const fieldToSearch_0 = collection_1?.admin?.useAsTitle || 'id';
return fieldToSearch_0 === 'id' && idOnly;
}, true);
setEnableWordBoundarySearch(!isIdOnly);
}, [relationTo, getEntityConfig]);
useEffect(() => {
if (openDrawerWhenRelationChanges.current) {
openDrawer();
openDrawerWhenRelationChanges.current = false;
}
}, [openDrawer, currentlyOpenRelationship]);
useEffect(() => {
// needed to sync the ref value when other fields influence the value
// i.e. when a drawer is opened and the value is set
valueRef.current = value;
}, [value]);
const valueToRender = findOptionsByValue({
allowEdit,
options,
value
});
if (!Array.isArray(valueToRender) && valueToRender?.value === 'null') {
valueToRender.value = null;
}
return /*#__PURE__*/_jsxs("div", {
className: [fieldBaseClass, baseClass, className, showError && 'error', errorLoading && 'error-loading', readOnly && `${baseClass}--read-only`, !readOnly && allowCreate && `${baseClass}--allow-create`].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__*/_jsxs("div", {
className: `${fieldBaseClass}__wrap`,
children: [/*#__PURE__*/_jsx(RenderCustomComponent, {
CustomComponent: Error,
Fallback: /*#__PURE__*/_jsx(FieldError, {
path: path,
showError: showError
})
}), BeforeInput, errorLoading ? /*#__PURE__*/_jsx("div", {
className: `${baseClass}__error-loading`,
children: errorLoading
}) : /*#__PURE__*/_jsxs("div", {
className: `${baseClass}__wrap`,
children: [/*#__PURE__*/_jsx(ReactSelect, {
backspaceRemovesValue: !(isDrawerOpen || isListDrawerOpen),
components: {
MultiValueLabel,
SingleValue,
...(appearance !== 'select' && {
DropdownIndicator: null
})
},
customProps: {
disableKeyDown: isDrawerOpen || isListDrawerOpen,
disableMouseDown: isDrawerOpen || isListDrawerOpen,
onDocumentOpen
},
disabled: readOnly || isDrawerOpen || isListDrawerOpen,
filterOption: enableWordBoundarySearch ? filterOption : undefined,
getOptionValue: option_3 => {
if (!option_3) {
return undefined;
}
return hasMany && Array.isArray(relationTo) ? `${option_3.relationTo}_${option_3.value}` : option_3.value;
},
isLoading: appearance === 'select' && isLoading,
isMulti: hasMany,
isSearchable: appearance === 'select',
isSortable: isSortable,
menuIsOpen: appearance === 'select' ? menuIsOpen : false,
onChange: !readOnly ? selected => {
if (hasMany) {
if (selected === null) {
valueRef.current = [];
onChange([]);
} else {
valueRef.current = selected;
onChange(selected);
}
} else if (hasMany === false) {
if (selected === null) {
valueRef.current = null;
onChange(null);
} else {
valueRef.current = selected;
onChange(selected);
}
}
} : undefined,
onInputChange: newSearch => handleInputChange({
search: newSearch,
...(hasMany === true ? {
hasMany,
value
} : {
hasMany,
value
})
}),
onMenuClose: () => {
setMenuIsOpen(false);
},
onMenuOpen: () => {
if (appearance === 'drawer') {
openListDrawer();
} else if (appearance === 'select') {
setMenuIsOpen(true);
if (!hasLoadedFirstPageRef.current) {
setIsLoading(true);
updateResultsEffectEvent({
filterOptions,
lastLoadedPage: {},
onSuccess: () => {
hasLoadedFirstPageRef.current = true;
setIsLoading(false);
},
...(hasMany === true ? {
hasMany,
value
} : {
hasMany,
value
})
});
}
}
},
onMenuScrollToBottom: () => {
setIsLoading(true);
updateResultsEffectEvent({
filterOptions,
lastFullyLoadedRelation,
lastLoadedPage,
onSuccess: () => {
setIsLoading(false);
},
search,
sort: false,
...(hasMany === true ? {
hasMany,
value: initialValue
} : {
hasMany,
value: initialValue
})
});
},
options: typeof formatDisplayedOptions === 'function' ? formatDisplayedOptions(options) : options,
placeholder: placeholder,
showError: showError,
value: valueToRender ?? null
}), !readOnly && allowCreate && /*#__PURE__*/_jsx(AddNewRelation, {
path: path,
relationTo: relationTo,
...(hasMany === true ? {
hasMany,
onChange,
value
} : {
hasMany,
onChange,
value
})
})]
}), AfterInput, /*#__PURE__*/_jsx(RenderCustomComponent, {
CustomComponent: Description,
Fallback: /*#__PURE__*/_jsx(FieldDescription, {
description: description,
path: path
})
})]
}), currentlyOpenRelationship.collectionSlug && currentlyOpenRelationship.hasReadPermission && /*#__PURE__*/_jsx(DocumentDrawer, {
onDelete: onDelete,
onDuplicate: onDuplicate
}), appearance === 'drawer' && !readOnly && /*#__PURE__*/_jsx(ListDrawer, {
allowCreate: allowCreate,
enableRowSelections: false,
onSelect: onListSelect
})]
});
};
//# sourceMappingURL=Input.js.map