Files
twenty/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts
T
42269cc45b fix(links): preserve percent-encoded URLs during normalization (#18792)
## Summary

This preserves percent-encoded payloads when normalizing links fields.

`lowercaseUrlOriginAndRemoveTrailingSlash` was decoding the path and
query string while lowercasing the URL origin. That changes URLs where
encoded payloads are semantically significant, such as Google Maps links
containing `%2F` segments.

Closes #18698.

## Changes

- stop decoding the path/query payload in
`lowercaseUrlOriginAndRemoveTrailingSlash`
- preserve the raw path, query, and hash while still lowercasing the
origin and trimming a trailing slash
- update shared URL normalization tests to assert encoded payloads stay
encoded
- add a server-side regression test covering imported links field
normalization

## Validation

- `corepack yarn jest --config packages/twenty-shared/jest.config.mjs
packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts
--runInBand`
- `corepack yarn jest --config packages/twenty-server/jest.config.mjs
packages/twenty-server/src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts
--runInBand`
- `corepack yarn nx test twenty-server --runInBand
--testFile=src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts`

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-20 16:56:12 +00:00

69 lines
2.2 KiB
TypeScript

import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddLinkToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetAtomState(
navigationMenuItemsDraftState,
);
const addLinkToDraft = (
label: string,
url: string,
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
): string => {
const normalizedUrl = normalizeUrl(url);
const folderId = targetFolderId ?? null;
const itemsInFolder = currentDraft.filter(
(item) =>
(item.folderId ?? null) === folderId &&
!isDefined(item.userWorkspaceId),
);
const index = targetIndex ?? itemsInFolder.length;
const { flatIndex, position } = computeInsertIndexAndPosition(
currentDraft,
folderId,
index,
);
const newItemId = v4();
const newItem: NavigationMenuItem = {
__typename: 'NavigationMenuItem',
id: newItemId,
type: NavigationMenuItemType.LINK,
viewId: undefined,
targetObjectMetadataId: undefined,
targetRecordId: undefined,
folderId: folderId ?? undefined,
position,
userWorkspaceId: undefined,
name: label.trim() || 'Link',
link: normalizedUrl,
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK,
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const newDraft = [
...currentDraft.slice(0, flatIndex),
newItem,
...currentDraft.slice(flatIndex),
];
setNavigationMenuItemsDraft(newDraft);
return newItemId;
};
return { addLinkToDraft };
};