Remove jotai from twenty-ui (#21937)
twenty-ui no longer depends on jotai, so its components work without a consumer-provided jotai store (better practice for a shared UI library). twenty-front keeps jotai; this is scoped to the library. - **Avatar**: tracks image-load failure in local `useState` instead of a global atom. - **Icons**: the icon registry moved from a jotai atom to a React Context. `IconsProvider` and `useIcons` keep identical signatures; the context itself stays internal. - Removed the unused `createState` helper, the `invalidAvatarUrlsAtomV2` / `iconsState` atoms, and `JotaiRootDecorator`; regenerated barrels and dropped the `jotai` dependency. No other package needs changes: nothing imports the removed symbols, and `twenty-sdk` (which re-exports twenty-ui via `export *`) simply stops surfacing the two leaked atoms on its next publish. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21937?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -76,7 +76,6 @@
|
||||
"date-fns": "^4.4.0",
|
||||
"framer-motion": "^11.18.0",
|
||||
"glob": "^11.1.0",
|
||||
"jotai": "^2.17.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-responsive": "^9.0.2",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { isNonEmptyString, isNull, isUndefined } from '@sniptt/guards';
|
||||
import { clsx } from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import { handleClickableElementKeyDown } from '@ui/accessibility/utils/handleClickableElementKeyDown';
|
||||
import { invalidAvatarUrlsAtomV2 } from '@ui/data-display/Avatar/states/invalidAvatarUrlsAtomV2';
|
||||
import { type AvatarSize } from '@ui/data-display/Avatar/types/AvatarSize';
|
||||
import { type AvatarType } from '@ui/data-display/Avatar/types/AvatarType';
|
||||
import { type IconComponent } from '@ui/icon/types/IconComponent';
|
||||
@@ -47,9 +45,9 @@ export const Avatar = ({
|
||||
}: AvatarProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const [invalidAvatarUrls, setInvalidAvatarUrls] = useAtom(
|
||||
invalidAvatarUrlsAtomV2,
|
||||
);
|
||||
const [erroredAvatarImageURI, setErroredAvatarImageURI] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const avatarImageURI = isNonEmptyString(avatarUrl)
|
||||
? getImageAbsoluteURI({
|
||||
@@ -64,11 +62,11 @@ export const Avatar = ({
|
||||
const placeholderChar = placeholderFirstChar?.toUpperCase() || '-';
|
||||
|
||||
const showPlaceholder =
|
||||
isNull(avatarImageURI) || invalidAvatarUrls.includes(avatarImageURI);
|
||||
isNull(avatarImageURI) || erroredAvatarImageURI === avatarImageURI;
|
||||
|
||||
const handleImageError = () => {
|
||||
if (isNonEmptyString(avatarImageURI)) {
|
||||
setInvalidAvatarUrls((prev) => [...prev, avatarImageURI]);
|
||||
setErroredAvatarImageURI(avatarImageURI);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import {
|
||||
AVATAR_URL_MOCK,
|
||||
ComponentDecorator,
|
||||
JotaiRootDecorator,
|
||||
} from '@ui/testing';
|
||||
import { AVATAR_URL_MOCK, ComponentDecorator } from '@ui/testing';
|
||||
|
||||
import { Avatar } from '@ui/data-display/Avatar/Avatar';
|
||||
|
||||
const meta: Meta<typeof Avatar> = {
|
||||
title: 'UI/Data Display/Avatar',
|
||||
component: Avatar,
|
||||
decorators: [ComponentDecorator, JotaiRootDecorator],
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
avatarUrl: AVATAR_URL_MOCK,
|
||||
size: 'md',
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const invalidAvatarUrlsAtomV2 = atom<string[]>([]);
|
||||
invalidAvatarUrlsAtomV2.debugLabel = 'invalidAvatarUrlsAtomV2';
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
AVATAR_URL_MOCK,
|
||||
CatalogDecorator,
|
||||
ComponentDecorator,
|
||||
JotaiRootDecorator,
|
||||
} from '@ui/testing';
|
||||
|
||||
import {
|
||||
@@ -43,7 +42,7 @@ export default meta;
|
||||
type Story = StoryObj<typeof AvatarGroup>;
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [ComponentDecorator, JotaiRootDecorator],
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: Story = {
|
||||
@@ -69,5 +68,5 @@ export const Catalog: Story = {
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator, JotaiRootDecorator],
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
|
||||
+2
-6
@@ -1,16 +1,12 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { IconBuildingSkyscraper, IconUser } from '@ui/icon';
|
||||
import {
|
||||
AVATAR_URL_MOCK,
|
||||
ComponentDecorator,
|
||||
JotaiRootDecorator,
|
||||
} from '@ui/testing';
|
||||
import { AVATAR_URL_MOCK, ComponentDecorator } from '@ui/testing';
|
||||
import { AvatarOrIcon } from '@ui/data-display/AvatarOrIcon/AvatarOrIcon';
|
||||
|
||||
const meta: Meta<typeof AvatarOrIcon> = {
|
||||
title: 'UI/Data Display/AvatarOrIcon',
|
||||
component: AvatarOrIcon,
|
||||
decorators: [ComponentDecorator, JotaiRootDecorator],
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -12,7 +12,6 @@ export { AnimatedCheckmark } from './AnimatedCheckmark/AnimatedCheckmark';
|
||||
export type { AvatarProps } from './Avatar/Avatar';
|
||||
export { Avatar } from './Avatar/Avatar';
|
||||
export { AVATAR_PROPERTIES_BY_SIZE } from './Avatar/constants/AvatarPropertiesBySize';
|
||||
export { invalidAvatarUrlsAtomV2 } from './Avatar/states/invalidAvatarUrlsAtomV2';
|
||||
export type { AvatarSize } from './Avatar/types/AvatarSize';
|
||||
export type { AvatarType } from './Avatar/types/AvatarType';
|
||||
export type { AvatarGroupProps } from './AvatarGroup/AvatarGroup';
|
||||
|
||||
@@ -2,7 +2,6 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { IconsProvider } from '@ui/icon/providers/IconsProvider';
|
||||
import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator';
|
||||
import { JotaiRootDecorator } from '@ui/testing/decorators/JotaiRootDecorator';
|
||||
|
||||
import { Icon } from '../Icon';
|
||||
|
||||
@@ -16,7 +15,6 @@ const meta: Meta<typeof Icon> = {
|
||||
<Story />
|
||||
</IconsProvider>
|
||||
),
|
||||
JotaiRootDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
+8
-6
@@ -1,11 +1,12 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
Icon123,
|
||||
IconBuildingSkyscraper,
|
||||
IconUser,
|
||||
} from '@ui/icon/components/TablerIcons';
|
||||
import { IconsContext } from '@ui/icon/internal/IconsContext';
|
||||
import { useIcons } from '@ui/icon/hooks/useIcons';
|
||||
|
||||
const mockedStateIcons = {
|
||||
@@ -14,14 +15,15 @@ const mockedStateIcons = {
|
||||
IconBuildingSkyscraper,
|
||||
};
|
||||
|
||||
jest.mock('jotai', () => ({
|
||||
...jest.requireActual('jotai'),
|
||||
useAtomValue: () => mockedStateIcons,
|
||||
}));
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<IconsContext.Provider value={mockedStateIcons}>
|
||||
{children}
|
||||
</IconsContext.Provider>
|
||||
);
|
||||
|
||||
describe('useIcons', () => {
|
||||
const { result } = renderHook(() => useIcons(), {
|
||||
wrapper: Provider,
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
it('returns default icon when no icon key is provided', () => {
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { Icon123 } from '@ui/icon/components/TablerIcons';
|
||||
import { iconsState } from '@ui/icon/states/iconsState';
|
||||
import { IconsContext } from '@ui/icon/internal/IconsContext';
|
||||
|
||||
export const useIcons = () => {
|
||||
const icons = useAtomValue(iconsState);
|
||||
const icons = useContext(IconsContext);
|
||||
const defaultIcon = Icon123;
|
||||
|
||||
const getIcons = () => {
|
||||
|
||||
@@ -489,5 +489,4 @@ export {
|
||||
export { ThinkingOrbitLoaderIcon } from './components/ThinkingOrbitLoaderIcon';
|
||||
export { useIcons } from './hooks/useIcons';
|
||||
export { IconsProvider } from './providers/IconsProvider';
|
||||
export { iconsState } from './states/iconsState';
|
||||
export type { IconComponentProps, IconComponent } from './types/IconComponent';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { type IconComponent } from '@ui/icon/types/IconComponent';
|
||||
|
||||
export const IconsContext = createContext<Record<string, IconComponent>>({});
|
||||
@@ -1,20 +1,22 @@
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type JSX, useEffect } from 'react';
|
||||
import { type JSX, useEffect, useState } from 'react';
|
||||
|
||||
import { iconsState } from '@ui/icon/states/iconsState';
|
||||
import { IconsContext } from '@ui/icon/internal/IconsContext';
|
||||
import { type IconComponent } from '@ui/icon/types/IconComponent';
|
||||
|
||||
type IconsProviderProps = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export const IconsProvider = ({ children }: IconsProviderProps) => {
|
||||
const setIcons = useSetAtom(iconsState);
|
||||
const [icons, setIcons] = useState<Record<string, IconComponent>>({});
|
||||
|
||||
useEffect(() => {
|
||||
import('./internal/AllIcons').then(({ ALL_ICONS }) => {
|
||||
setIcons(ALL_ICONS);
|
||||
});
|
||||
}, [setIcons]);
|
||||
}, []);
|
||||
|
||||
return children;
|
||||
return (
|
||||
<IconsContext.Provider value={icons}>{children}</IconsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { type IconComponent } from '@ui/icon/types/IconComponent';
|
||||
|
||||
export const iconsState = atom<Record<string, IconComponent>>({});
|
||||
iconsState.debugLabel = 'iconsState';
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
CatalogDecorator,
|
||||
type CatalogStory,
|
||||
ComponentWithRouterDecorator,
|
||||
JotaiRootDecorator,
|
||||
} from '@ui/testing';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
@@ -28,7 +27,7 @@ const TabContainer = ({ children }: { children?: ReactNode }) => {
|
||||
const meta: Meta<typeof TabButton> = {
|
||||
title: 'UI/Input/Button/TabButton',
|
||||
component: TabButton,
|
||||
decorators: [ComponentWithRouterDecorator, JotaiRootDecorator],
|
||||
decorators: [ComponentWithRouterDecorator],
|
||||
args: {
|
||||
id: 'tab-button',
|
||||
title: 'Tab Title',
|
||||
|
||||
+2
-3
@@ -9,7 +9,6 @@ import {
|
||||
type CatalogOptions,
|
||||
type CatalogStory,
|
||||
ComponentDecorator,
|
||||
JotaiRootDecorator,
|
||||
} from '@ui/testing';
|
||||
import { MenuItemMultiSelectAvatar } from '@ui/navigation/MenuItemMultiSelectAvatar/MenuItemMultiSelectAvatar';
|
||||
|
||||
@@ -29,7 +28,7 @@ export const Default: Story = {
|
||||
contextualText: 'Contextual text',
|
||||
avatar: <Avatar avatarUrl={AVATAR_URL_MOCK} placeholder="L" />,
|
||||
},
|
||||
decorators: [ComponentDecorator, JotaiRootDecorator],
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof MenuItemMultiSelectAvatar> = {
|
||||
@@ -84,5 +83,5 @@ export const Catalog: CatalogStory<Story, typeof MenuItemMultiSelectAvatar> = {
|
||||
} as CatalogOptions,
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator, JotaiRootDecorator],
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
|
||||
-3
@@ -9,7 +9,6 @@ import {
|
||||
type CatalogOptions,
|
||||
type CatalogStory,
|
||||
ComponentDecorator,
|
||||
JotaiRootDecorator,
|
||||
} from '@ui/testing';
|
||||
import { MenuItemSelectAvatar } from '@ui/navigation/MenuItemSelectAvatar/MenuItemSelectAvatar';
|
||||
|
||||
@@ -38,7 +37,6 @@ export const Default: Story = {
|
||||
</div>
|
||||
),
|
||||
ComponentDecorator,
|
||||
JotaiRootDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -107,6 +105,5 @@ export const Catalog: CatalogStory<Story, typeof MenuItemSelectAvatar> = {
|
||||
</div>
|
||||
),
|
||||
CatalogDecorator,
|
||||
JotaiRootDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { type Decorator } from '@storybook/react-vite';
|
||||
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
|
||||
export const JotaiRootDecorator: Decorator = (Story) => (
|
||||
<JotaiProvider>
|
||||
<Story />
|
||||
</JotaiProvider>
|
||||
);
|
||||
@@ -21,7 +21,6 @@ export {
|
||||
computeLocation,
|
||||
ComponentWithRouterDecorator,
|
||||
} from './decorators/ComponentWithRouterDecorator';
|
||||
export { JotaiRootDecorator } from './decorators/JotaiRootDecorator';
|
||||
export { RouterDecorator } from './decorators/RouterDecorator';
|
||||
export { AVATAR_URL_MOCK } from './mocks/avatarUrlMock';
|
||||
export type { CatalogStory } from './types/CatalogStory';
|
||||
|
||||
@@ -24,7 +24,6 @@ export { isNavigationModifierPressed } from './navigation/isNavigationModifierPr
|
||||
export type { TriggerEventType } from './navigation/types/trigger-event.type';
|
||||
export { useIsMobile } from './responsive/hooks/useIsMobile';
|
||||
export { useScreenSize } from './screen-size/hooks/useScreenSize';
|
||||
export { createState } from './state/utils/createState';
|
||||
export type { ClickOutsideAttributes } from './types/ClickOutsideAttributes';
|
||||
export type { Nullable } from './types/Nullable';
|
||||
export { getDisplayValueByUrlType } from './utils/getDisplayValueByUrlType';
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { atom, type PrimitiveAtom } from 'jotai';
|
||||
|
||||
export const createState = <ValueType>({
|
||||
key,
|
||||
defaultValue,
|
||||
}: {
|
||||
key: string;
|
||||
defaultValue: ValueType;
|
||||
}): PrimitiveAtom<ValueType> => {
|
||||
const jotaiAtom = atom<ValueType>(defaultValue);
|
||||
jotaiAtom.debugLabel = key;
|
||||
return jotaiAtom;
|
||||
};
|
||||
Reference in New Issue
Block a user