fix: remove useless rotf future flag (#23139)

This commit is contained in:
Simone 2025-03-14 11:39:02 +01:00 committed by GitHub
parent f09bff0783
commit 97e9de4427
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 2 additions and 585 deletions

View File

@ -1,6 +1,5 @@
module.exports = ({ env }) => ({
future: {
unstablePreviewSideEditor: true,
unstableRelationsOnTheFly: true,
},
});

View File

@ -141,220 +141,6 @@ export interface RelationsFormValue {
* At present we do not expose this to plugin developers, however, they are able to overwrite it themselves should
* they wish to do so.
*/
const UnstableRelationsField = React.forwardRef<HTMLDivElement, RelationsFieldProps>(
({ disabled, label, ...props }, ref) => {
const [currentPage, setCurrentPage] = React.useState(1);
const { document, model: documentModel } = useDoc();
const documentId = document?.documentId;
const { formatMessage } = useIntl();
const [{ query }] = useQueryParams();
const params = buildValidParams(query);
const isMorph = props.attribute.relation.toLowerCase().includes('morph');
const isDisabled = isMorph || disabled;
const { componentId, componentUID } = useComponent('RelationsField', ({ uid, id }) => ({
componentId: id,
componentUID: uid,
}));
const isSubmitting = useForm('RelationsList', (state) => state.isSubmitting);
React.useEffect(() => {
setCurrentPage(1);
}, [isSubmitting]);
/**
* We'll always have a documentId in a created entry, so we look for a componentId first.
* Same with `uid` and `documentModel`.
*/
const id = componentId ? componentId.toString() : documentId;
const model = componentUID ?? documentModel;
/**
* The `name` prop is a complete path to the field, e.g. `field1.field2.field3`.
* Where the above example would a nested field within two components, however
* we only require the field on the component not the complete path since we query
* individual components. Therefore we split the string and take the last item.
*/
const [targetField] = props.name.split('.').slice(-1);
const { data, isLoading, isFetching } = useGetRelationsQuery(
{
model,
targetField,
// below we don't run the query if there is no id.
id: id!,
params: {
...params,
pageSize: RELATIONS_TO_DISPLAY,
page: currentPage,
},
},
{
refetchOnMountOrArgChange: true,
skip: !id,
selectFromResult: (result) => {
return {
...result,
data: {
...result.data,
results: result.data?.results ? result.data.results : [],
},
};
},
}
);
const handleLoadMore = () => {
setCurrentPage((prev) => prev + 1);
};
const field = useField(props.name);
const isFetchingMoreRelations = isLoading || isFetching;
const realServerRelationsCount =
'pagination' in data && data.pagination ? data.pagination.total : 0;
/**
* Items that are already connected, but reordered would be in
* this list, so to get an accurate figure, we remove them.
*/
const relationsConnected =
(field.value?.connect ?? []).filter(
(rel: Relation) => data.results.findIndex((relation) => relation.id === rel.id) === -1
).length ?? 0;
const relationsDisconnected = field.value?.disconnect?.length ?? 0;
const relationsCount = realServerRelationsCount + relationsConnected - relationsDisconnected;
/**
* This is it, the source of truth for reordering in conjunction with partial loading & updating
* of relations. Relations on load are given __temp_key__ when fetched, because we don't want to
* create brand new keys everytime the data updates, just keep adding them onto the newly loaded ones.
*/
const relations = React.useMemo(() => {
const ctx = {
field: field.value,
// @ts-expect-error targetModel does exist on the attribute. But it's not typed.
href: `../${COLLECTION_TYPES}/${props.attribute.targetModel}`,
mainField: props.mainField,
};
/**
* Tidy up our data.
*/
const transformations = pipe(
removeConnected(ctx),
removeDisconnected(ctx),
addLabelAndHref(ctx)
);
const transformedRels = transformations([...data.results]);
/**
* THIS IS CRUCIAL. If you don't sort by the __temp_key__ which comes from fractional indexing
* then the list will be in the wrong order.
*/
return [...transformedRels, ...(field.value?.connect ?? [])].sort((a, b) => {
if (a.__temp_key__ < b.__temp_key__) return -1;
if (a.__temp_key__ > b.__temp_key__) return 1;
return 0;
});
}, [
data.results,
field.value,
// @ts-expect-error targetModel does exist on the attribute. But it's not typed.
props.attribute.targetModel,
props.mainField,
]);
const handleDisconnect = useHandleDisconnect(props.name, 'RelationsField');
const handleConnect: RelationsInputProps['onChange'] = (relation) => {
const [lastItemInList] = relations.slice(-1);
const item = {
id: relation.id,
apiData: {
id: relation.id,
documentId: relation.documentId,
locale: relation.locale,
},
status: relation.status,
/**
* If there's a last item, that's the first key we use to generate out next one.
*/
__temp_key__: generateNKeysBetween(lastItemInList?.__temp_key__ ?? null, null, 1)[0],
// Fallback to `id` if there is no `mainField` value, which will overwrite the above `id` property with the exact same data.
[props.mainField?.name ?? 'documentId']: relation[props.mainField?.name ?? 'documentId'],
label: getRelationLabel(relation, props.mainField),
// @ts-expect-error targetModel does exist on the attribute, but it's not typed.
href: `../${COLLECTION_TYPES}/${props.attribute.targetModel}/${relation.documentId}?${relation.locale ? `plugins[i18n][locale]=${relation.locale}` : ''}`,
};
if (ONE_WAY_RELATIONS.includes(props.attribute.relation)) {
// Remove any existing relation so they can be replaced with the new one
field.value?.connect?.forEach(handleDisconnect);
relations.forEach(handleDisconnect);
field.onChange(`${props.name}.connect`, [item]);
} else {
field.onChange(`${props.name}.connect`, [...(field.value?.connect ?? []), item]);
}
};
return (
<Flex
ref={ref}
direction="column"
gap={3}
justifyContent="space-between"
alignItems="stretch"
wrap="wrap"
>
<StyledFlex direction="column" alignItems="start" gap={2} width="100%">
<RelationsInput
disabled={isDisabled}
// NOTE: we should not default to using the documentId if the component is being created (componentUID is undefined)
id={componentUID ? (componentId ? `${componentId}` : '') : documentId}
label={`${label} ${relationsCount > 0 ? `(${relationsCount})` : ''}`}
model={model}
onChange={handleConnect}
{...props}
/>
{'pagination' in data &&
data.pagination &&
data.pagination.pageCount > data.pagination.page ? (
<TextButton
disabled={isFetchingMoreRelations}
onClick={handleLoadMore}
loading={isFetchingMoreRelations}
startIcon={<ArrowClockwise />}
// prevent the label from line-wrapping
shrink={0}
>
{formatMessage({
id: getTranslation('relation.loadMore'),
defaultMessage: 'Load More',
})}
</TextButton>
) : null}
</StyledFlex>
<UnstableRelationsList
data={relations}
serverData={data.results}
disabled={isDisabled}
name={props.name}
isLoading={isFetchingMoreRelations}
relationType={props.attribute.relation}
/>
</Flex>
);
}
);
const RelationsField = React.forwardRef<HTMLDivElement, RelationsFieldProps>(
({ disabled, label, ...props }, ref) => {
const [currentPage, setCurrentPage] = React.useState(1);
@ -846,240 +632,6 @@ interface RelationsListProps extends Pick<RelationsFieldProps, 'disabled' | 'nam
serverData: RelationResult[];
}
const UnstableRelationsList = ({
data,
serverData,
disabled,
name,
isLoading,
relationType,
}: RelationsListProps) => {
const ariaDescriptionId = React.useId();
const { formatMessage } = useIntl();
const listRef = React.useRef<FixedSizeList>(null);
const outerListRef = React.useRef<HTMLUListElement>(null);
const [overflow, setOverflow] = React.useState<'top' | 'bottom' | 'top-bottom'>();
const [liveText, setLiveText] = React.useState('');
const field = useField(name);
React.useEffect(() => {
if (data.length <= RELATIONS_TO_DISPLAY) {
return setOverflow(undefined);
}
const handleNativeScroll = (e: Event) => {
const el = e.target as HTMLUListElement;
const parentScrollContainerHeight = (el.parentNode as HTMLDivElement).scrollHeight;
const maxScrollBottom = el.scrollHeight - el.scrollTop;
if (el.scrollTop === 0) {
return setOverflow('bottom');
}
if (maxScrollBottom === parentScrollContainerHeight) {
return setOverflow('top');
}
return setOverflow('top-bottom');
};
const outerListRefCurrent = outerListRef?.current;
if (!isLoading && data.length > 0 && outerListRefCurrent) {
outerListRef.current.addEventListener('scroll', handleNativeScroll);
}
return () => {
if (outerListRefCurrent) {
outerListRefCurrent.removeEventListener('scroll', handleNativeScroll);
}
};
}, [isLoading, data.length]);
const getItemPos = (index: number) => `${index + 1} of ${data.length}`;
const handleMoveItem: UseDragAndDropOptions['onMoveItem'] = (newIndex, oldIndex) => {
const item = data[oldIndex];
setLiveText(
formatMessage(
{
id: getTranslation('dnd.reorder'),
defaultMessage: '{item}, moved. New position in list: {position}.',
},
{
item: item.label ?? item.documentId,
position: getItemPos(newIndex),
}
)
);
/**
* Splicing mutates the array, so we need to create a new array
*/
const newData = [...data];
const currentRow = data[oldIndex];
const startKey =
oldIndex > newIndex ? newData[newIndex - 1]?.__temp_key__ : newData[newIndex]?.__temp_key__;
const endKey =
oldIndex > newIndex ? newData[newIndex]?.__temp_key__ : newData[newIndex + 1]?.__temp_key__;
/**
* We're moving the relation between two other relations, so
* we need to generate a new key that keeps the order
*/
const [newKey] = generateNKeysBetween(startKey, endKey, 1);
newData.splice(oldIndex, 1);
newData.splice(newIndex, 0, { ...currentRow, __temp_key__: newKey });
/**
* Now we diff against the server to understand what's different so we
* can keep the connect array nice and tidy. It also needs reversing because
* we reverse the relations from the server in the first place.
*/
const connectedRelations = newData
.reduce<Relation[]>((acc, relation, currentIndex, array) => {
const relationOnServer = serverData.find((oldRelation) => oldRelation.id === relation.id);
const relationInFront = array[currentIndex + 1];
if (!relationOnServer || relationOnServer.__temp_key__ !== relation.__temp_key__) {
const position = relationInFront
? {
before: relationInFront.documentId,
locale: relationInFront.locale,
status:
'publishedAt' in relationInFront && relationInFront.publishedAt
? 'published'
: 'draft',
}
: { end: true };
const relationWithPosition: Relation = {
...relation,
...{
apiData: {
id: relation.id,
documentId: relation.documentId,
locale: relation.locale,
position,
},
},
};
return [...acc, relationWithPosition];
}
return acc;
}, [])
.toReversed();
field.onChange(`${name}.connect`, connectedRelations);
};
const handleGrabItem: UseDragAndDropOptions['onGrabItem'] = (index) => {
const item = data[index];
setLiveText(
formatMessage(
{
id: getTranslation('dnd.grab-item'),
defaultMessage: `{item}, grabbed. Current position in list: {position}. Press up and down arrow to change position, Spacebar to drop, Escape to cancel.`,
},
{
item: item.label ?? item.documentId,
position: getItemPos(index),
}
)
);
};
const handleDropItem: UseDragAndDropOptions['onDropItem'] = (index) => {
const { href: _href, label, ...item } = data[index];
setLiveText(
formatMessage(
{
id: getTranslation('dnd.drop-item'),
defaultMessage: `{item}, dropped. Final position in list: {position}.`,
},
{
item: label ?? item.documentId,
position: getItemPos(index),
}
)
);
};
const handleCancel: UseDragAndDropOptions['onCancel'] = (index) => {
const item = data[index];
setLiveText(
formatMessage(
{
id: getTranslation('dnd.cancel-item'),
defaultMessage: '{item}, dropped. Re-order cancelled.',
},
{
item: item.label ?? item.documentId,
}
)
);
};
const handleDisconnect = useHandleDisconnect(name, 'RelationsList');
/**
* These relation types will only ever have one item
* in their list, so you can't reorder a single item!
*/
const canReorder = !ONE_WAY_RELATIONS.includes(relationType);
const dynamicListHeight =
data.length > RELATIONS_TO_DISPLAY
? Math.min(data.length, RELATIONS_TO_DISPLAY) * (RELATION_ITEM_HEIGHT + RELATION_GUTTER) +
RELATION_ITEM_HEIGHT / 2
: Math.min(data.length, RELATIONS_TO_DISPLAY) * (RELATION_ITEM_HEIGHT + RELATION_GUTTER);
return (
<ShadowBox $overflowDirection={overflow}>
<VisuallyHidden id={ariaDescriptionId}>
{formatMessage({
id: getTranslation('dnd.instructions'),
defaultMessage: `Press spacebar to grab and re-order`,
})}
</VisuallyHidden>
<VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>
{/* @ts-expect-error width is expected, but we've not needed to pass it before. */}
<FixedSizeList
height={dynamicListHeight}
ref={listRef}
outerRef={outerListRef}
itemCount={data.length}
itemSize={RELATION_ITEM_HEIGHT + RELATION_GUTTER}
itemData={{
ariaDescribedBy: ariaDescriptionId,
canDrag: canReorder,
disabled,
handleCancel,
handleDropItem,
handleGrabItem,
handleMoveItem,
name,
handleDisconnect,
relations: data,
}}
itemKey={(index) => data[index].id}
innerElementType="ol"
>
{UnstableListItem}
</FixedSizeList>
</ShadowBox>
);
};
const RelationsList = ({
data,
serverData,
@ -1369,128 +921,6 @@ interface ListItemProps extends Pick<ListChildComponentProps, 'style' | 'index'>
};
}
const CustomTextButton = styled(TextButton)`
& > span {
font-size: ${({ theme }) => theme.fontSizes[2]};
}
`;
const UnstableListItem = ({ data, index, style }: ListItemProps) => {
const {
ariaDescribedBy,
canDrag = false,
disabled = false,
handleCancel,
handleDisconnect,
handleDropItem,
handleGrabItem,
handleMoveItem,
name,
relations,
} = data;
const { formatMessage } = useIntl();
const { id, label, status } = relations[index];
const [{ handlerId, isDragging, handleKeyDown }, relationRef, dropRef, dragRef, dragPreviewRef] =
useDragAndDrop<number, Omit<RelationDragPreviewProps, 'width'>, HTMLDivElement>(
canDrag && !disabled,
{
type: `${ItemTypes.RELATION}_${name}`,
index,
item: {
displayedValue: label,
status,
id: id,
index,
},
onMoveItem: handleMoveItem,
onDropItem: handleDropItem,
onGrabItem: handleGrabItem,
onCancel: handleCancel,
dropSensitivity: DROP_SENSITIVITY.REGULAR,
}
);
const composedRefs = useComposedRefs<HTMLDivElement>(relationRef, dragRef);
React.useEffect(() => {
dragPreviewRef(getEmptyImage());
}, [dragPreviewRef]);
return (
<Box
style={style}
tag="li"
ref={dropRef}
aria-describedby={ariaDescribedBy}
cursor={canDrag ? 'all-scroll' : 'default'}
>
{isDragging ? (
<RelationItemPlaceholder />
) : (
<Flex
paddingTop={2}
paddingBottom={2}
paddingLeft={canDrag ? 2 : 4}
paddingRight={4}
hasRadius
borderColor="neutral200"
background={disabled ? 'neutral150' : 'neutral0'}
justifyContent="space-between"
ref={composedRefs}
data-handler-id={handlerId}
>
<FlexWrapper gap={1}>
{canDrag ? (
<IconButton
tag="div"
role="button"
tabIndex={0}
withTooltip={false}
label={formatMessage({
id: getTranslation('components.RelationInput.icon-button-aria-label'),
defaultMessage: 'Drag',
})}
variant="ghost"
onKeyDown={handleKeyDown}
disabled={disabled}
>
<Drag />
</IconButton>
) : null}
<Flex width="100%" minWidth={0} justifyContent="space-between">
<Box minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
<Tooltip description={label}>
{/* eslint-disable-next-line no-console */}
<CustomTextButton onClick={() => console.log('OPEN MODAL')}>
{label}
</CustomTextButton>
</Tooltip>
</Box>
{status ? <DocumentStatus status={status} /> : null}
</Flex>
</FlexWrapper>
<Box paddingLeft={4}>
<IconButton
onClick={() => handleDisconnect(relations[index])}
disabled={disabled}
label={formatMessage({
id: getTranslation('relation.disconnect'),
defaultMessage: 'Remove',
})}
variant="ghost"
size="S"
>
<Cross />
</IconButton>
</Box>
</Flex>
)}
</Box>
);
};
const ListItem = ({ data, index, style }: ListItemProps) => {
const {
ariaDescribedBy,
@ -1661,13 +1091,6 @@ const RelationItemPlaceholder = () => (
);
const MemoizedRelationsField = React.memo(RelationsField);
const MemoizedUnstableRelationsField = React.memo(UnstableRelationsField);
export {
MemoizedRelationsField as RelationsInput,
MemoizedUnstableRelationsField as UnstableRelationsInput,
FlexWrapper,
DisconnectButton,
LinkEllipsis,
};
export { MemoizedRelationsField as RelationsInput, FlexWrapper, DisconnectButton, LinkEllipsis };
export type { RelationsFieldProps };

View File

@ -18,7 +18,7 @@ import { BlocksInput } from './FormInputs/BlocksInput/BlocksInput';
import { ComponentInput } from './FormInputs/Component/Input';
import { DynamicZone, useDynamicZone } from './FormInputs/DynamicZone/Field';
import { NotAllowedInput } from './FormInputs/NotAllowed';
import { RelationsInput, UnstableRelationsInput } from './FormInputs/Relations';
import { RelationsInput } from './FormInputs/Relations';
import { UIDInput } from './FormInputs/UID';
import { Wysiwyg } from './FormInputs/Wysiwyg/Field';
@ -142,9 +142,6 @@ const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererP
case 'dynamiczone':
return <DynamicZone {...props} hint={hint} disabled={fieldIsDisabled} />;
case 'relation':
if (window.strapi.future.isEnabled('unstableRelationsOnTheFly')) {
return <UnstableRelationsInput {...props} hint={hint} disabled={fieldIsDisabled} />;
}
return <RelationsInput {...props} hint={hint} disabled={fieldIsDisabled} />;
case 'richtext':
return <Wysiwyg {...props} hint={hint} type={props.type} disabled={fieldIsDisabled} />;

View File

@ -1,7 +1,6 @@
export interface FeaturesConfig {
future?: {
unstablePreviewSideEditor?: boolean;
unstableRelationsOnTheFly?: boolean;
};
}

View File

@ -1,6 +1,5 @@
module.exports = ({ env }) => ({
future: {
unstablePreviewSideEditor: env.bool('STRAPI_FEATURES_UNSTABLE_PREVIEW_SIDE_EDITOR', false),
unstableRelationsOnTheFly: env.bool('STRAPI_FEATURES_UNSTABLE_RELATIONS_ON_THE_FLY', false),
},
});