diff --git a/docs/docs/contributor/frontend/basics/basics.mdx b/docs/docs/contributor/frontend/basics/basics.mdx new file mode 100644 index 0000000000000..9e837f0659110 --- /dev/null +++ b/docs/docs/contributor/frontend/basics/basics.mdx @@ -0,0 +1,56 @@ +--- +title: Basics +# description: Overview +sidebar_position: 0 +sidebar_custom_props: + icon: TbEyeglass +--- + +import DocCardList from '@theme/DocCardList'; + + + +## Tech Stack + +We took care of having a clean and simple stack, with minimal boilerplate code. + +**App** + +- [React](https://react.dev/) +- [Apollo](https://www.apollographql.com/docs/) +- [GraphQL Codegen](https://the-guild.dev/graphql/codegen) +- [Recoil](https://recoiljs.org/docs/introduction/core-concepts) +- [TypeScript](https://www.typescriptlang.org/) + +**Testing** + +- [Jest](https://jestjs.io/) +- [Storybook](https://storybook.js.org/) + +**Tooling** + +- [Yarn](https://yarnpkg.com/) +- [Craco](https://craco.js.org/docs/) +- [ESLint](https://eslint.org/) + +## Architecture + +### Routing + +We use [React Router](https://reactrouter.com/) for routing. + +To avoid unnecessary [re-renders](/contributor/frontend/advanced/best-practices#managing-re-renders) we handle all the routing logic in a `useEffect` in `PageChangeEffect`. + +### State Management + +We use [Recoil](https://recoiljs.org/docs/introduction/core-concepts) for state management. + +See our [best practices](/contributor/frontend/advanced/best-practices#state-management) for more managing state. + +## Testing + +We use [Jest](https://jestjs.io/) for unit testing and [Storybook](https://storybook.js.org/) for component testing. + +Jest is mainly used for testing utility functions, and not components themselves. + +Storybook is used for testing the behavior of isolated components, as well as displaying our [design system](/contributor/frontend/basics/design-system). diff --git a/docs/docs/contributor/frontend/basics/contributing.mdx b/docs/docs/contributor/frontend/basics/contributing.mdx index 25096fd064161..14a4e29c47f61 100644 --- a/docs/docs/contributor/frontend/basics/contributing.mdx +++ b/docs/docs/contributor/frontend/basics/contributing.mdx @@ -4,6 +4,9 @@ sidebar_position: 1 sidebar_custom_props: icon: TbTopologyStar --- +import DocCardList from '@theme/DocCardList'; + + ## Pre-requesites diff --git a/docs/docs/contributor/frontend/basics/design-system.mdx b/docs/docs/contributor/frontend/basics/design-system.mdx index 3cde9d40de6d5..dd6bd1a1d285a 100644 --- a/docs/docs/contributor/frontend/basics/design-system.mdx +++ b/docs/docs/contributor/frontend/basics/design-system.mdx @@ -1,9 +1,14 @@ --- +title: Design System sidebar_position: 7 sidebar_custom_props: icon: TbPaint --- +import DocCardList from '@theme/DocCardList'; + + + # Design System We rely on our internal and custom design system, that is built on top of styled-components. diff --git a/docs/docs/contributor/frontend/basics/folder-architecture.mdx b/docs/docs/contributor/frontend/basics/folder-architecture.mdx index c5c080cbfec16..07043a3c82f18 100644 --- a/docs/docs/contributor/frontend/basics/folder-architecture.mdx +++ b/docs/docs/contributor/frontend/basics/folder-architecture.mdx @@ -1,9 +1,14 @@ --- +title: Folder Architecture sidebar_position: 5 sidebar_custom_props: icon: TbFolder --- +import DocCardList from '@theme/DocCardList'; + + + # Folder Architecture In this guide, you will explore the details of the project directory structure and how it contributes to the organization and maintainability of Twenty. diff --git a/docs/docs/contributor/frontend/basics/overview.mdx b/docs/docs/contributor/frontend/basics/overview.mdx deleted file mode 100644 index 73457206c990b..0000000000000 --- a/docs/docs/contributor/frontend/basics/overview.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Overview -description: Overview -sidebar_position: 0 -sidebar_custom_props: - icon: TbEyeglass ---- - -## Tech Stack - -We took care of having a clean and simple stack, with minimal boilerplate code. - -**App** - -- [React](https://react.dev/) -- [Apollo](https://www.apollographql.com/docs/) -- [GraphQL Codegen](https://the-guild.dev/graphql/codegen) -- [Recoil](https://recoiljs.org/docs/introduction/core-concepts) -- [TypeScript](https://www.typescriptlang.org/) - -**Testing** - -- [Jest](https://jestjs.io/) -- [Storybook](https://storybook.js.org/) - -**Tooling** - -- [Yarn](https://yarnpkg.com/) -- [Craco](https://craco.js.org/docs/) -- [ESLint](https://eslint.org/) - -## Architecture - -### Routing - -We use [React Router](https://reactrouter.com/) for routing. - -To avoid unnecessary [re-renders](/contributor/frontend/advanced/best-practices#managing-re-renders) we handle all the routing logic in a `useEffect` in `PageChangeEffect`. - -### State Management - -We use [Recoil](https://recoiljs.org/docs/introduction/core-concepts) for state management. - -See our [best practices](/contributor/frontend/advanced/best-practices#state-management) for more managing state. - -## Testing - -We use [Jest](https://jestjs.io/) for unit testing and [Storybook](https://storybook.js.org/) for component testing. - -Jest is mainly used for testing utility functions, and not components themselves. - -Storybook is used for testing the behavior of isolated components, as well as displaying our [design system](/contributor/frontend/basics/design-system). diff --git a/docs/docs/contributor/frontend/basics/work-with-figma.mdx b/docs/docs/contributor/frontend/basics/work-with-figma.mdx index 1111e85d043c0..a42c9bc39764b 100644 --- a/docs/docs/contributor/frontend/basics/work-with-figma.mdx +++ b/docs/docs/contributor/frontend/basics/work-with-figma.mdx @@ -5,6 +5,10 @@ sidebar_custom_props: icon: TbBrandFigma --- +import DocCardList from '@theme/DocCardList'; + + + Figma is a collaborative interface design tool that aids in bridging the communication barrier between designers and developers. In this guide, we'll go over how to collaborate with Twenty’s Figma. diff --git a/docs/docs/contributor/frontend/frontend.mdx b/docs/docs/contributor/frontend/frontend.mdx index 5b684c1811cf7..7087b9804ed6a 100644 --- a/docs/docs/contributor/frontend/frontend.mdx +++ b/docs/docs/contributor/frontend/frontend.mdx @@ -7,6 +7,7 @@ sidebar_custom_props: icon: TbTerminal2 isSidebarRoot: true --- + Welcome to the Frontend Development section of the documentation. Here you will find information about the frontend development process, the tools we use, and the best practices we follow. diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 2c637ae13c73a..7b1761d6b747f 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -234,4 +234,8 @@ a.table-of-contents__link:hover{ .tabs-container { padding: 20px; +} + +.card{ + text-decoration: none; } \ No newline at end of file diff --git a/front/src/App.tsx b/front/src/App.tsx index 9819a65ec34cc..aa0d9d9516ea5 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -2,7 +2,7 @@ import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; -import { DefaultLayout } from '@/ui/layout/components/DefaultLayout'; +import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; diff --git a/front/src/AppNavbar.tsx b/front/src/AppNavbar.tsx index 2f953c4b43b64..8bf15a7034fd6 100644 --- a/front/src/AppNavbar.tsx +++ b/front/src/AppNavbar.tsx @@ -12,11 +12,11 @@ import { IconSettings, IconTargetArrow, IconUser, -} from '@/ui/icon/index'; +} from '@/ui/display/icon/index'; import { useIsSubMenuNavbarDisplayed } from '@/ui/layout/hooks/useIsSubMenuNavbarDisplayed'; -import MainNavbar from '@/ui/navbar/components/MainNavbar'; -import NavItem from '@/ui/navbar/components/NavItem'; -import NavTitle from '@/ui/navbar/components/NavTitle'; +import MainNavbar from '@/ui/navigation/navbar/components/MainNavbar'; +import NavItem from '@/ui/navigation/navbar/components/NavItem'; +import NavTitle from '@/ui/navigation/navbar/components/NavTitle'; import { measureTotalFrameLoad } from './utils/measureTotalFrameLoad'; diff --git a/front/src/effect-components/PageChangeEffect.tsx b/front/src/effect-components/PageChangeEffect.tsx index dcbbd2b7eeb30..fb50693c35d13 100644 --- a/front/src/effect-components/PageChangeEffect.tsx +++ b/front/src/effect-components/PageChangeEffect.tsx @@ -12,9 +12,9 @@ import { AppBasePath } from '@/types/AppBasePath'; import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { SettingsPath } from '@/types/SettingsPath'; -import { TableHotkeyScope } from '@/ui/data-table/types/TableHotkeyScope'; -import { IconCheckbox, IconNotes } from '@/ui/icon'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { TableHotkeyScope } from '@/ui/data/data-table/types/TableHotkeyScope'; +import { IconCheckbox, IconNotes } from '@/ui/display/icon'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { ActivityType, diff --git a/front/src/index.tsx b/front/src/index.tsx index 003c0ca88a62f..9b9fa92477d73 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -7,8 +7,8 @@ import { RecoilRoot } from 'recoil'; import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; -import { DialogProvider } from '@/ui/dialog/components/DialogProvider'; -import { SnackBarProvider } from '@/ui/snack-bar/components/SnackBarProvider'; +import { DialogProvider } from '@/ui/feedback/dialog/components/DialogProvider'; +import { SnackBarProvider } from '@/ui/feedback/snack-bar/components/SnackBarProvider'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { ThemeType } from '@/ui/theme/constants/theme'; import { UserProvider } from '@/users/components/UserProvider'; diff --git a/front/src/modules/activities/components/ActivityBodyEditor.tsx b/front/src/modules/activities/components/ActivityBodyEditor.tsx index cf90d557fae18..26018b4df8411 100644 --- a/front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -5,7 +5,7 @@ import { useBlockNote } from '@blocknote/react'; import styled from '@emotion/styled'; import debounce from 'lodash.debounce'; -import { BlockEditor } from '@/ui/editor/components/BlockEditor'; +import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { Activity, useUpdateActivityMutation } from '~/generated/graphql'; import { ACTIVITY_UPDATE_FRAGMENT } from '../graphql/fragments/activityUpdateFragment'; diff --git a/front/src/modules/activities/components/ActivityCreateButton.tsx b/front/src/modules/activities/components/ActivityCreateButton.tsx index ed519b8d078e2..8dd8dfcc1feda 100644 --- a/front/src/modules/activities/components/ActivityCreateButton.tsx +++ b/front/src/modules/activities/components/ActivityCreateButton.tsx @@ -1,6 +1,10 @@ -import { Button } from '@/ui/button/components/Button'; -import { ButtonGroup } from '@/ui/button/components/ButtonGroup'; -import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon/index'; +import { + IconCheckbox, + IconNotes, + IconTimelineEvent, +} from '@/ui/display/icon/index'; +import { Button } from '@/ui/input/button/components/Button'; +import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; type ActivityCreateButtonProps = { onNoteClick?: () => void; diff --git a/front/src/modules/activities/components/ActivityEditor.tsx b/front/src/modules/activities/components/ActivityEditor.tsx index ecb7f0e5765fa..a07f757fbf41a 100644 --- a/front/src/modules/activities/components/ActivityEditor.tsx +++ b/front/src/modules/activities/components/ActivityEditor.tsx @@ -7,7 +7,7 @@ import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor'; import { ActivityComments } from '@/activities/components/ActivityComments'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities'; -import { PropertyBox } from '@/ui/inline-cell/property-box/components/PropertyBox'; +import { PropertyBox } from '@/ui/data/inline-cell/property-box/components/PropertyBox'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { diff --git a/front/src/modules/activities/components/ActivityTypeDropdown.tsx b/front/src/modules/activities/components/ActivityTypeDropdown.tsx index f27f2f3733f3d..0064270598e38 100644 --- a/front/src/modules/activities/components/ActivityTypeDropdown.tsx +++ b/front/src/modules/activities/components/ActivityTypeDropdown.tsx @@ -5,8 +5,8 @@ import { ChipAccent, ChipSize, ChipVariant, -} from '@/ui/chip/components/Chip'; -import { IconCheckbox, IconNotes } from '@/ui/icon'; +} from '@/ui/display/chip/components/Chip'; +import { IconCheckbox, IconNotes } from '@/ui/display/icon'; import { Activity, ActivityType } from '~/generated/graphql'; type ActivityTypeDropdownProps = { diff --git a/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx index c5f7d5836184b..2a43045b7e80f 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx @@ -1,9 +1,9 @@ -import { FieldContext } from '@/ui/field/contexts/FieldContext'; -import { FieldDefinition } from '@/ui/field/types/FieldDefinition'; -import { FieldRelationMetadata } from '@/ui/field/types/FieldMetadata'; -import { IconUserCircle } from '@/ui/icon'; -import { InlineCell } from '@/ui/inline-cell/components/InlineCell'; -import { InlineCellHotkeyScope } from '@/ui/inline-cell/types/InlineCellHotkeyScope'; +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { FieldDefinition } from '@/ui/data/field/types/FieldDefinition'; +import { FieldRelationMetadata } from '@/ui/data/field/types/FieldMetadata'; +import { InlineCell } from '@/ui/data/inline-cell/components/InlineCell'; +import { InlineCellHotkeyScope } from '@/ui/data/inline-cell/types/InlineCellHotkeyScope'; +import { IconUserCircle } from '@/ui/display/icon'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; import { Company, User, useUpdateActivityMutation } from '~/generated/graphql'; diff --git a/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableFieldEditMode.tsx b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableFieldEditMode.tsx index 6d6c39fb4fdcd..19912aabeeac5 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableFieldEditMode.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableFieldEditMode.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { ActivityAssigneePicker } from '@/activities/components/ActivityAssigneePicker'; -import { useInlineCell } from '@/ui/inline-cell/hooks/useInlineCell'; +import { useInlineCell } from '@/ui/data/inline-cell/hooks/useInlineCell'; import { Activity, User } from '~/generated/graphql'; const StyledContainer = styled.div` diff --git a/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx b/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx index 9b5cd6500145a..472fb72d0211c 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx @@ -1,9 +1,9 @@ -import { FieldContext } from '@/ui/field/contexts/FieldContext'; -import { FieldDefinition } from '@/ui/field/types/FieldDefinition'; -import { FieldDateMetadata } from '@/ui/field/types/FieldMetadata'; -import { IconCalendar } from '@/ui/icon/index'; -import { InlineCell } from '@/ui/inline-cell/components/InlineCell'; -import { InlineCellHotkeyScope } from '@/ui/inline-cell/types/InlineCellHotkeyScope'; +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { FieldDefinition } from '@/ui/data/field/types/FieldDefinition'; +import { FieldDateMetadata } from '@/ui/data/field/types/FieldMetadata'; +import { InlineCell } from '@/ui/data/inline-cell/components/InlineCell'; +import { InlineCellHotkeyScope } from '@/ui/data/inline-cell/types/InlineCellHotkeyScope'; +import { IconCalendar } from '@/ui/display/icon/index'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { useUpdateActivityMutation } from '~/generated/graphql'; diff --git a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx index 6db6c74a0136b..818f8b102ea6f 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx @@ -1,7 +1,7 @@ import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; -import { IconArrowUpRight, IconPencil } from '@/ui/icon'; -import { InlineCellContainer } from '@/ui/inline-cell/components/InlineCellContainer'; -import { FieldRecoilScopeContext } from '@/ui/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext'; +import { InlineCellContainer } from '@/ui/data/inline-cell/components/InlineCellContainer'; +import { FieldRecoilScopeContext } from '@/ui/data/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext'; +import { IconArrowUpRight, IconPencil } from '@/ui/display/icon'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { Activity, ActivityTarget, Company, Person } from '~/generated/graphql'; diff --git a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx index 80c36546f195e..e17cd9bfe46c6 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx @@ -5,7 +5,7 @@ import { useHandleCheckableActivityTargetChange } from '@/activities/hooks/useHa import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName'; import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery'; import { useFilteredSearchPeopleQuery } from '@/people/hooks/useFilteredSearchPeopleQuery'; -import { useInlineCell } from '@/ui/inline-cell/hooks/useInlineCell'; +import { useInlineCell } from '@/ui/data/inline-cell/hooks/useInlineCell'; import { MultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect'; import { Activity, ActivityTarget } from '~/generated/graphql'; import { assertNotNull } from '~/utils/assert'; diff --git a/front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts b/front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts index ee410ad270888..75c30eb9b1d74 100644 --- a/front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts +++ b/front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts @@ -1,8 +1,8 @@ import { useRecoilState } from 'recoil'; -import { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer'; -import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope'; -import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { viewableActivityIdState } from '../states/viewableActivityIdState'; diff --git a/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 50723a95fd6c1..dd831f0c44384 100644 --- a/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -5,9 +5,9 @@ import { v4 } from 'uuid'; import { currentUserState } from '@/auth/states/currentUserState'; import { GET_COMPANIES } from '@/companies/graphql/queries/getCompanies'; import { GET_PEOPLE } from '@/people/graphql/queries/getPeople'; -import { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer'; -import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope'; -import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { ActivityType, useCreateActivityMutation } from '~/generated/graphql'; diff --git a/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts b/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts index b94aa6477e48e..e7e7cb393bc15 100644 --- a/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts +++ b/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts @@ -1,6 +1,6 @@ import { useRecoilValue } from 'recoil'; -import { selectedRowIdsSelector } from '@/ui/data-table/states/selectors/selectedRowIdsSelector'; +import { selectedRowIdsSelector } from '@/ui/data/data-table/states/selectors/selectedRowIdsSelector'; import { ActivityType } from '~/generated/graphql'; import { diff --git a/front/src/modules/activities/notes/components/Notes.tsx b/front/src/modules/activities/notes/components/Notes.tsx index d22c6e9c9ebab..3d91cb670350c 100644 --- a/front/src/modules/activities/notes/components/Notes.tsx +++ b/front/src/modules/activities/notes/components/Notes.tsx @@ -4,8 +4,8 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct import { NoteList } from '@/activities/notes/components/NoteList'; import { useNotes } from '@/activities/notes/hooks/useNotes'; import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity'; -import { Button } from '@/ui/button/components/Button'; -import { IconNotes } from '@/ui/icon'; +import { IconNotes } from '@/ui/display/icon'; +import { Button } from '@/ui/input/button/components/Button'; import { ActivityType } from '~/generated/graphql'; const StyledTaskGroupEmptyContainer = styled.div` diff --git a/front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx b/front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx index e3d62e2221655..6c7ad192fe47a 100644 --- a/front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx +++ b/front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx @@ -5,9 +5,9 @@ import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities'; import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/graphql/queries/getActivitiesByTarget'; import { GET_COMPANIES } from '@/companies/graphql/queries/getCompanies'; import { GET_PEOPLE } from '@/people/graphql/queries/getPeople'; -import { LightIconButton } from '@/ui/button/components/LightIconButton'; -import { IconTrash } from '@/ui/icon'; -import { isRightDrawerOpenState } from '@/ui/right-drawer/states/isRightDrawerOpenState'; +import { IconTrash } from '@/ui/display/icon'; +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; import { useDeleteActivityMutation } from '~/generated/graphql'; type ActivityActionBarProps = { diff --git a/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx b/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx index 2abeeb7ab5341..1902fe4f9f224 100644 --- a/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx +++ b/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; import { ActivityEditor } from '@/activities/components/ActivityEditor'; -import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState'; +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; import { useGetActivityQuery } from '~/generated/graphql'; import '@blocknote/core/style.css'; diff --git a/front/src/modules/activities/table/components/CommentChip.tsx b/front/src/modules/activities/table/components/CommentChip.tsx index d3c6fd776beca..2c64c650d810f 100644 --- a/front/src/modules/activities/table/components/CommentChip.tsx +++ b/front/src/modules/activities/table/components/CommentChip.tsx @@ -1,7 +1,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComment } from '@/ui/icon'; +import { IconComment } from '@/ui/display/icon'; export type CommentChipProps = { count: number; diff --git a/front/src/modules/activities/tasks/components/AddTaskButton.tsx b/front/src/modules/activities/tasks/components/AddTaskButton.tsx index e5e07989b2698..0e8f931dced34 100644 --- a/front/src/modules/activities/tasks/components/AddTaskButton.tsx +++ b/front/src/modules/activities/tasks/components/AddTaskButton.tsx @@ -1,7 +1,7 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity'; -import { Button } from '@/ui/button/components/Button'; -import { IconPlus } from '@/ui/icon'; +import { IconPlus } from '@/ui/display/icon'; +import { Button } from '@/ui/input/button/components/Button'; import { ActivityType } from '~/generated/graphql'; export const AddTaskButton = ({ diff --git a/front/src/modules/activities/tasks/components/PageAddTaskButton.tsx b/front/src/modules/activities/tasks/components/PageAddTaskButton.tsx index 1a03497915166..fdfe3e5cfa149 100644 --- a/front/src/modules/activities/tasks/components/PageAddTaskButton.tsx +++ b/front/src/modules/activities/tasks/components/PageAddTaskButton.tsx @@ -1,8 +1,8 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext'; -import { PageAddButton } from '@/ui/layout/components/PageAddButton'; +import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState'; +import { PageAddButton } from '@/ui/layout/page/PageAddButton'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; -import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; import { ActivityType } from '~/generated/graphql'; export const PageAddTaskButton = () => { diff --git a/front/src/modules/activities/tasks/components/TaskGroups.tsx b/front/src/modules/activities/tasks/components/TaskGroups.tsx index f19cf18f9e812..b42aad8851d6f 100644 --- a/front/src/modules/activities/tasks/components/TaskGroups.tsx +++ b/front/src/modules/activities/tasks/components/TaskGroups.tsx @@ -4,9 +4,9 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext'; import { useTasks } from '@/activities/tasks/hooks/useTasks'; import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity'; -import { Button } from '@/ui/button/components/Button'; -import { IconCheckbox } from '@/ui/icon'; -import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState'; +import { IconCheckbox } from '@/ui/display/icon'; +import { Button } from '@/ui/input/button/components/Button'; +import { activeTabIdScopedState } from '@/ui/layout/tab/states/activeTabIdScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { ActivityType } from '~/generated/graphql'; diff --git a/front/src/modules/activities/tasks/components/TaskRow.tsx b/front/src/modules/activities/tasks/components/TaskRow.tsx index b9991606d5df8..585980f3444e9 100644 --- a/front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/front/src/modules/activities/tasks/components/TaskRow.tsx @@ -3,9 +3,9 @@ import styled from '@emotion/styled'; import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; -import { IconCalendar, IconComment } from '@/ui/icon'; +import { IconCalendar, IconComment } from '@/ui/display/icon'; +import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip'; import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox'; -import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils'; import { TaskForList } from '../../types/TaskForList'; diff --git a/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts b/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts index a8ab8d83e467c..2f140d3f17e78 100644 --- a/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts +++ b/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts @@ -2,7 +2,7 @@ import { DateTime } from 'luxon'; import { useRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; -import { turnFilterIntoWhereClause } from '@/ui/view-bar/utils/turnFilterIntoWhereClause'; +import { turnFilterIntoWhereClause } from '@/ui/data/view-bar/utils/turnFilterIntoWhereClause'; import { ActivityType, useGetActivitiesQuery, diff --git a/front/src/modules/activities/tasks/hooks/useTasks.ts b/front/src/modules/activities/tasks/hooks/useTasks.ts index 04101fb448639..fbba10c1c1018 100644 --- a/front/src/modules/activities/tasks/hooks/useTasks.ts +++ b/front/src/modules/activities/tasks/hooks/useTasks.ts @@ -2,9 +2,9 @@ import { DateTime } from 'luxon'; import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext'; import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity'; +import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState'; +import { turnFilterIntoWhereClause } from '@/ui/data/view-bar/utils/turnFilterIntoWhereClause'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; -import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; -import { turnFilterIntoWhereClause } from '@/ui/view-bar/utils/turnFilterIntoWhereClause'; import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql'; import { parseDate } from '~/utils/date-utils'; diff --git a/front/src/modules/activities/timeline/components/TimelineActivity.tsx b/front/src/modules/activities/timeline/components/TimelineActivity.tsx index bea4ca5d21cc0..a436f2954e281 100644 --- a/front/src/modules/activities/timeline/components/TimelineActivity.tsx +++ b/front/src/modules/activities/timeline/components/TimelineActivity.tsx @@ -3,8 +3,8 @@ import styled from '@emotion/styled'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask'; -import { IconNotes } from '@/ui/icon'; -import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; +import { IconNotes } from '@/ui/display/icon'; +import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { Activity, User } from '~/generated/graphql'; import { diff --git a/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx b/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx index ceb96fd230f2d..5c4e698a5dfbb 100644 --- a/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx +++ b/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; +import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip'; import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox'; -import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; import { ActivityType } from '~/generated/graphql'; const StyledTitleContainer = styled.div` diff --git a/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx b/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx index 246b33f1946a1..ea3d5e54dae1c 100644 --- a/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx +++ b/front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx @@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer'; -import { IconCircleDot } from '@/ui/icon'; +import { IconCircleDot } from '@/ui/display/icon'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { TimelineActivity } from './TimelineActivity'; diff --git a/front/src/modules/auth/components/Modal.tsx b/front/src/modules/auth/components/Modal.tsx index 4c1d61884b788..db7d03d86ad70 100644 --- a/front/src/modules/auth/components/Modal.tsx +++ b/front/src/modules/auth/components/Modal.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from '@emotion/styled'; -import { Modal as UIModal } from '@/ui/modal/components/Modal'; +import { Modal as UIModal } from '@/ui/layout/modal/components/Modal'; const StyledContent = styled(UIModal.Content)` align-items: center; diff --git a/front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx index 827aba0dc8931..335d663c0fcb9 100644 --- a/front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx +++ b/front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx @@ -4,8 +4,8 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; -import { MainButton } from '@/ui/button/components/MainButton'; -import { IconBrandGoogle } from '@/ui/icon'; +import { IconBrandGoogle } from '@/ui/display/icon'; +import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInput } from '@/ui/input/components/TextInput'; import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; diff --git a/front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx b/front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx index 9c8dd8bae40f6..0d34f7eeaa96f 100644 --- a/front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx +++ b/front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx @@ -9,7 +9,7 @@ import { authProvidersState } from '@/client-config/states/authProvidersState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx index aa27fa5c53a4d..7996d1a48a0ff 100644 --- a/front/src/modules/command-menu/components/CommandMenu.tsx +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; -import { IconNotes } from '@/ui/icon'; +import { IconNotes } from '@/ui/display/icon'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { Avatar } from '@/users/components/Avatar'; diff --git a/front/src/modules/command-menu/components/CommandMenuItem.tsx b/front/src/modules/command-menu/components/CommandMenuItem.tsx index ad5c9baa2d436..53b1cf7fb2569 100644 --- a/front/src/modules/command-menu/components/CommandMenuItem.tsx +++ b/front/src/modules/command-menu/components/CommandMenuItem.tsx @@ -1,8 +1,8 @@ import { useNavigate } from 'react-router-dom'; -import { IconArrowUpRight } from '@/ui/icon'; -import { IconComponent } from '@/ui/icon/types/IconComponent'; -import { MenuItemCommand } from '@/ui/menu-item/components/MenuItemCommand'; +import { IconArrowUpRight } from '@/ui/display/icon'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { MenuItemCommand } from '@/ui/navigation/menu-item/components/MenuItemCommand'; import { useCommandMenu } from '../hooks/useCommandMenu'; diff --git a/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 521bcab3e9b9b..779b08fd07f76 100644 --- a/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -5,7 +5,7 @@ import { userEvent, within } from '@storybook/testing-library'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { CommandType } from '@/command-menu/types/Command'; -import { IconCheckbox, IconNotes } from '@/ui/icon'; +import { IconCheckbox, IconNotes } from '@/ui/display/icon'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { sleep } from '~/testing/sleep'; diff --git a/front/src/modules/command-menu/types/Command.ts b/front/src/modules/command-menu/types/Command.ts index 2d4448d36acc8..59ca09d0d1d98 100644 --- a/front/src/modules/command-menu/types/Command.ts +++ b/front/src/modules/command-menu/types/Command.ts @@ -1,4 +1,4 @@ -import { IconComponent } from '@/ui/icon/types/IconComponent'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; export enum CommandType { Navigate = 'Navigate', diff --git a/front/src/modules/companies/__stories__/CompanyBoardCard.stories.tsx b/front/src/modules/companies/__stories__/CompanyBoardCard.stories.tsx index 12ce3b94f985b..b7f9a626a0ad1 100644 --- a/front/src/modules/companies/__stories__/CompanyBoardCard.stories.tsx +++ b/front/src/modules/companies/__stories__/CompanyBoardCard.stories.tsx @@ -4,9 +4,9 @@ import { Meta, StoryObj } from '@storybook/react'; import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard'; import { pipelineAvailableFieldDefinitions } from '@/pipeline/constants/pipelineAvailableFieldDefinitions'; -import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext'; -import { boardCardFieldsScopedState } from '@/ui/board/states/boardCardFieldsScopedState'; -import { BoardColumnRecoilScopeContext } from '@/ui/board/states/recoil-scope-contexts/BoardColumnRecoilScopeContext'; +import { BoardCardIdContext } from '@/ui/layout/board/contexts/BoardCardIdContext'; +import { boardCardFieldsScopedState } from '@/ui/layout/board/states/boardCardFieldsScopedState'; +import { BoardColumnRecoilScopeContext } from '@/ui/layout/board/states/recoil-scope-contexts/BoardColumnRecoilScopeContext'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; diff --git a/front/src/modules/companies/board/components/CompanyBoard.tsx b/front/src/modules/companies/board/components/CompanyBoard.tsx index bfd515ddf81b5..717f308a2b354 100644 --- a/front/src/modules/companies/board/components/CompanyBoard.tsx +++ b/front/src/modules/companies/board/components/CompanyBoard.tsx @@ -1,12 +1,12 @@ import { BoardContext } from '@/companies/states/contexts/BoardContext'; import { pipelineAvailableFieldDefinitions } from '@/pipeline/constants/pipelineAvailableFieldDefinitions'; +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; import { EntityBoard, EntityBoardProps, -} from '@/ui/board/components/EntityBoard'; -import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar'; -import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu'; -import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; +} from '@/ui/layout/board/components/EntityBoard'; +import { EntityBoardActionBar } from '@/ui/layout/board/components/EntityBoardActionBar'; +import { EntityBoardContextMenu } from '@/ui/layout/board/components/EntityBoardContextMenu'; import { useBoardViews } from '@/views/hooks/useBoardViews'; import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions'; diff --git a/front/src/modules/companies/components/AddPersonToCompany.tsx b/front/src/modules/companies/components/AddPersonToCompany.tsx index 4158917fe8459..086468cfd15ee 100644 --- a/front/src/modules/companies/components/AddPersonToCompany.tsx +++ b/front/src/modules/companies/components/AddPersonToCompany.tsx @@ -9,10 +9,10 @@ import { PersonForSelect, } from '@/people/components/PeoplePicker'; import { GET_PEOPLE } from '@/people/graphql/queries/getPeople'; -import { LightIconButton } from '@/ui/button/components/LightIconButton'; -import { DoubleTextInput } from '@/ui/field/meta-types/input/components/internal/DoubleTextInput'; -import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText'; -import { IconPlus } from '@/ui/icon'; +import { DoubleTextInput } from '@/ui/data/field/meta-types/input/components/internal/DoubleTextInput'; +import { FieldDoubleText } from '@/ui/data/field/types/FieldDoubleText'; +import { IconPlus } from '@/ui/display/icon'; +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; diff --git a/front/src/modules/companies/components/CompanyBoardCard.tsx b/front/src/modules/companies/components/CompanyBoardCard.tsx index 413174e7a0237..1cad5cb722c8a 100644 --- a/front/src/modules/companies/components/CompanyBoardCard.tsx +++ b/front/src/modules/companies/components/CompanyBoardCard.tsx @@ -2,15 +2,15 @@ import { ReactNode, useContext } from 'react'; import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; -import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext'; -import { useBoardContext } from '@/ui/board/hooks/useBoardContext'; -import { useCurrentCardSelected } from '@/ui/board/hooks/useCurrentCardSelected'; -import { visibleBoardCardFieldsScopedSelector } from '@/ui/board/states/selectors/visibleBoardCardFieldsScopedSelector'; -import { EntityChipVariant } from '@/ui/chip/components/EntityChip'; -import { FieldContext } from '@/ui/field/contexts/FieldContext'; -import { InlineCell } from '@/ui/inline-cell/components/InlineCell'; -import { InlineCellHotkeyScope } from '@/ui/inline-cell/types/InlineCellHotkeyScope'; +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { InlineCell } from '@/ui/data/inline-cell/components/InlineCell'; +import { InlineCellHotkeyScope } from '@/ui/data/inline-cell/types/InlineCellHotkeyScope'; +import { EntityChipVariant } from '@/ui/display/chip/components/EntityChip'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; +import { BoardCardIdContext } from '@/ui/layout/board/contexts/BoardCardIdContext'; +import { useBoardContext } from '@/ui/layout/board/hooks/useBoardContext'; +import { useCurrentCardSelected } from '@/ui/layout/board/hooks/useCurrentCardSelected'; +import { visibleBoardCardFieldsScopedSelector } from '@/ui/layout/board/states/selectors/visibleBoardCardFieldsScopedSelector'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql'; import { getLogoUrlFromDomainName } from '~/utils'; diff --git a/front/src/modules/companies/components/CompanyChip.tsx b/front/src/modules/companies/components/CompanyChip.tsx index 39016efc67542..7beeb9b6f475b 100644 --- a/front/src/modules/companies/components/CompanyChip.tsx +++ b/front/src/modules/companies/components/CompanyChip.tsx @@ -1,4 +1,7 @@ -import { EntityChip, EntityChipVariant } from '@/ui/chip/components/EntityChip'; +import { + EntityChip, + EntityChipVariant, +} from '@/ui/display/chip/components/EntityChip'; type CompanyChipProps = { id: string; diff --git a/front/src/modules/companies/components/CompanyProgressPicker.tsx b/front/src/modules/companies/components/CompanyProgressPicker.tsx index 300e72211bc6f..d997cc5c2f8c5 100644 --- a/front/src/modules/companies/components/CompanyProgressPicker.tsx +++ b/front/src/modules/companies/components/CompanyProgressPicker.tsx @@ -2,16 +2,16 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; -import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; -import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput'; -import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; -import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; -import { IconChevronDown } from '@/ui/icon'; +import { IconChevronDown } from '@/ui/display/icon'; import { SingleEntitySelectBase } from '@/ui/input/relation-picker/components/SingleEntitySelectBase'; import { useEntitySelectSearch } from '@/ui/input/relation-picker/hooks/useEntitySelectSearch'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; -import { MenuItem } from '@/ui/menu-item/components/MenuItem'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { useFilteredSearchCompanyQuery } from '../hooks/useFilteredSearchCompanyQuery'; diff --git a/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx b/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx index 39f5e34c62cf1..87cdb8046d809 100644 --- a/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx +++ b/front/src/modules/companies/components/FilterDropdownCompanySearchSelect.tsx @@ -1,9 +1,9 @@ +import { FilterDropdownEntitySearchSelect } from '@/ui/data/view-bar/components/FilterDropdownEntitySearchSelect'; +import { useViewBarContext } from '@/ui/data/view-bar/hooks/useViewBarContext'; +import { filterDropdownSearchInputScopedState } from '@/ui/data/view-bar/states/filterDropdownSearchInputScopedState'; +import { filterDropdownSelectedEntityIdScopedState } from '@/ui/data/view-bar/states/filterDropdownSelectedEntityIdScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; -import { FilterDropdownEntitySearchSelect } from '@/ui/view-bar/components/FilterDropdownEntitySearchSelect'; -import { useViewBarContext } from '@/ui/view-bar/hooks/useViewBarContext'; -import { filterDropdownSearchInputScopedState } from '@/ui/view-bar/states/filterDropdownSearchInputScopedState'; -import { filterDropdownSelectedEntityIdScopedState } from '@/ui/view-bar/states/filterDropdownSelectedEntityIdScopedState'; import { useFilteredSearchCompanyQuery } from '../hooks/useFilteredSearchCompanyQuery'; diff --git a/front/src/modules/companies/components/HooksCompanyBoardEffect.tsx b/front/src/modules/companies/components/HooksCompanyBoardEffect.tsx index 241dfaedca5f8..cdb7eed833783 100644 --- a/front/src/modules/companies/components/HooksCompanyBoardEffect.tsx +++ b/front/src/modules/companies/components/HooksCompanyBoardEffect.tsx @@ -2,22 +2,22 @@ import { useEffect, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useRecoilCallback, useRecoilState } from 'recoil'; -import { useBoardActionBarEntries } from '@/ui/board/hooks/useBoardActionBarEntries'; -import { useBoardContextMenuEntries } from '@/ui/board/hooks/useBoardContextMenuEntries'; -import { isBoardLoadedState } from '@/ui/board/states/isBoardLoadedState'; +import { availableFiltersScopedState } from '@/ui/data/view-bar/states/availableFiltersScopedState'; +import { availableSortsScopedState } from '@/ui/data/view-bar/states/availableSortsScopedState'; +import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState'; +import { entityCountInCurrentViewState } from '@/ui/data/view-bar/states/entityCountInCurrentViewState'; +import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState'; +import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState'; +import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState'; +import { sortsOrderByScopedSelector } from '@/ui/data/view-bar/states/selectors/sortsOrderByScopedSelector'; +import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState'; +import { turnFilterIntoWhereClause } from '@/ui/data/view-bar/utils/turnFilterIntoWhereClause'; +import { useBoardActionBarEntries } from '@/ui/layout/board/hooks/useBoardActionBarEntries'; +import { useBoardContextMenuEntries } from '@/ui/layout/board/hooks/useBoardContextMenuEntries'; +import { isBoardLoadedState } from '@/ui/layout/board/states/isBoardLoadedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; -import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState'; -import { availableSortsScopedState } from '@/ui/view-bar/states/availableSortsScopedState'; -import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; -import { entityCountInCurrentViewState } from '@/ui/view-bar/states/entityCountInCurrentViewState'; -import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; -import { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamilyState'; -import { savedSortsFamilyState } from '@/ui/view-bar/states/savedSortsFamilyState'; -import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; -import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState'; -import { turnFilterIntoWhereClause } from '@/ui/view-bar/utils/turnFilterIntoWhereClause'; import { Pipeline, PipelineProgressableType, diff --git a/front/src/modules/companies/components/NewCompanyProgressButton.tsx b/front/src/modules/companies/components/NewCompanyProgressButton.tsx index c47fd1352c7c3..3a76334fb6af4 100644 --- a/front/src/modules/companies/components/NewCompanyProgressButton.tsx +++ b/front/src/modules/companies/components/NewCompanyProgressButton.tsx @@ -1,11 +1,11 @@ import { useCallback, useContext, useState } from 'react'; -import { NewButton } from '@/ui/board/components/NewButton'; -import { BoardColumnContext } from '@/ui/board/contexts/BoardColumnContext'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { NewButton } from '@/ui/layout/board/components/NewButton'; +import { BoardColumnContext } from '@/ui/layout/board/contexts/BoardColumnContext'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; diff --git a/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx b/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx index eee7c45eb2d6f..ef41276684720 100644 --- a/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx +++ b/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx @@ -1,4 +1,4 @@ -import { ColumnDefinition } from '@/ui/data-table/types/ColumnDefinition'; +import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition'; import { FieldBooleanMetadata, FieldChipMetadata, @@ -9,7 +9,7 @@ import { FieldRelationMetadata, FieldTextMetadata, FieldURLMetadata, -} from '@/ui/field/types/FieldMetadata'; +} from '@/ui/data/field/types/FieldMetadata'; import { IconArrowUpRight, IconBrandLinkedin, @@ -23,7 +23,7 @@ import { IconTarget, IconUserCircle, IconUsers, -} from '@/ui/icon/index'; +} from '@/ui/display/icon/index'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; import { User } from '~/generated/graphql'; diff --git a/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx index b0e0e19d6348c..60d0940132fd9 100644 --- a/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx +++ b/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import styled from '@emotion/styled'; -import { FieldRecoilScopeContext } from '@/ui/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext'; +import { FieldRecoilScopeContext } from '@/ui/data/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; diff --git a/front/src/modules/companies/hooks/useCompanyQuery.ts b/front/src/modules/companies/hooks/useCompanyQuery.ts index bcd7e6a34bc2b..5565fca76ae65 100644 --- a/front/src/modules/companies/hooks/useCompanyQuery.ts +++ b/front/src/modules/companies/hooks/useCompanyQuery.ts @@ -1,6 +1,6 @@ import { useSetRecoilState } from 'recoil'; -import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState'; +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; import { useGetCompanyQuery } from '~/generated/graphql'; export const useCompanyQuery = (id: string) => { diff --git a/front/src/modules/companies/hooks/useCompanyTableActionBarEntries.tsx b/front/src/modules/companies/hooks/useCompanyTableActionBarEntries.tsx index e68c58ca1ece8..910c02dd094b9 100644 --- a/front/src/modules/companies/hooks/useCompanyTableActionBarEntries.tsx +++ b/front/src/modules/companies/hooks/useCompanyTableActionBarEntries.tsx @@ -2,8 +2,8 @@ import { useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; -import { actionBarEntriesState } from '@/ui/action-bar/states/actionBarEntriesState'; -import { IconCheckbox, IconNotes, IconTrash } from '@/ui/icon'; +import { IconCheckbox, IconNotes, IconTrash } from '@/ui/display/icon'; +import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; import { ActivityType } from '~/generated/graphql'; import { useDeleteSelectedComapnies } from './useDeleteCompanies'; diff --git a/front/src/modules/companies/hooks/useCompanyTableContextMenuEntries.tsx b/front/src/modules/companies/hooks/useCompanyTableContextMenuEntries.tsx index 3d831cef622ee..da15a8e96f912 100644 --- a/front/src/modules/companies/hooks/useCompanyTableContextMenuEntries.tsx +++ b/front/src/modules/companies/hooks/useCompanyTableContextMenuEntries.tsx @@ -3,16 +3,16 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { contextMenuEntriesState } from '@/ui/context-menu/states/contextMenuEntriesState'; -import { useResetTableRowSelection } from '@/ui/data-table/hooks/useResetTableRowSelection'; -import { selectedRowIdsSelector } from '@/ui/data-table/states/selectors/selectedRowIdsSelector'; +import { useResetTableRowSelection } from '@/ui/data/data-table/hooks/useResetTableRowSelection'; +import { selectedRowIdsSelector } from '@/ui/data/data-table/states/selectors/selectedRowIdsSelector'; import { IconCheckbox, IconHeart, IconHeartOff, IconNotes, IconTrash, -} from '@/ui/icon'; +} from '@/ui/display/icon'; +import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState'; import { ActivityType, useGetFavoritesQuery } from '~/generated/graphql'; import { useDeleteSelectedComapnies } from './useDeleteCompanies'; diff --git a/front/src/modules/companies/hooks/useCreateCompanyProgress.ts b/front/src/modules/companies/hooks/useCreateCompanyProgress.ts index b062409222f4e..ff1078b1e03a0 100644 --- a/front/src/modules/companies/hooks/useCreateCompanyProgress.ts +++ b/front/src/modules/companies/hooks/useCreateCompanyProgress.ts @@ -5,7 +5,7 @@ import { v4 } from 'uuid'; import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress'; import { GET_PIPELINES } from '@/pipeline/graphql/queries/getPipelines'; import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; -import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState'; +import { boardCardIdsByColumnIdFamilyState } from '@/ui/layout/board/states/boardCardIdsByColumnIdFamilyState'; import { useCreateOneCompanyPipelineProgressMutation } from '~/generated/graphql'; export const useCreateCompanyProgress = () => { diff --git a/front/src/modules/companies/hooks/useDeleteCompanies.ts b/front/src/modules/companies/hooks/useDeleteCompanies.ts index 04a675903331c..afd95a51dce49 100644 --- a/front/src/modules/companies/hooks/useDeleteCompanies.ts +++ b/front/src/modules/companies/hooks/useDeleteCompanies.ts @@ -3,9 +3,9 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict'; import { GET_PIPELINES } from '@/pipeline/graphql/queries/getPipelines'; -import { useResetTableRowSelection } from '@/ui/data-table/hooks/useResetTableRowSelection'; -import { selectedRowIdsSelector } from '@/ui/data-table/states/selectors/selectedRowIdsSelector'; -import { tableRowIdsState } from '@/ui/data-table/states/tableRowIdsState'; +import { useResetTableRowSelection } from '@/ui/data/data-table/hooks/useResetTableRowSelection'; +import { selectedRowIdsSelector } from '@/ui/data/data-table/states/selectors/selectedRowIdsSelector'; +import { tableRowIdsState } from '@/ui/data/data-table/states/tableRowIdsState'; import { useDeleteManyCompaniesMutation } from '~/generated/graphql'; export const useDeleteSelectedComapnies = () => { diff --git a/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts index 221f66285e87c..95e5f71b8d924 100644 --- a/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts +++ b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts @@ -2,7 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { SpreadsheetOptions } from '@/spreadsheet-import/types'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; import { useInsertManyCompanyMutation } from '~/generated/graphql'; import { fieldsForCompany } from '../utils/fieldsForCompany'; diff --git a/front/src/modules/companies/hooks/useUpdateBoardCardIds.ts b/front/src/modules/companies/hooks/useUpdateBoardCardIds.ts index 4756b533eaa64..c5095ba82863b 100644 --- a/front/src/modules/companies/hooks/useUpdateBoardCardIds.ts +++ b/front/src/modules/companies/hooks/useUpdateBoardCardIds.ts @@ -1,7 +1,7 @@ import { useRecoilCallback } from 'recoil'; -import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState'; -import { boardColumnsState } from '@/ui/board/states/boardColumnsState'; +import { boardCardIdsByColumnIdFamilyState } from '@/ui/layout/board/states/boardCardIdsByColumnIdFamilyState'; +import { boardColumnsState } from '@/ui/layout/board/states/boardColumnsState'; import { GetPipelineProgressQuery } from '~/generated/graphql'; export const useUpdateCompanyBoardCardIds = () => diff --git a/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts b/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts index 5562813a2340f..2ceed24e86f2c 100644 --- a/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts +++ b/front/src/modules/companies/hooks/useUpdateCompanyBoardColumns.ts @@ -1,11 +1,11 @@ import { useRecoilCallback } from 'recoil'; import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; -import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState'; -import { boardColumnsState } from '@/ui/board/states/boardColumnsState'; -import { savedBoardColumnsState } from '@/ui/board/states/savedBoardColumnsState'; -import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition'; -import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState'; +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; +import { boardCardIdsByColumnIdFamilyState } from '@/ui/layout/board/states/boardCardIdsByColumnIdFamilyState'; +import { boardColumnsState } from '@/ui/layout/board/states/boardColumnsState'; +import { savedBoardColumnsState } from '@/ui/layout/board/states/savedBoardColumnsState'; +import { BoardColumnDefinition } from '@/ui/layout/board/types/BoardColumnDefinition'; import { isThemeColor } from '@/ui/theme/utils/castStringAsThemeColor'; import { Pipeline } from '~/generated/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; diff --git a/front/src/modules/companies/table/components/CompanyTable.tsx b/front/src/modules/companies/table/components/CompanyTable.tsx index 61e6e0d8edfb2..569c4998b73c4 100644 --- a/front/src/modules/companies/table/components/CompanyTable.tsx +++ b/front/src/modules/companies/table/components/CompanyTable.tsx @@ -3,15 +3,15 @@ import { getCompaniesOptimisticEffectDefinition } from '@/companies/graphql/opti import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries'; import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries'; import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport'; -import { DataTable } from '@/ui/data-table/components/DataTable'; -import { DataTableEffect } from '@/ui/data-table/components/DataTableEffect'; -import { TableContext } from '@/ui/data-table/contexts/TableContext'; -import { useUpsertDataTableItem } from '@/ui/data-table/hooks/useUpsertDataTableItem'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { DataTable } from '@/ui/data/data-table/components/DataTable'; +import { DataTableEffect } from '@/ui/data/data-table/components/DataTableEffect'; +import { TableContext } from '@/ui/data/data-table/contexts/TableContext'; +import { useUpsertDataTableItem } from '@/ui/data/data-table/hooks/useUpsertDataTableItem'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; +import { filtersWhereScopedSelector } from '@/ui/data/view-bar/states/selectors/filtersWhereScopedSelector'; +import { sortsOrderByScopedSelector } from '@/ui/data/view-bar/states/selectors/sortsOrderByScopedSelector'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; -import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; -import { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filtersWhereScopedSelector'; -import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { useTableViews } from '@/views/hooks/useTableViews'; import { UpdateOneCompanyMutationVariables, diff --git a/front/src/modules/companies/table/components/CompanyTableMockDataEffect.tsx b/front/src/modules/companies/table/components/CompanyTableMockDataEffect.tsx index 29bc77592b0ae..a2105506abcbf 100644 --- a/front/src/modules/companies/table/components/CompanyTableMockDataEffect.tsx +++ b/front/src/modules/companies/table/components/CompanyTableMockDataEffect.tsx @@ -1,8 +1,8 @@ import { useEffect } from 'react'; -import { useSetDataTableData } from '@/ui/data-table/hooks/useSetDataTableData'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; -import { tableColumnsScopedState } from '@/ui/data-table/states/tableColumnsScopedState'; +import { useSetDataTableData } from '@/ui/data/data-table/hooks/useSetDataTableData'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { tableColumnsScopedState } from '@/ui/data/data-table/states/tableColumnsScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { companiesAvailableColumnDefinitions } from '../../constants/companiesAvailableColumnDefinitions'; diff --git a/front/src/modules/companies/table/components/CompanyTableMockMode.tsx b/front/src/modules/companies/table/components/CompanyTableMockMode.tsx index ab8c93c496030..8267a1ee1d73c 100644 --- a/front/src/modules/companies/table/components/CompanyTableMockMode.tsx +++ b/front/src/modules/companies/table/components/CompanyTableMockMode.tsx @@ -1,6 +1,6 @@ -import { DataTable } from '@/ui/data-table/components/DataTable'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; -import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; +import { DataTable } from '@/ui/data/data-table/components/DataTable'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; import { useUpdateOneCompanyMutation } from '~/generated/graphql'; import { CompanyTableMockDataEffect } from './CompanyTableMockDataEffect'; diff --git a/front/src/modules/companies/utils/fieldsForCompany.tsx b/front/src/modules/companies/utils/fieldsForCompany.tsx index 2287a09186c35..28388a6ae9a37 100644 --- a/front/src/modules/companies/utils/fieldsForCompany.tsx +++ b/front/src/modules/companies/utils/fieldsForCompany.tsx @@ -7,7 +7,7 @@ import { IconMoneybag, IconTarget, IconUsers, -} from '@/ui/icon'; +} from '@/ui/display/icon'; export const fieldsForCompany = [ { diff --git a/front/src/modules/favorites/components/Favorites.tsx b/front/src/modules/favorites/components/Favorites.tsx index ab7b960548d7d..1cae8177eec87 100644 --- a/front/src/modules/favorites/components/Favorites.tsx +++ b/front/src/modules/favorites/components/Favorites.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; -import NavItem from '@/ui/navbar/components/NavItem'; -import NavTitle from '@/ui/navbar/components/NavTitle'; +import NavItem from '@/ui/navigation/navbar/components/NavItem'; +import NavTitle from '@/ui/navigation/navbar/components/NavTitle'; import { Avatar } from '@/users/components/Avatar'; import { useGetFavoritesQuery } from '~/generated/graphql'; import { getLogoUrlFromDomainName } from '~/utils'; diff --git a/front/src/modules/metadata/components/ObjectDataTableEffect.tsx b/front/src/modules/metadata/components/ObjectDataTableEffect.tsx index 3badecf633f1f..78d630fcf2a0e 100644 --- a/front/src/modules/metadata/components/ObjectDataTableEffect.tsx +++ b/front/src/modules/metadata/components/ObjectDataTableEffect.tsx @@ -2,13 +2,13 @@ import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useRecoilCallback } from 'recoil'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState'; +import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState'; +import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState'; +import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState'; +import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState'; import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; -import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; -import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; -import { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamilyState'; -import { savedSortsFamilyState } from '@/ui/view-bar/states/savedSortsFamilyState'; -import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState'; import { useFindManyCustomObjects } from '../hooks/useFindManyCustomObjects'; diff --git a/front/src/modules/metadata/components/ObjectTable.tsx b/front/src/modules/metadata/components/ObjectTable.tsx index 04bd98a342393..fdaf3ae2a5f0a 100644 --- a/front/src/modules/metadata/components/ObjectTable.tsx +++ b/front/src/modules/metadata/components/ObjectTable.tsx @@ -1,9 +1,9 @@ import { suppliersAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions'; import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport'; -import { DataTable } from '@/ui/data-table/components/DataTable'; -import { TableContext } from '@/ui/data-table/contexts/TableContext'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; -import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; +import { DataTable } from '@/ui/data/data-table/components/DataTable'; +import { TableContext } from '@/ui/data/data-table/contexts/TableContext'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; import { useTableViews } from '@/views/hooks/useTableViews'; import { ObjectDataTableEffect } from './ObjectDataTableEffect'; diff --git a/front/src/modules/metadata/components/useSetDataTableData.ts b/front/src/modules/metadata/components/useSetDataTableData.ts index 17025876bb5b4..f09d4a158372c 100644 --- a/front/src/modules/metadata/components/useSetDataTableData.ts +++ b/front/src/modules/metadata/components/useSetDataTableData.ts @@ -1,15 +1,15 @@ import { useRecoilCallback } from 'recoil'; -import { useResetTableRowSelection } from '@/ui/data-table/hooks/useResetTableRowSelection'; -import { isFetchingDataTableDataState } from '@/ui/data-table/states/isFetchingDataTableDataState'; -import { numberOfTableRowsState } from '@/ui/data-table/states/numberOfTableRowsState'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; -import { tableRowIdsState } from '@/ui/data-table/states/tableRowIdsState'; -import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState'; +import { useResetTableRowSelection } from '@/ui/data/data-table/hooks/useResetTableRowSelection'; +import { isFetchingDataTableDataState } from '@/ui/data/data-table/states/isFetchingDataTableDataState'; +import { numberOfTableRowsState } from '@/ui/data/data-table/states/numberOfTableRowsState'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { tableRowIdsState } from '@/ui/data/data-table/states/tableRowIdsState'; +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; +import { availableFiltersScopedState } from '@/ui/data/view-bar/states/availableFiltersScopedState'; +import { availableSortsScopedState } from '@/ui/data/view-bar/states/availableSortsScopedState'; +import { entityCountInCurrentViewState } from '@/ui/data/view-bar/states/entityCountInCurrentViewState'; import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; -import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState'; -import { availableSortsScopedState } from '@/ui/view-bar/states/availableSortsScopedState'; -import { entityCountInCurrentViewState } from '@/ui/view-bar/states/entityCountInCurrentViewState'; export const useSetObjectDataTableData = () => { const resetTableRowSelection = useResetTableRowSelection(); diff --git a/front/src/modules/people/components/FilterDropdownPeopleSearchSelect.tsx b/front/src/modules/people/components/FilterDropdownPeopleSearchSelect.tsx index c647042b255f6..4c9aebf4370a5 100644 --- a/front/src/modules/people/components/FilterDropdownPeopleSearchSelect.tsx +++ b/front/src/modules/people/components/FilterDropdownPeopleSearchSelect.tsx @@ -1,10 +1,10 @@ import { useFilteredSearchPeopleQuery } from '@/people/hooks/useFilteredSearchPeopleQuery'; +import { FilterDropdownEntitySearchSelect } from '@/ui/data/view-bar/components/FilterDropdownEntitySearchSelect'; +import { useViewBarContext } from '@/ui/data/view-bar/hooks/useViewBarContext'; +import { filterDropdownSearchInputScopedState } from '@/ui/data/view-bar/states/filterDropdownSearchInputScopedState'; +import { filterDropdownSelectedEntityIdScopedState } from '@/ui/data/view-bar/states/filterDropdownSelectedEntityIdScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; -import { FilterDropdownEntitySearchSelect } from '@/ui/view-bar/components/FilterDropdownEntitySearchSelect'; -import { useViewBarContext } from '@/ui/view-bar/hooks/useViewBarContext'; -import { filterDropdownSearchInputScopedState } from '@/ui/view-bar/states/filterDropdownSearchInputScopedState'; -import { filterDropdownSelectedEntityIdScopedState } from '@/ui/view-bar/states/filterDropdownSelectedEntityIdScopedState'; export const FilterDropdownPeopleSearchSelect = () => { const { ViewBarRecoilScopeContext } = useViewBarContext(); diff --git a/front/src/modules/people/components/PeopleCard.tsx b/front/src/modules/people/components/PeopleCard.tsx index 1156ca79d12a4..e1780043e057a 100644 --- a/front/src/modules/people/components/PeopleCard.tsx +++ b/front/src/modules/people/components/PeopleCard.tsx @@ -4,11 +4,11 @@ import { getOperationName } from '@apollo/client/utilities'; import styled from '@emotion/styled'; import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react'; -import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton'; -import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; -import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; -import { IconDotsVertical, IconLinkOff, IconTrash } from '@/ui/icon'; -import { MenuItem } from '@/ui/menu-item/components/MenuItem'; +import { IconDotsVertical, IconLinkOff, IconTrash } from '@/ui/display/icon'; +import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { Avatar } from '@/users/components/Avatar'; import { diff --git a/front/src/modules/people/components/PersonChip.tsx b/front/src/modules/people/components/PersonChip.tsx index 00ebfbfecc211..5879ee96f91f7 100644 --- a/front/src/modules/people/components/PersonChip.tsx +++ b/front/src/modules/people/components/PersonChip.tsx @@ -1,4 +1,7 @@ -import { EntityChip, EntityChipVariant } from '@/ui/chip/components/EntityChip'; +import { + EntityChip, + EntityChipVariant, +} from '@/ui/display/chip/components/EntityChip'; export type PersonChipProps = { id: string; diff --git a/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx b/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx index 6a841d539d13b..badd2fb8b40a8 100644 --- a/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx +++ b/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx @@ -1,4 +1,4 @@ -import { ColumnDefinition } from '@/ui/data-table/types/ColumnDefinition'; +import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition'; import { FieldDateMetadata, FieldDoubleTextChipMetadata, @@ -8,7 +8,7 @@ import { FieldRelationMetadata, FieldTextMetadata, FieldURLMetadata, -} from '@/ui/field/types/FieldMetadata'; +} from '@/ui/data/field/types/FieldMetadata'; import { IconArrowUpRight, IconBrandLinkedin, @@ -21,7 +21,7 @@ import { IconPencil, IconPhone, IconUser, -} from '@/ui/icon/index'; +} from '@/ui/display/icon/index'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; import { Company } from '~/generated/graphql'; import { getLogoUrlFromDomainName } from '~/utils'; diff --git a/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx b/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx index 4ddcda882c990..1d7bb629f0275 100644 --- a/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx +++ b/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { FieldRecoilScopeContext } from '@/ui/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext'; +import { FieldRecoilScopeContext } from '@/ui/data/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext'; import { EntityTitleDoubleTextInput } from '@/ui/input/components/EntityTitleDoubleTextInput'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { Person, useUpdateOnePersonMutation } from '~/generated/graphql'; diff --git a/front/src/modules/people/hooks/useCreateActivityForPeople.ts b/front/src/modules/people/hooks/useCreateActivityForPeople.ts index 018d7b90f0d4e..5964d7a68fc32 100644 --- a/front/src/modules/people/hooks/useCreateActivityForPeople.ts +++ b/front/src/modules/people/hooks/useCreateActivityForPeople.ts @@ -5,8 +5,8 @@ import { ActivityTargetableEntity, ActivityTargetableEntityType, } from '@/activities/types/ActivityTargetableEntity'; -import { selectedRowIdsSelector } from '@/ui/data-table/states/selectors/selectedRowIdsSelector'; -import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState'; +import { selectedRowIdsSelector } from '@/ui/data/data-table/states/selectors/selectedRowIdsSelector'; +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; import { ActivityType, Person } from '~/generated/graphql'; export const useCreateActivityForPeople = () => { diff --git a/front/src/modules/people/hooks/usePeopleTableContextMenuEntries.tsx b/front/src/modules/people/hooks/usePeopleTableContextMenuEntries.tsx index ad5ee7805564b..a61811f226f7a 100644 --- a/front/src/modules/people/hooks/usePeopleTableContextMenuEntries.tsx +++ b/front/src/modules/people/hooks/usePeopleTableContextMenuEntries.tsx @@ -2,17 +2,17 @@ import { getOperationName } from '@apollo/client/utilities'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { contextMenuEntriesState } from '@/ui/context-menu/states/contextMenuEntriesState'; -import { useResetTableRowSelection } from '@/ui/data-table/hooks/useResetTableRowSelection'; -import { selectedRowIdsSelector } from '@/ui/data-table/states/selectors/selectedRowIdsSelector'; -import { tableRowIdsState } from '@/ui/data-table/states/tableRowIdsState'; +import { useResetTableRowSelection } from '@/ui/data/data-table/hooks/useResetTableRowSelection'; +import { selectedRowIdsSelector } from '@/ui/data/data-table/states/selectors/selectedRowIdsSelector'; +import { tableRowIdsState } from '@/ui/data/data-table/states/tableRowIdsState'; import { IconCheckbox, IconHeart, IconHeartOff, IconNotes, IconTrash, -} from '@/ui/icon'; +} from '@/ui/display/icon'; +import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState'; import { ActivityType, useDeleteManyPersonMutation, diff --git a/front/src/modules/people/hooks/usePersonQuery.ts b/front/src/modules/people/hooks/usePersonQuery.ts index 887d647c41ff7..ab20e18a0fcee 100644 --- a/front/src/modules/people/hooks/usePersonQuery.ts +++ b/front/src/modules/people/hooks/usePersonQuery.ts @@ -1,6 +1,6 @@ import { useSetRecoilState } from 'recoil'; -import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState'; +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; import { useGetPersonQuery } from '~/generated/graphql'; export const usePersonQuery = (id: string) => { diff --git a/front/src/modules/people/hooks/usePersonTableActionBarEntries.tsx b/front/src/modules/people/hooks/usePersonTableActionBarEntries.tsx index 77df3e860887e..2705a25c43df3 100644 --- a/front/src/modules/people/hooks/usePersonTableActionBarEntries.tsx +++ b/front/src/modules/people/hooks/usePersonTableActionBarEntries.tsx @@ -1,11 +1,11 @@ import { getOperationName } from '@apollo/client/utilities'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; -import { actionBarEntriesState } from '@/ui/action-bar/states/actionBarEntriesState'; -import { useResetTableRowSelection } from '@/ui/data-table/hooks/useResetTableRowSelection'; -import { selectedRowIdsSelector } from '@/ui/data-table/states/selectors/selectedRowIdsSelector'; -import { tableRowIdsState } from '@/ui/data-table/states/tableRowIdsState'; -import { IconCheckbox, IconNotes, IconTrash } from '@/ui/icon'; +import { useResetTableRowSelection } from '@/ui/data/data-table/hooks/useResetTableRowSelection'; +import { selectedRowIdsSelector } from '@/ui/data/data-table/states/selectors/selectedRowIdsSelector'; +import { tableRowIdsState } from '@/ui/data/data-table/states/tableRowIdsState'; +import { IconCheckbox, IconNotes, IconTrash } from '@/ui/display/icon'; +import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; import { ActivityType, useDeleteManyPersonMutation } from '~/generated/graphql'; import { GET_PEOPLE } from '../graphql/queries/getPeople'; diff --git a/front/src/modules/people/hooks/useSetPeopleDataTable.ts b/front/src/modules/people/hooks/useSetPeopleDataTable.ts index e79e28b960224..048534a51e86c 100644 --- a/front/src/modules/people/hooks/useSetPeopleDataTable.ts +++ b/front/src/modules/people/hooks/useSetPeopleDataTable.ts @@ -1,14 +1,14 @@ import { useLocation } from 'react-router-dom'; import { useRecoilCallback } from 'recoil'; -import { useResetTableRowSelection } from '@/ui/data-table/hooks/useResetTableRowSelection'; -import { isFetchingDataTableDataState } from '@/ui/data-table/states/isFetchingDataTableDataState'; -import { numberOfTableRowsState } from '@/ui/data-table/states/numberOfTableRowsState'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; -import { tableRowIdsState } from '@/ui/data-table/states/tableRowIdsState'; +import { useResetTableRowSelection } from '@/ui/data/data-table/hooks/useResetTableRowSelection'; +import { isFetchingDataTableDataState } from '@/ui/data/data-table/states/isFetchingDataTableDataState'; +import { numberOfTableRowsState } from '@/ui/data/data-table/states/numberOfTableRowsState'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { tableRowIdsState } from '@/ui/data/data-table/states/tableRowIdsState'; +import { availableFiltersScopedState } from '@/ui/data/view-bar/states/availableFiltersScopedState'; import { currentPageLocationState } from '@/ui/utilities/loading-state/states/currentPageLocationState'; import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; -import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState'; import { GetPeopleQuery } from '~/generated/graphql'; import { peopleFilters } from '~/pages/people/people-filters'; diff --git a/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts index 4ba3c74a7bcfa..f9c16d228c890 100644 --- a/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts +++ b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts @@ -2,7 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { SpreadsheetOptions } from '@/spreadsheet-import/types'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; import { useInsertManyPersonMutation } from '~/generated/graphql'; import { fieldsForPerson } from '../utils/fieldsForPerson'; diff --git a/front/src/modules/people/table/components/PeopleTable.tsx b/front/src/modules/people/table/components/PeopleTable.tsx index d72e3c02ce324..fe16ef4928671 100644 --- a/front/src/modules/people/table/components/PeopleTable.tsx +++ b/front/src/modules/people/table/components/PeopleTable.tsx @@ -3,15 +3,15 @@ import { getPeopleOptimisticEffectDefinition } from '@/people/graphql/optimistic import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries'; import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries'; import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport'; -import { DataTable } from '@/ui/data-table/components/DataTable'; -import { DataTableEffect } from '@/ui/data-table/components/DataTableEffect'; -import { TableContext } from '@/ui/data-table/contexts/TableContext'; -import { useUpsertDataTableItem } from '@/ui/data-table/hooks/useUpsertDataTableItem'; -import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { DataTable } from '@/ui/data/data-table/components/DataTable'; +import { DataTableEffect } from '@/ui/data/data-table/components/DataTableEffect'; +import { TableContext } from '@/ui/data/data-table/contexts/TableContext'; +import { useUpsertDataTableItem } from '@/ui/data/data-table/hooks/useUpsertDataTableItem'; +import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; +import { filtersWhereScopedSelector } from '@/ui/data/view-bar/states/selectors/filtersWhereScopedSelector'; +import { sortsOrderByScopedSelector } from '@/ui/data/view-bar/states/selectors/sortsOrderByScopedSelector'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; -import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext'; -import { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filtersWhereScopedSelector'; -import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { useTableViews } from '@/views/hooks/useTableViews'; import { UpdateOnePersonMutationVariables, diff --git a/front/src/modules/people/utils/fieldsForPerson.tsx b/front/src/modules/people/utils/fieldsForPerson.tsx index 53f0aec8846fa..a2fe05a037de8 100644 --- a/front/src/modules/people/utils/fieldsForPerson.tsx +++ b/front/src/modules/people/utils/fieldsForPerson.tsx @@ -8,7 +8,7 @@ import { IconMail, IconMap, IconUser, -} from '@/ui/icon'; +} from '@/ui/display/icon'; export const fieldsForPerson = [ { diff --git a/front/src/modules/pipeline/components/PipelineAddButton.tsx b/front/src/modules/pipeline/components/PipelineAddButton.tsx index 300b455a43a0c..f335112c66768 100644 --- a/front/src/modules/pipeline/components/PipelineAddButton.tsx +++ b/front/src/modules/pipeline/components/PipelineAddButton.tsx @@ -1,13 +1,13 @@ import { CompanyProgressPicker } from '@/companies/components/CompanyProgressPicker'; import { useCreateCompanyProgress } from '@/companies/hooks/useCreateCompanyProgress'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { IconButton } from '@/ui/button/components/IconButton'; -import { useDropdown } from '@/ui/dropdown/hooks/useDropdown'; -import { IconPlus } from '@/ui/icon/index'; +import { ViewBarDropdownButton } from '@/ui/data/view-bar/components/ViewBarDropdownButton'; +import { IconPlus } from '@/ui/display/icon/index'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; +import { IconButton } from '@/ui/input/button/components/IconButton'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; -import { ViewBarDropdownButton } from '@/ui/view-bar/components/ViewBarDropdownButton'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { logError } from '~/utils/logError'; export const PipelineAddButton = () => { diff --git a/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx b/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx index 32486c60000aa..bfea0f2de0ac9 100644 --- a/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx +++ b/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx @@ -1,19 +1,19 @@ -import { BoardFieldDefinition } from '@/ui/board/types/BoardFieldDefinition'; import { FieldDateMetadata, FieldMetadata, FieldNumberMetadata, FieldProbabilityMetadata, FieldRelationMetadata, -} from '@/ui/field/types/FieldMetadata'; +} from '@/ui/data/field/types/FieldMetadata'; import { IconCalendarEvent, IconCurrencyDollar, IconPencil, IconProgressCheck, IconUser, -} from '@/ui/icon'; +} from '@/ui/display/icon'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { BoardFieldDefinition } from '@/ui/layout/board/types/BoardFieldDefinition'; import { Person } from '~/generated/graphql'; export const pipelineAvailableFieldDefinitions: BoardFieldDefinition[] = diff --git a/front/src/modules/pipeline/hooks/usePipelineStages.ts b/front/src/modules/pipeline/hooks/usePipelineStages.ts index d7a09fd9a5d63..fcb1ce9907484 100644 --- a/front/src/modules/pipeline/hooks/usePipelineStages.ts +++ b/front/src/modules/pipeline/hooks/usePipelineStages.ts @@ -1,7 +1,7 @@ import { getOperationName } from '@apollo/client/utilities'; import { useRecoilValue } from 'recoil'; -import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition'; +import { BoardColumnDefinition } from '@/ui/layout/board/types/BoardColumnDefinition'; import { useCreatePipelineStageMutation, useDeletePipelineStageMutation, diff --git a/front/src/modules/settings/components/IconWithLabel.tsx b/front/src/modules/settings/components/IconWithLabel.tsx index 1470b32dc96e1..92791e1d10cdc 100644 --- a/front/src/modules/settings/components/IconWithLabel.tsx +++ b/front/src/modules/settings/components/IconWithLabel.tsx @@ -1,7 +1,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComponent } from '@/ui/icon/types/IconComponent'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; type IconWithLabelProps = { Icon: IconComponent; diff --git a/front/src/modules/settings/components/SettingsIconSection.tsx b/front/src/modules/settings/components/SettingsIconSection.tsx index 6224032da7588..b3995e0a02454 100644 --- a/front/src/modules/settings/components/SettingsIconSection.tsx +++ b/front/src/modules/settings/components/SettingsIconSection.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; -import { IconComponent } from '@/ui/icon/types/IconComponent'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { H2Title } from '@/ui/display/typography/components/H2Title'; import { IconPicker } from '@/ui/input/components/IconPicker'; -import { H2Title } from '@/ui/typography/components/H2Title'; import ArrowRight from '../assets/ArrowRight.svg'; diff --git a/front/src/modules/settings/components/SettingsNavbar.tsx b/front/src/modules/settings/components/SettingsNavbar.tsx index 17f9fbfd08227..c23131209286b 100644 --- a/front/src/modules/settings/components/SettingsNavbar.tsx +++ b/front/src/modules/settings/components/SettingsNavbar.tsx @@ -10,10 +10,10 @@ import { IconSettings, IconUserCircle, IconUsers, -} from '@/ui/icon/index'; -import NavItem from '@/ui/navbar/components/NavItem'; -import NavTitle from '@/ui/navbar/components/NavTitle'; -import SubMenuNavbar from '@/ui/navbar/components/SubMenuNavbar'; +} from '@/ui/display/icon/index'; +import NavItem from '@/ui/navigation/navbar/components/NavItem'; +import NavTitle from '@/ui/navigation/navbar/components/NavTitle'; +import SubMenuNavbar from '@/ui/navigation/navbar/components/SubMenuNavbar'; export const SettingsNavbar = () => { const navigate = useNavigate(); diff --git a/front/src/modules/settings/objects/components/ObjectFieldDataType.tsx b/front/src/modules/settings/objects/components/ObjectFieldDataType.tsx index ffdc9009ebe76..d07f7c5e0d218 100644 --- a/front/src/modules/settings/objects/components/ObjectFieldDataType.tsx +++ b/front/src/modules/settings/objects/components/ObjectFieldDataType.tsx @@ -8,8 +8,8 @@ import { IconPlug, IconSocial, IconUserCircle, -} from '@/ui/icon'; -import { IconComponent } from '@/ui/icon/types/IconComponent'; +} from '@/ui/display/icon'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { ObjectFieldItem } from '../types/ObjectFieldItem'; diff --git a/front/src/modules/settings/objects/components/ObjectFieldItemTableRow.tsx b/front/src/modules/settings/objects/components/ObjectFieldItemTableRow.tsx index 6652e82943322..b7c23a52686b3 100644 --- a/front/src/modules/settings/objects/components/ObjectFieldItemTableRow.tsx +++ b/front/src/modules/settings/objects/components/ObjectFieldItemTableRow.tsx @@ -1,9 +1,9 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconDotsVertical } from '@/ui/icon'; -import { TableCell } from '@/ui/table/components/TableCell'; -import { TableRow } from '@/ui/table/components/TableRow'; +import { IconDotsVertical } from '@/ui/display/icon'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; import { ObjectFieldItem } from '../types/ObjectFieldItem'; diff --git a/front/src/modules/settings/objects/constants/mockObjects.ts b/front/src/modules/settings/objects/constants/mockObjects.ts index 011fcdf5f4001..a242c3328b03b 100644 --- a/front/src/modules/settings/objects/constants/mockObjects.ts +++ b/front/src/modules/settings/objects/constants/mockObjects.ts @@ -14,7 +14,7 @@ import { IconUser, IconUserCircle, IconUsers, -} from '@/ui/icon'; +} from '@/ui/display/icon'; export const activeObjectItems = [ { diff --git a/front/src/modules/settings/objects/types/ObjectFieldItem.ts b/front/src/modules/settings/objects/types/ObjectFieldItem.ts index 8feb6bc01a50b..77edcb1b6cbb8 100644 --- a/front/src/modules/settings/objects/types/ObjectFieldItem.ts +++ b/front/src/modules/settings/objects/types/ObjectFieldItem.ts @@ -1,4 +1,4 @@ -import { IconComponent } from '@/ui/icon/types/IconComponent'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; export type ObjectFieldItem = { name: string; diff --git a/front/src/modules/settings/profile/components/DeleteAccount.tsx b/front/src/modules/settings/profile/components/DeleteAccount.tsx index 85563f40e9bc1..d3e52ad09ecae 100644 --- a/front/src/modules/settings/profile/components/DeleteAccount.tsx +++ b/front/src/modules/settings/profile/components/DeleteAccount.tsx @@ -5,9 +5,9 @@ import { useRecoilValue } from 'recoil'; import { useAuth } from '@/auth/hooks/useAuth'; import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; -import { Button } from '@/ui/button/components/Button'; -import { ConfirmationModal } from '@/ui/modal/components/ConfirmationModal'; -import { H2Title } from '@/ui/typography/components/H2Title'; +import { H2Title } from '@/ui/display/typography/components/H2Title'; +import { Button } from '@/ui/input/button/components/Button'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useDeleteUserAccountMutation } from '~/generated/graphql'; export const DeleteAccount = () => { diff --git a/front/src/modules/settings/profile/components/DeleteWorkspace.tsx b/front/src/modules/settings/profile/components/DeleteWorkspace.tsx index f8289bdb2ca16..4264bd9282997 100644 --- a/front/src/modules/settings/profile/components/DeleteWorkspace.tsx +++ b/front/src/modules/settings/profile/components/DeleteWorkspace.tsx @@ -5,11 +5,11 @@ import { useRecoilValue } from 'recoil'; import { useAuth } from '@/auth/hooks/useAuth'; import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; +import { H2Title } from '@/ui/display/typography/components/H2Title'; import { ConfirmationModal, StyledConfirmationButton, -} from '@/ui/modal/components/ConfirmationModal'; -import { H2Title } from '@/ui/typography/components/H2Title'; +} from '@/ui/layout/modal/components/ConfirmationModal'; import { useDeleteCurrentWorkspaceMutation } from '~/generated/graphql'; export const DeleteWorkspace = () => { diff --git a/front/src/modules/settings/profile/components/ToggleField.tsx b/front/src/modules/settings/profile/components/ToggleField.tsx index 3cd20fdb38122..84401e2372414 100644 --- a/front/src/modules/settings/profile/components/ToggleField.tsx +++ b/front/src/modules/settings/profile/components/ToggleField.tsx @@ -1,8 +1,8 @@ import { useRecoilValue } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; import { Toggle } from '@/ui/input/components/Toggle'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { useUpdateAllowImpersonationMutation } from '~/generated/graphql'; export const ToggleField = () => { diff --git a/front/src/modules/spreadsheet-import/components/ContinueButton.tsx b/front/src/modules/spreadsheet-import/components/ContinueButton.tsx index 8d81951ce92b6..a3b06aba4279b 100644 --- a/front/src/modules/spreadsheet-import/components/ContinueButton.tsx +++ b/front/src/modules/spreadsheet-import/components/ContinueButton.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; -import { MainButton } from '@/ui/button/components/MainButton'; -import { Modal } from '@/ui/modal/components/Modal'; -import { CircularProgressBar } from '@/ui/progress-bar/components/CircularProgressBar'; +import { CircularProgressBar } from '@/ui/feedback/progress-bar/components/CircularProgressBar'; +import { MainButton } from '@/ui/input/button/components/MainButton'; +import { Modal } from '@/ui/layout/modal/components/Modal'; const StyledFooter = styled(Modal.Footer)` height: 60px; diff --git a/front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx b/front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx index 18bf6f629d5a4..1fa871c206045 100644 --- a/front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx +++ b/front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx @@ -13,13 +13,13 @@ import debounce from 'lodash.debounce'; import { ReadonlyDeep } from 'type-fest'; import { SelectOption } from '@/spreadsheet-import/types'; -import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; -import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput'; -import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; -import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; -import { MenuItem } from '@/ui/menu-item/components/MenuItem'; -import { MenuItemSelect } from '@/ui/menu-item/components/MenuItemSelect'; -import { AppTooltip } from '@/ui/tooltip/AppTooltip'; +import { AppTooltip } from '@/ui/display/tooltip/AppTooltip'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useUpdateEffect } from '~/hooks/useUpdateEffect'; diff --git a/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx b/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx index a95bcb05f8921..d8593bb17b983 100644 --- a/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx +++ b/front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx @@ -2,10 +2,10 @@ import styled from '@emotion/styled'; import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; -import { IconButton } from '@/ui/button/components/IconButton'; -import { useDialog } from '@/ui/dialog/hooks/useDialog'; -import { IconX } from '@/ui/icon/index'; -import { useStepBar } from '@/ui/step-bar/hooks/useStepBar'; +import { IconX } from '@/ui/display/icon/index'; +import { useDialog } from '@/ui/feedback/dialog//hooks/useDialog'; +import { IconButton } from '@/ui/input/button/components/IconButton'; +import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar'; const StyledCloseButtonContainer = styled.div` align-items: center; diff --git a/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx b/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx index fb7a1ad1bce14..a6814e9c44221 100644 --- a/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx +++ b/front/src/modules/spreadsheet-import/components/ModalWrapper.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; -import { Modal } from '@/ui/modal/components/Modal'; +import { Modal } from '@/ui/layout/modal/components/Modal'; import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { ModalCloseButton } from './ModalCloseButton'; diff --git a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx index 63136e603acc4..46c5bd8fbce9e 100644 --- a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx @@ -11,9 +11,9 @@ import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableDat import { setColumn } from '@/spreadsheet-import/utils/setColumn'; import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn'; import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn'; -import { useDialog } from '@/ui/dialog/hooks/useDialog'; -import { Modal } from '@/ui/modal/components/Modal'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { useDialog } from '@/ui/feedback/dialog//hooks/useDialog'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; +import { Modal } from '@/ui/layout/modal/components/Modal'; import { ColumnGrid } from './components/ColumnGrid'; import { TemplateColumn } from './components/TemplateColumn'; diff --git a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx index 7c22740876688..858b498c6bc72 100644 --- a/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx @@ -11,7 +11,7 @@ import styled from '@emotion/styled'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { Fields } from '@/spreadsheet-import/types'; -import { IconChevronDown, IconForbid } from '@/ui/icon'; +import { IconChevronDown, IconForbid } from '@/ui/display/icon'; import { Column, Columns } from '../MatchColumnsStep'; import { ColumnType } from '../MatchColumnsStep'; diff --git a/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx b/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx index 126cfb30b4664..90b595e30bbfc 100644 --- a/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton'; import { Heading } from '@/spreadsheet-import/components/Heading'; import { RawData } from '@/spreadsheet-import/types'; -import { Modal } from '@/ui/modal/components/Modal'; +import { Modal } from '@/ui/layout/modal/components/Modal'; import { SelectHeaderTable } from './components/SelectHeaderTable'; diff --git a/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx b/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx index b07a40ca231ed..6739fa2ebe54d 100644 --- a/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx @@ -5,7 +5,7 @@ import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton'; import { Heading } from '@/spreadsheet-import/components/Heading'; import { Radio } from '@/ui/input/components/Radio'; import { RadioGroup } from '@/ui/input/components/RadioGroup'; -import { Modal } from '@/ui/modal/components/Modal'; +import { Modal } from '@/ui/layout/modal/components/Modal'; const StyledContent = styled(Modal.Content)` align-items: center; diff --git a/front/src/modules/spreadsheet-import/steps/components/Steps.tsx b/front/src/modules/spreadsheet-import/steps/components/Steps.tsx index 3700afbcb8704..5fc6dcacd87f2 100644 --- a/front/src/modules/spreadsheet-import/steps/components/Steps.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/Steps.tsx @@ -2,9 +2,9 @@ import styled from '@emotion/styled'; import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; -import { Modal } from '@/ui/modal/components/Modal'; -import { StepBar } from '@/ui/step-bar/components/StepBar'; -import { useStepBar } from '@/ui/step-bar/hooks/useStepBar'; +import { Modal } from '@/ui/layout/modal/components/Modal'; +import { StepBar } from '@/ui/navigation/step-bar/components/StepBar'; +import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar'; import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme'; import { UploadFlow } from './UploadFlow'; diff --git a/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx index ca75a08df90d1..a627c14da6a85 100644 --- a/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx @@ -7,9 +7,9 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre import { RawData } from '@/spreadsheet-import/types'; import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords'; import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook'; -import { Modal } from '@/ui/modal/components/Modal'; -import { CircularProgressBar } from '@/ui/progress-bar/components/CircularProgressBar'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { CircularProgressBar } from '@/ui/feedback/progress-bar/components/CircularProgressBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; +import { Modal } from '@/ui/layout/modal/components/Modal'; import { MatchColumnsStep } from './MatchColumnsStep/MatchColumnsStep'; import { SelectHeaderStep } from './SelectHeaderStep/SelectHeaderStep'; diff --git a/front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx index 619bde6355d36..f0cdc1bb43839 100644 --- a/front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'; import styled from '@emotion/styled'; import { WorkBook } from 'xlsx-ugnis'; -import { Modal } from '@/ui/modal/components/Modal'; +import { Modal } from '@/ui/layout/modal/components/Modal'; import { DropZone } from './components/DropZone'; diff --git a/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx index a2768d1140e9b..aa08114716113 100644 --- a/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx @@ -5,8 +5,8 @@ import * as XLSX from 'xlsx-ugnis'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync'; -import { MainButton } from '@/ui/button/components/MainButton'; -import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar'; +import { MainButton } from '@/ui/input/button/components/MainButton'; const StyledContainer = styled.div` align-items: center; diff --git a/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx index 7b6046e14c540..b65cc254e3e14 100644 --- a/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx @@ -3,7 +3,7 @@ import { createPortal } from 'react-dom'; import styled from '@emotion/styled'; import { Fields } from '@/spreadsheet-import/types'; -import { AppTooltip } from '@/ui/tooltip/AppTooltip'; +import { AppTooltip } from '@/ui/display/tooltip/AppTooltip'; const StyledHeaderContainer = styled.div` align-items: center; diff --git a/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index 6758f11995404..75085649f9f57 100644 --- a/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -8,11 +8,11 @@ import { Table } from '@/spreadsheet-import/components/Table'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { Data } from '@/spreadsheet-import/types'; import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; -import { Button } from '@/ui/button/components/Button'; -import { useDialog } from '@/ui/dialog/hooks/useDialog'; -import { IconTrash } from '@/ui/icon'; +import { IconTrash } from '@/ui/display/icon'; +import { useDialog } from '@/ui/feedback/dialog//hooks/useDialog'; +import { Button } from '@/ui/input/button/components/Button'; import { Toggle } from '@/ui/input/components/Toggle'; -import { Modal } from '@/ui/modal/components/Modal'; +import { Modal } from '@/ui/layout/modal/components/Modal'; import { generateColumns } from './components/columns'; import { Meta } from './types'; diff --git a/front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx index d6f37309519bf..0af8b7f4f2c70 100644 --- a/front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx @@ -4,10 +4,10 @@ import styled from '@emotion/styled'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import { Data, Fields } from '@/spreadsheet-import/types'; +import { AppTooltip } from '@/ui/display/tooltip/AppTooltip'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; import { TextInput } from '@/ui/input/components/TextInput'; import { Toggle } from '@/ui/input/components/Toggle'; -import { AppTooltip } from '@/ui/tooltip/AppTooltip'; import { Meta } from '../types'; diff --git a/front/src/modules/spreadsheet-import/types/index.ts b/front/src/modules/spreadsheet-import/types/index.ts index ffacca7c2ed7e..cadcf4c926a92 100644 --- a/front/src/modules/spreadsheet-import/types/index.ts +++ b/front/src/modules/spreadsheet-import/types/index.ts @@ -3,7 +3,7 @@ import { ReadonlyDeep } from 'type-fest'; import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { StepState } from '@/spreadsheet-import/steps/components/UploadFlow'; import { Meta } from '@/spreadsheet-import/steps/components/ValidationStep/types'; -import { IconComponent } from '@/ui/icon/types/IconComponent'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; export type SpreadsheetOptions = { // Is modal visible. diff --git a/front/src/modules/ui/field/components/FieldDisplay.tsx b/front/src/modules/ui/Data/Field/components/FieldDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/components/FieldDisplay.tsx rename to front/src/modules/ui/Data/Field/components/FieldDisplay.tsx diff --git a/front/src/modules/ui/field/components/FieldInput.tsx b/front/src/modules/ui/Data/Field/components/FieldInput.tsx similarity index 100% rename from front/src/modules/ui/field/components/FieldInput.tsx rename to front/src/modules/ui/Data/Field/components/FieldInput.tsx diff --git a/front/src/modules/ui/field/contexts/FieldContext.ts b/front/src/modules/ui/Data/Field/contexts/FieldContext.ts similarity index 100% rename from front/src/modules/ui/field/contexts/FieldContext.ts rename to front/src/modules/ui/Data/Field/contexts/FieldContext.ts diff --git a/front/src/modules/ui/field/hooks/useIsFieldEmpty.ts b/front/src/modules/ui/Data/Field/hooks/useIsFieldEmpty.ts similarity index 100% rename from front/src/modules/ui/field/hooks/useIsFieldEmpty.ts rename to front/src/modules/ui/Data/Field/hooks/useIsFieldEmpty.ts diff --git a/front/src/modules/ui/field/hooks/useIsFieldInputOnly.ts b/front/src/modules/ui/Data/Field/hooks/useIsFieldInputOnly.ts similarity index 100% rename from front/src/modules/ui/field/hooks/useIsFieldInputOnly.ts rename to front/src/modules/ui/Data/Field/hooks/useIsFieldInputOnly.ts diff --git a/front/src/modules/ui/field/hooks/usePersistField.ts b/front/src/modules/ui/Data/Field/hooks/usePersistField.ts similarity index 100% rename from front/src/modules/ui/field/hooks/usePersistField.ts rename to front/src/modules/ui/Data/Field/hooks/usePersistField.ts diff --git a/front/src/modules/ui/Data/Field/meta-types/__stories__/FieldContextProvider.tsx b/front/src/modules/ui/Data/Field/meta-types/__stories__/FieldContextProvider.tsx new file mode 100644 index 0000000000000..4282661637cf8 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/__stories__/FieldContextProvider.tsx @@ -0,0 +1,35 @@ +import { + FieldContext, + GenericFieldContextType, +} from '@/ui/data/field/contexts/FieldContext'; + +type FieldContextProviderProps = { + children: React.ReactNode; + fieldDefinition: GenericFieldContextType['fieldDefinition']; + entityId?: string; +}; + +export const FieldContextProvider = ({ + children, + fieldDefinition, + entityId, +}: FieldContextProviderProps) => { + return ( + [ + () => { + return; + }, + {}, + ], + }} + > + {children} + + ); +}; diff --git a/front/src/modules/ui/field/meta-types/display/components/ChipFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/ChipFieldDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/components/ChipFieldDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/components/ChipFieldDisplay.tsx diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/DateFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/DateFieldDisplay.tsx new file mode 100644 index 0000000000000..36d9b53e066bb --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/DateFieldDisplay.tsx @@ -0,0 +1,9 @@ +import { DateDisplay } from '@/ui/data/field/meta-types/display/content-display/components/DateDisplay'; + +import { useDateField } from '../../hooks/useDateField'; + +export const DateFieldDisplay = () => { + const { fieldValue } = useDateField(); + + return ; +}; diff --git a/front/src/modules/ui/field/meta-types/display/components/DoubleTextChipFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/DoubleTextChipFieldDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/components/DoubleTextChipFieldDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/components/DoubleTextChipFieldDisplay.tsx diff --git a/front/src/modules/ui/field/meta-types/display/components/DoubleTextFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/DoubleTextFieldDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/components/DoubleTextFieldDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/components/DoubleTextFieldDisplay.tsx diff --git a/front/src/modules/ui/field/meta-types/display/components/EmailFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/EmailFieldDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/components/EmailFieldDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/components/EmailFieldDisplay.tsx diff --git a/front/src/modules/ui/field/meta-types/display/components/MoneyFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/MoneyFieldDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/components/MoneyFieldDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/components/MoneyFieldDisplay.tsx diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/NumberFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/NumberFieldDisplay.tsx new file mode 100644 index 0000000000000..ae62090fbb7d8 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/NumberFieldDisplay.tsx @@ -0,0 +1,9 @@ +import { NumberDisplay } from '@/ui/data/field/meta-types/display/content-display/components/NumberDisplay'; + +import { useNumberField } from '../../hooks/useNumberField'; + +export const NumberFieldDisplay = () => { + const { fieldValue } = useNumberField(); + + return ; +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/PhoneFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/PhoneFieldDisplay.tsx new file mode 100644 index 0000000000000..fc5d486d3c40d --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/PhoneFieldDisplay.tsx @@ -0,0 +1,9 @@ +import { PhoneDisplay } from '@/ui/data/field/meta-types/display/content-display/components/PhoneDisplay'; + +import { usePhoneField } from '../../hooks/usePhoneField'; + +export const PhoneFieldDisplay = () => { + const { fieldValue } = usePhoneField(); + + return ; +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/RelationFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/RelationFieldDisplay.tsx new file mode 100644 index 0000000000000..2d1b705f05e01 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/RelationFieldDisplay.tsx @@ -0,0 +1,24 @@ +import { EntityChip } from '@/ui/display/chip/components/EntityChip'; + +import { useRelationField } from '../../hooks/useRelationField'; + +export const RelationFieldDisplay = () => { + const { fieldValue, fieldDefinition } = useRelationField(); + const { entityChipDisplayMapper } = fieldDefinition; + if (!entityChipDisplayMapper) { + throw new Error( + "Missing entityChipDisplayMapper in FieldContext. Please provide it in the FieldContextProvider's value prop.", + ); + } + const { name, pictureUrl, avatarType } = + entityChipDisplayMapper?.(fieldValue); + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/TextFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/TextFieldDisplay.tsx new file mode 100644 index 0000000000000..a801f3186142a --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/TextFieldDisplay.tsx @@ -0,0 +1,9 @@ +import { TextDisplay } from '@/ui/data/field/meta-types/display/content-display/components/TextDisplay'; + +import { useTextField } from '../../hooks/useTextField'; + +export const TextFieldDisplay = () => { + const { fieldValue } = useTextField(); + + return ; +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/URLFieldDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/URLFieldDisplay.tsx new file mode 100644 index 0000000000000..f7a7339e00524 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/URLFieldDisplay.tsx @@ -0,0 +1,9 @@ +import { URLDisplay } from '@/ui/data/field/meta-types/display/content-display/components/URLDisplay'; + +import { useURLField } from '../../hooks/useURLField'; + +export const URLFieldDisplay = () => { + const { fieldValue } = useURLField(); + + return ; +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..41712eb24aac3 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx @@ -0,0 +1,77 @@ +import { useEffect } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; + +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDateField } from '../../../hooks/useDateField'; +import { DateFieldDisplay } from '../DateFieldDisplay'; + +const formattedDate = new Date(); + +const DateFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useDateField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type DateFieldDisplayWithContextProps = { + value: string; + entityId?: string; +}; + +const DateFieldDisplayWithContext = ({ + value, + entityId, +}: DateFieldDisplayWithContextProps) => { + return ( + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/DateFieldDisplay', + component: DateFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: formattedDate.toISOString(), + }, +}; + +export const Elipsis: Story = { + args: { + value: formattedDate.toISOString(), + }, + argTypes: { + value: { control: false }, + }, + parameters: { + container: { + width: 50, + }, + }, + decorators: [ComponentDecorator], +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..06ac6d6cd8b5f --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; + +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDoubleTextField } from '../../../hooks/useDoubleTextField'; +import { DoubleTextFieldDisplay } from '../DoubleTextFieldDisplay'; // Import your component + +const DoubleTextFieldDisplayValueSetterEffect = ({ + firstValue, + secondValue, +}: { + firstValue: string; + secondValue: string; +}) => { + const { setFirstValue, setSecondValue } = useDoubleTextField(); + + useEffect(() => { + setFirstValue(firstValue); + setSecondValue(secondValue); + }, [setFirstValue, setSecondValue, firstValue, secondValue]); + + return <>; +}; + +type DoubleTextFieldDisplayWithContextProps = { + firstValue: string; + secondValue: string; + entityId?: string; +}; + +const DoubleTextFieldDisplayWithContext = ({ + firstValue, + secondValue, + entityId, +}: DoubleTextFieldDisplayWithContextProps) => { + return ( + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/DoubleTextFieldDisplay', + component: DoubleTextFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + firstValue: 'Lorem', + secondValue: 'ipsum', + }, +}; + +export const CustomValues: Story = { + args: { + firstValue: 'Lorem', + secondValue: 'ipsum', + }, +}; + +export const Elipsis: Story = { + args: { + firstValue: + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + secondValue: 'ipsum dolor sit amet, consectetur adipiscing elit.', + }, + argTypes: { + firstValue: { control: true }, + secondValue: { control: true }, + }, + parameters: { + container: { + width: 100, + }, + }, + decorators: [ComponentDecorator], +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..55be4c07375b3 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx @@ -0,0 +1,79 @@ +import { useEffect } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { Meta, StoryObj } from '@storybook/react'; + +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useEmailField } from '../../../hooks/useEmailField'; +import { EmailFieldDisplay } from '../EmailFieldDisplay'; + +const EmailFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useEmailField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type EmailFieldDisplayWithContextProps = { + value: string; + entityId?: string; +}; + +const EmailFieldDisplayWithContext = ({ + value, + entityId, +}: EmailFieldDisplayWithContextProps) => { + return ( + + + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/EmailFieldDisplay', + component: EmailFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 'Test@Test.test', + }, +}; + +export const Elipsis: Story = { + args: { + value: 'Test@Test.test', + }, + argTypes: { + value: { control: false }, + }, + parameters: { + container: { + width: 50, + }, + }, + decorators: [ComponentDecorator], +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..a4b657d9269bd --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx @@ -0,0 +1,111 @@ +import { useEffect } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { v4 } from 'uuid'; + +import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { CatalogStory } from '~/testing/types'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useMoneyField } from '../../../hooks/useMoneyField'; +import { MoneyFieldDisplay } from '../MoneyFieldDisplay'; + +const MoneyFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useMoneyField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type MoneyFieldDisplayWithContextProps = { + value: number; + entityId?: string; +}; + +const MoneyFieldDisplayWithContext = ({ + value, + entityId, +}: MoneyFieldDisplayWithContextProps) => { + return ( + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/MoneyFieldDisplay', + component: MoneyFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 100, + }, +}; + +export const Elipsis: Story = { + args: { + value: 1e100, + }, + argTypes: { + value: { control: false }, + }, + parameters: { + container: { + width: 100, + }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = + { + argTypes: { + value: { control: false }, + }, + parameters: { + catalog: { + dimensions: [ + { + name: 'currency', + values: ['$'] satisfies string[], + props: (_value: string) => ({}), + }, + { + name: 'value', + values: [ + 100, 1000, -1000, 1e10, 1.357802, -1.283, 0, + ] satisfies number[], + props: (value: number) => ({ value, entityId: v4() }), + }, + ], + options: { + elementContainer: { + width: 100, + }, + }, + }, + }, + decorators: [CatalogDecorator], + }; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..99a3076c03499 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx @@ -0,0 +1,108 @@ +import { useEffect } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { v4 } from 'uuid'; + +import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { CatalogStory } from '~/testing/types'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useNumberField } from '../../../hooks/useNumberField'; +import { NumberFieldDisplay } from '../NumberFieldDisplay'; + +const NumberFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useNumberField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type NumberFieldDisplayWithContextProps = { + value: number; + entityId?: string; +}; + +const NumberFieldDisplayWithContext = ({ + value, + entityId, +}: NumberFieldDisplayWithContextProps) => { + return ( + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/NumberFieldDisplay', + component: NumberFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 100, + }, +}; + +export const Elipsis: Story = { + args: { + value: 1e100, + }, + argTypes: { + value: { control: false }, + }, + parameters: { + container: { + width: 100, + }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory< + Story, + typeof NumberFieldDisplayWithContext +> = { + argTypes: { + value: { control: false }, + }, + parameters: { + catalog: { + dimensions: [ + { + name: 'value', + values: [ + 100, 1000, -1000, 1e10, 1.357802, -1.283, 0, + ] satisfies number[], + props: (value: number) => ({ value, entityId: v4() }), + }, + ], + options: { + elementContainer: { + width: 100, + }, + }, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..d967b48f2eecd --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx @@ -0,0 +1,79 @@ +import { useEffect } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { Meta, StoryObj } from '@storybook/react'; + +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { usePhoneField } from '../../../hooks/usePhoneField'; +import { PhoneFieldDisplay } from '../PhoneFieldDisplay'; + +const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = usePhoneField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type PhoneFieldDisplayWithContextProps = { + value: string; + entityId?: string; +}; + +const PhoneFieldDisplayWithContext = ({ + value, + entityId, +}: PhoneFieldDisplayWithContextProps) => { + return ( + + + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/PhoneFieldDisplay', + component: PhoneFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: '362763872687362', + }, +}; + +export const Elipsis: Story = { + args: { + value: '362763872687362', + }, + argTypes: { + value: { control: false }, + }, + parameters: { + container: { + width: 50, + }, + }, + decorators: [ComponentDecorator], +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..52832339f0676 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx @@ -0,0 +1,110 @@ +import { useEffect } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { v4 } from 'uuid'; + +import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { CatalogStory } from '~/testing/types'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useTextField } from '../../../hooks/useTextField'; +import { TextFieldDisplay } from '../TextFieldDisplay'; + +const TextFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useTextField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type TextFieldDisplayWithContextProps = { + value: string; + entityId?: string; +}; + +const TextFieldDisplayWithContext = ({ + value, + entityId, +}: TextFieldDisplayWithContextProps) => { + return ( + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/TextFieldDisplay', + component: TextFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 'Lorem ipsum', + }, +}; + +export const Elipsis: Story = { + args: { + value: + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.', + }, + argTypes: { + value: { control: false }, + }, + parameters: { + container: { + width: 100, + }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = + { + argTypes: { + value: { control: false }, + }, + parameters: { + catalog: { + dimensions: [ + { + name: 'value', + values: [ + 'Hello world', + 'Test', + '1234567890', + ' ', + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.', + ] satisfies string[], + props: (value: string) => ({ value, entityId: v4() }), + }, + ], + options: { + elementContainer: { + width: 100, + }, + }, + }, + }, + decorators: [CatalogDecorator], + }; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx new file mode 100644 index 0000000000000..057d144cf712d --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx @@ -0,0 +1,79 @@ +import { useEffect } from 'react'; +import { MemoryRouter } from 'react-router'; +import { Meta, StoryObj } from '@storybook/react'; + +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useURLField } from '../../../hooks/useURLField'; +import { URLFieldDisplay } from '../URLFieldDisplay'; + +const URLFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useURLField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type URLFieldDisplayWithContextProps = { + value: string; + entityId?: string; +}; + +const URLFieldDisplayWithContext = ({ + value, + entityId, +}: URLFieldDisplayWithContextProps) => { + return ( + + + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/display/URLFieldDisplay', + component: URLFieldDisplayWithContext, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 'https://github.com/GitStartHQ', + }, +}; + +export const Elipsis: Story = { + args: { + value: 'https://www.instagram.com/gitstart/', + }, + argTypes: { + value: { control: true }, + }, + parameters: { + container: { + width: 200, + }, + }, + decorators: [ComponentDecorator], +}; diff --git a/front/src/modules/ui/field/meta-types/display/content-display/components/ChipDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/ChipDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/content-display/components/ChipDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/content-display/components/ChipDisplay.tsx diff --git a/front/src/modules/ui/field/meta-types/display/content-display/components/DateDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/DateDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/content-display/components/DateDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/content-display/components/DateDisplay.tsx diff --git a/front/src/modules/ui/field/meta-types/display/content-display/components/DoubleTextDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/DoubleTextDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/content-display/components/DoubleTextDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/content-display/components/DoubleTextDisplay.tsx diff --git a/front/src/modules/ui/field/meta-types/display/content-display/components/EllipsisDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/EllipsisDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/content-display/components/EllipsisDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/content-display/components/EllipsisDisplay.tsx diff --git a/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/EmailDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/EmailDisplay.tsx new file mode 100644 index 0000000000000..f393d1a6e1032 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/EmailDisplay.tsx @@ -0,0 +1,31 @@ +import { MouseEvent } from 'react'; + +import { ContactLink } from '@/ui/navigation/link/components/ContactLink'; + +import { EllipsisDisplay } from './EllipsisDisplay'; + +const validateEmail = (email: string) => { + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailPattern.test(email.trim()); +}; + +type EmailDisplayProps = { + value: string | null; +}; + +export const EmailDisplay = ({ value }: EmailDisplayProps) => ( + + {value && validateEmail(value) ? ( + ) => { + event.stopPropagation(); + }} + > + {value} + + ) : ( + {value} + )} + +); diff --git a/front/src/modules/ui/field/meta-types/display/content-display/components/MoneyDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/MoneyDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/content-display/components/MoneyDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/content-display/components/MoneyDisplay.tsx diff --git a/front/src/modules/ui/field/meta-types/display/content-display/components/NumberDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/NumberDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/content-display/components/NumberDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/content-display/components/NumberDisplay.tsx diff --git a/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/PhoneDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/PhoneDisplay.tsx new file mode 100644 index 0000000000000..7ad5b97ecbf7c --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/PhoneDisplay.tsx @@ -0,0 +1,27 @@ +import { MouseEvent } from 'react'; +import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'; + +import { ContactLink } from '@/ui/navigation/link/components/ContactLink'; + +import { EllipsisDisplay } from './EllipsisDisplay'; + +type PhoneDisplayProps = { + value: string | null; +}; + +export const PhoneDisplay = ({ value }: PhoneDisplayProps) => ( + + {value && isValidPhoneNumber(value) ? ( + ) => { + event.stopPropagation(); + }} + > + {parsePhoneNumber(value, 'FR')?.formatInternational() || value} + + ) : ( + {value} + )} + +); diff --git a/front/src/modules/ui/field/meta-types/display/content-display/components/TextDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/TextDisplay.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/display/content-display/components/TextDisplay.tsx rename to front/src/modules/ui/Data/Field/meta-types/display/content-display/components/TextDisplay.tsx diff --git a/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/URLDisplay.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/URLDisplay.tsx new file mode 100644 index 0000000000000..b76662c716187 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/URLDisplay.tsx @@ -0,0 +1,72 @@ +import { MouseEvent } from 'react'; +import styled from '@emotion/styled'; + +import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink'; +import { + LinkType, + SocialLink, +} from '@/ui/navigation/link/components/SocialLink'; + +import { EllipsisDisplay } from './EllipsisDisplay'; + +const StyledRawLink = styled(RoundedLink)` + overflow: hidden; + + a { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +`; + +type URLDisplayProps = { + value: string | null; +}; + +const checkUrlType = (url: string) => { + if ( + /^(http|https):\/\/(?:www\.)?linkedin.com(\w+:{0,1}\w*@)?(\S+)(:([0-9])+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test( + url, + ) + ) { + return LinkType.LinkedIn; + } + if (url.match(/^((http|https):\/\/)?(?:www\.)?twitter\.com\/(\w+)?/i)) { + return LinkType.Twitter; + } + + return LinkType.Url; +}; + +export const URLDisplay = ({ value }: URLDisplayProps) => { + const handleClick = (event: MouseEvent) => { + event.stopPropagation(); + }; + + const absoluteUrl = value + ? value.startsWith('http') + ? value + : 'https://' + value + : ''; + + const displayedValue = value ?? ''; + + const type = checkUrlType(absoluteUrl); + + if (type === LinkType.LinkedIn || type === LinkType.Twitter) { + return ( + + + {displayedValue} + + + ); + } + return ( + + + {displayedValue} + + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/__stories__/PhoneInputDisplay.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/__stories__/PhoneInputDisplay.stories.tsx new file mode 100644 index 0000000000000..d78bb99f47e64 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/display/content-display/components/__stories__/PhoneInputDisplay.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; + +import { PhoneDisplay } from '../PhoneDisplay'; // Adjust the import path as needed + +const meta: Meta = { + title: 'UI/input/PhoneInputDisplay', + component: PhoneDisplay, + decorators: [ComponentWithRouterDecorator], + args: { + value: '+33788901234', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/front/src/modules/ui/field/meta-types/hooks/useBooleanField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useBooleanField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useBooleanField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useBooleanField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useChipField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useChipField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useChipField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useChipField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useDateField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useDateField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useDateField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useDateField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useDoubleTextChipField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useDoubleTextChipField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useDoubleTextChipField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useDoubleTextChipField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useDoubleTextField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useDoubleTextField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useDoubleTextField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useDoubleTextField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useEmailField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useEmailField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useEmailField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useEmailField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useMoneyField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useMoneyField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useMoneyField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useMoneyField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useNumberField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useNumberField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useNumberField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useNumberField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/usePhoneField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/usePhoneField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/usePhoneField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/usePhoneField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useProbabilityField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useProbabilityField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useProbabilityField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useProbabilityField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useRelationField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useRelationField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useRelationField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useRelationField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useTextField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useTextField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useTextField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useTextField.ts diff --git a/front/src/modules/ui/field/meta-types/hooks/useURLField.ts b/front/src/modules/ui/Data/Field/meta-types/hooks/useURLField.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/hooks/useURLField.ts rename to front/src/modules/ui/Data/Field/meta-types/hooks/useURLField.ts diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/BooleanFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/BooleanFieldInput.tsx new file mode 100644 index 0000000000000..003a2f2ca311f --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/BooleanFieldInput.tsx @@ -0,0 +1,32 @@ +import { BooleanInput } from '@/ui/data/field/meta-types/input/components/internal/BooleanInput'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useBooleanField } from '../../hooks/useBooleanField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type BooleanFieldInputProps = { + onSubmit?: FieldInputEvent; + testId?: string; +}; + +export const BooleanFieldInput = ({ + onSubmit, + testId, +}: BooleanFieldInputProps) => { + const { fieldValue } = useBooleanField(); + + const persistField = usePersistField(); + + const handleToggle = (newValue: boolean) => { + onSubmit?.(() => persistField(newValue)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/ChipFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/ChipFieldInput.tsx new file mode 100644 index 0000000000000..232a96290ffbd --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/ChipFieldInput.tsx @@ -0,0 +1,63 @@ +import { TextInput } from '@/ui/data/field/meta-types/input/components/internal/TextInput'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useChipField } from '../../hooks/useChipField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type ChipFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const ChipFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: ChipFieldInputProps) => { + const { fieldDefinition, contentFieldValue, hotkeyScope } = useChipField(); + + const persistField = usePersistField(); + + const handleEnter = (newText: string) => { + onEnter?.(() => persistField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => persistField(newText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => persistField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => persistField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => persistField(newText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/DateFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/DateFieldInput.tsx new file mode 100644 index 0000000000000..70890f8bd12f8 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/DateFieldInput.tsx @@ -0,0 +1,62 @@ +import { DateInput } from '@/ui/data/field/meta-types/input/components/internal/DateInput'; +import { Nullable } from '~/types/Nullable'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useDateField } from '../../hooks/useDateField'; + +export type FieldInputEvent = (persist: () => void) => void; + +export type DateFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const DateFieldInput = ({ + onEnter, + onEscape, + onClickOutside, +}: DateFieldInputProps) => { + const { fieldValue, hotkeyScope } = useDateField(); + + const persistField = usePersistField(); + + const persistDate = (newDate: Nullable) => { + if (!newDate) { + persistField(''); + } else { + const newDateISO = newDate?.toISOString(); + + persistField(newDateISO); + } + }; + + const handleEnter = (newDate: Nullable) => { + onEnter?.(() => persistDate(newDate)); + }; + + const handleEscape = (newDate: Nullable) => { + onEscape?.(() => persistDate(newDate)); + }; + + const handleClickOutside = ( + _event: MouseEvent | TouchEvent, + newDate: Nullable, + ) => { + onClickOutside?.(() => persistDate(newDate)); + }; + + const dateValue = fieldValue ? new Date(fieldValue) : null; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/DoubleTextChipFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/DoubleTextChipFieldInput.tsx new file mode 100644 index 0000000000000..fa5551645ce47 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/DoubleTextChipFieldInput.tsx @@ -0,0 +1,66 @@ +import { DoubleTextInput } from '@/ui/data/field/meta-types/input/components/internal/DoubleTextInput'; +import { FieldDoubleText } from '@/ui/data/field/types/FieldDoubleText'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type DoubleTextChipFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const DoubleTextChipFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: DoubleTextChipFieldInputProps) => { + const { fieldDefinition, firstValue, secondValue, hotkeyScope } = + useDoubleTextChipField(); + + const persistField = usePersistField(); + + const handleEnter = (newDoubleText: FieldDoubleText) => { + onEnter?.(() => persistField(newDoubleText)); + }; + + const handleEscape = (newDoubleText: FieldDoubleText) => { + onEscape?.(() => persistField(newDoubleText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newDoubleText: FieldDoubleText, + ) => { + onClickOutside?.(() => persistField(newDoubleText)); + }; + + const handleTab = (newDoubleText: FieldDoubleText) => { + onTab?.(() => persistField(newDoubleText)); + }; + + const handleShiftTab = (newDoubleText: FieldDoubleText) => { + onShiftTab?.(() => persistField(newDoubleText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/DoubleTextFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/DoubleTextFieldInput.tsx new file mode 100644 index 0000000000000..08b29c634df7f --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/DoubleTextFieldInput.tsx @@ -0,0 +1,66 @@ +import { DoubleTextInput } from '@/ui/data/field/meta-types/input/components/internal/DoubleTextInput'; +import { FieldDoubleText } from '@/ui/data/field/types/FieldDoubleText'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useDoubleTextField } from '../../hooks/useDoubleTextField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type DoubleTextFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const DoubleTextFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: DoubleTextFieldInputProps) => { + const { fieldDefinition, firstValue, secondValue, hotkeyScope } = + useDoubleTextField(); + + const persistField = usePersistField(); + + const handleEnter = (newDoubleText: FieldDoubleText) => { + onEnter?.(() => persistField(newDoubleText)); + }; + + const handleEscape = (newDoubleText: FieldDoubleText) => { + onEscape?.(() => persistField(newDoubleText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newDoubleText: FieldDoubleText, + ) => { + onClickOutside?.(() => persistField(newDoubleText)); + }; + + const handleTab = (newDoubleText: FieldDoubleText) => { + onTab?.(() => persistField(newDoubleText)); + }; + + const handleShiftTab = (newDoubleText: FieldDoubleText) => { + onShiftTab?.(() => persistField(newDoubleText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/EmailFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/EmailFieldInput.tsx new file mode 100644 index 0000000000000..4c1042db036b8 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/EmailFieldInput.tsx @@ -0,0 +1,63 @@ +import { TextInput } from '@/ui/data/field/meta-types/input/components/internal/TextInput'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useEmailField } from '../../hooks/useEmailField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type EmailFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const EmailFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: EmailFieldInputProps) => { + const { fieldDefinition, fieldValue, hotkeyScope } = useEmailField(); + + const persistField = usePersistField(); + + const handleEnter = (newText: string) => { + onEnter?.(() => persistField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => persistField(newText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => persistField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => persistField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => persistField(newText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/MoneyFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/MoneyFieldInput.tsx new file mode 100644 index 0000000000000..18265dd6f0886 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/MoneyFieldInput.tsx @@ -0,0 +1,61 @@ +import { TextInput } from '@/ui/data/field/meta-types/input/components/internal/TextInput'; + +import { useMoneyField } from '../../hooks/useMoneyField'; + +export type FieldInputEvent = (persist: () => void) => void; + +export type MoneyFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const MoneyFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: MoneyFieldInputProps) => { + const { fieldDefinition, fieldValue, hotkeyScope, persistMoneyField } = + useMoneyField(); + + const handleEnter = (newText: string) => { + onEnter?.(() => persistMoneyField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => persistMoneyField(newText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => persistMoneyField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => persistMoneyField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => persistMoneyField(newText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/NumberFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/NumberFieldInput.tsx new file mode 100644 index 0000000000000..81e5660215550 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/NumberFieldInput.tsx @@ -0,0 +1,61 @@ +import { TextInput } from '@/ui/data/field/meta-types/input/components/internal/TextInput'; + +import { useNumberField } from '../../hooks/useNumberField'; + +export type FieldInputEvent = (persist: () => void) => void; + +export type NumberFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const NumberFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: NumberFieldInputProps) => { + const { fieldDefinition, fieldValue, hotkeyScope, persistNumberField } = + useNumberField(); + + const handleEnter = (newText: string) => { + onEnter?.(() => persistNumberField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => persistNumberField(newText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => persistNumberField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => persistNumberField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => persistNumberField(newText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/PhoneFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/PhoneFieldInput.tsx new file mode 100644 index 0000000000000..2b4b9cd8cea2d --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/PhoneFieldInput.tsx @@ -0,0 +1,61 @@ +import { PhoneInput } from '@/ui/data/field/meta-types/input/components/internal/PhoneInput'; + +import { usePhoneField } from '../../hooks/usePhoneField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type PhoneFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const PhoneFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: PhoneFieldInputProps) => { + const { fieldDefinition, fieldValue, hotkeyScope, persistPhoneField } = + usePhoneField(); + + const handleEnter = (newText: string) => { + onEnter?.(() => persistPhoneField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => persistPhoneField(newText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => persistPhoneField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => persistPhoneField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => persistPhoneField(newText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/ProbabilityFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/ProbabilityFieldInput.tsx new file mode 100644 index 0000000000000..0a82fc7dbf4f7 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/ProbabilityFieldInput.tsx @@ -0,0 +1,29 @@ +import { ProbabilityInput } from '@/ui/data/field/meta-types/input/components/internal/ProbabilityInput'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useProbabilityField } from '../../hooks/useProbabilityField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type ProbabilityFieldInputProps = { + onSubmit?: FieldInputEvent; +}; + +export const ProbabilityFieldInput = ({ + onSubmit, +}: ProbabilityFieldInputProps) => { + const { probabilityIndex } = useProbabilityField(); + + const persistField = usePersistField(); + + const handleChange = (newValue: number) => { + onSubmit?.(() => persistField(newValue)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/RelationFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/RelationFieldInput.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/input/components/RelationFieldInput.tsx rename to front/src/modules/ui/Data/Field/meta-types/input/components/RelationFieldInput.tsx diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/TextFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/TextFieldInput.tsx new file mode 100644 index 0000000000000..922746dc8c3d1 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/TextFieldInput.tsx @@ -0,0 +1,63 @@ +import { TextInput } from '@/ui/data/field/meta-types/input/components/internal/TextInput'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useTextField } from '../../hooks/useTextField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type TextFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const TextFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: TextFieldInputProps) => { + const { fieldDefinition, fieldValue, hotkeyScope } = useTextField(); + + const persistField = usePersistField(); + + const handleEnter = (newText: string) => { + onEnter?.(() => persistField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => persistField(newText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => persistField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => persistField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => persistField(newText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/URLFieldInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/URLFieldInput.tsx new file mode 100644 index 0000000000000..50f4c5d3a682c --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/URLFieldInput.tsx @@ -0,0 +1,61 @@ +import { TextInput } from '@/ui/data/field/meta-types/input/components/internal/TextInput'; + +import { useURLField } from '../../hooks/useURLField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type URLFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const URLFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: URLFieldInputProps) => { + const { fieldDefinition, fieldValue, hotkeyScope, persistURLField } = + useURLField(); + + const handleEnter = (newText: string) => { + onEnter?.(() => persistURLField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => persistURLField(newText)); + }; + + const handleClickOutside = ( + event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => persistURLField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => persistURLField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => persistURLField(newText)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx new file mode 100644 index 0000000000000..0e6957c5c47fd --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx @@ -0,0 +1,100 @@ +import { useEffect } from 'react'; +import { jest } from '@storybook/jest'; +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useBooleanField } from '../../../hooks/useBooleanField'; +import { + BooleanFieldInput, + BooleanFieldInputProps, +} from '../BooleanFieldInput'; + +const BooleanFieldValueSetterEffect = ({ value }: { value: boolean }) => { + const { setFieldValue } = useBooleanField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type BooleanFieldInputWithContextProps = BooleanFieldInputProps & { + value: boolean; + entityId?: string; +}; + +const BooleanFieldInputWithContext = ({ + value, + entityId, + onSubmit, +}: BooleanFieldInputWithContextProps) => { + return ( + + + + + ); +}; + +const meta: Meta = { + title: 'UI/field/input/BooleanFieldInput', + component: BooleanFieldInputWithContext, + args: { + value: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +const submitJestFn = jest.fn(); + +export const Default: Story = {}; + +export const Toggle: Story = { + args: { + onSubmit: submitJestFn, + }, + argTypes: { + onSubmit: { + control: false, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const input = canvas.getByTestId('boolean-field-input'); + + const trueText = await within(input).findByText('True'); + + await expect(trueText).toBeInTheDocument(); + + await expect(submitJestFn).toHaveBeenCalledTimes(0); + + await userEvent.click(input); + + await expect(input).toHaveTextContent('False'); + + await expect(submitJestFn).toHaveBeenCalledTimes(1); + + await userEvent.click(input); + + await expect(input).toHaveTextContent('True'); + + await expect(submitJestFn).toHaveBeenCalledTimes(2); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx new file mode 100644 index 0000000000000..717060d3565a7 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx @@ -0,0 +1,177 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useChipField } from '../../../hooks/useChipField'; +import { ChipFieldInput, ChipFieldInputProps } from '../ChipFieldInput'; + +const ChipFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setContentFieldValue } = useChipField(); + + useEffect(() => { + setContentFieldValue(value); + }, [setContentFieldValue, value]); + + return <>; +}; + +type ChipFieldInputWithContextProps = ChipFieldInputProps & { + value: string; + entityId?: string; +}; + +const ChipFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: ChipFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/ChipFieldInput', + component: ChipFieldInputWithContext, + args: { + value: 'chip', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + parameters: { + clearMocks: true, + }, + decorators: [clearMocksDecorator], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx new file mode 100644 index 0000000000000..059999591d008 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx @@ -0,0 +1,131 @@ +import { useEffect } from 'react'; +import { expect } from '@storybook/jest'; +import { jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDateField } from '../../../hooks/useDateField'; +import { DateFieldInput, DateFieldInputProps } from '../DateFieldInput'; + +const formattedDate = new Date(); + +const DateFieldValueSetterEffect = ({ value }: { value: Date }) => { + const { setFieldValue } = useDateField(); + + useEffect(() => { + setFieldValue(value.toISOString()); + }, [setFieldValue, value]); + + return <>; +}; + +type DateFieldInputWithContextProps = DateFieldInputProps & { + value: Date; + entityId?: string; +}; + +const DateFieldInputWithContext = ({ + value, + entityId, + onEscape, + onEnter, + onClickOutside, +}: DateFieldInputWithContextProps) => { + const setHotkeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotkeyScope('hotkey-scope'); + }, [setHotkeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const escapeJestFn = jest.fn(); +const enterJestFn = jest.fn(); +const clickOutsideJestFn = jest.fn(); + +const meta: Meta = { + title: 'UI/field/input/DateFieldInput', + component: DateFieldInputWithContext, + args: { + value: formattedDate, + onEscape: escapeJestFn, + onEnter: enterJestFn, + onClickOutside: clickOutsideJestFn, + }, + argTypes: { + onEscape: { + control: false, + }, + onEnter: { + control: false, + }, + onClickOutside: { + control: false, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + await userEvent.click(emptyDiv); + + await expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const Escape: Story = { + play: async () => { + await expect(escapeJestFn).toHaveBeenCalledTimes(0); + + await userEvent.keyboard('{esc}'); + + await expect(escapeJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const Enter: Story = { + play: async () => { + await expect(enterJestFn).toHaveBeenCalledTimes(0); + + await userEvent.keyboard('{enter}'); + + await expect(enterJestFn).toHaveBeenCalledTimes(1); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx new file mode 100644 index 0000000000000..cbd083a17ca78 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx @@ -0,0 +1,194 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDoubleTextChipField } from '../../../hooks/useDoubleTextChipField'; +import { + DoubleTextChipFieldInput, + DoubleTextChipFieldInputProps, +} from '../DoubleTextChipFieldInput'; + +const DoubleTextChipFieldValueSetterEffect = ({ + firstValue, + secondValue, +}: { + firstValue: string; + secondValue: string; +}) => { + const { setFirstValue, setSecondValue } = useDoubleTextChipField(); + + useEffect(() => { + setFirstValue(firstValue); + setSecondValue(secondValue); + }, [firstValue, secondValue, setFirstValue, setSecondValue]); + + return <>; +}; + +type DoubleTextChipFieldInputWithContextProps = + DoubleTextChipFieldInputProps & { + firstValue: string; + secondValue: string; + entityId?: string; + }; + +const DoubleTextChipFieldInputWithContext = ({ + entityId, + firstValue, + secondValue, + onClickOutside, + onEnter, + onEscape, + onTab, + onShiftTab, +}: DoubleTextChipFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/DoubleTextChipFieldInput', + component: DoubleTextChipFieldInputWithContext, + args: { + firstValue: 'first value', + secondValue: 'second value', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + parameters: { + clearMocks: true, + }, + decorators: [clearMocksDecorator], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx new file mode 100644 index 0000000000000..191af8f1faef4 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx @@ -0,0 +1,191 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useDoubleTextField } from '../../../hooks/useDoubleTextField'; +import { + DoubleTextFieldInput, + DoubleTextFieldInputProps, +} from '../DoubleTextFieldInput'; + +const DoubleTextFieldValueSetterEffect = ({ + firstValue, + secondValue, +}: { + firstValue: string; + secondValue: string; +}) => { + const { setFirstValue, setSecondValue } = useDoubleTextField(); + + useEffect(() => { + setFirstValue(firstValue); + setSecondValue(secondValue); + }, [firstValue, secondValue, setFirstValue, setSecondValue]); + + return <>; +}; + +type DoubleTextFieldInputWithContextProps = DoubleTextFieldInputProps & { + firstValue: string; + secondValue: string; + entityId?: string; +}; + +const DoubleTextFieldInputWithContext = ({ + entityId, + firstValue, + secondValue, + onClickOutside, + onEnter, + onEscape, + onTab, + onShiftTab, +}: DoubleTextFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/DoubleTextFieldInput', + component: DoubleTextFieldInputWithContext, + args: { + firstValue: 'first value', + secondValue: 'second value', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = await canvas.findByTestId( + 'data-field-input-click-outside-div', + ); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx new file mode 100644 index 0000000000000..1f026b074339f --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx @@ -0,0 +1,174 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useEmailField } from '../../../hooks/useEmailField'; +import { EmailFieldInput, EmailFieldInputProps } from '../EmailFieldInput'; + +const EmailFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useEmailField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type EmailFieldInputWithContextProps = EmailFieldInputProps & { + value: string; + entityId?: string; +}; + +const EmailFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: EmailFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/EmailFieldInput', + component: EmailFieldInputWithContext, + args: { + value: 'username@email.com', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx new file mode 100644 index 0000000000000..cf241e8a33969 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx @@ -0,0 +1,175 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useMoneyField } from '../../../hooks/useMoneyField'; +import { MoneyFieldInput, MoneyFieldInputProps } from '../MoneyFieldInput'; + +const MoneyFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useMoneyField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type MoneyFieldInputWithContextProps = MoneyFieldInputProps & { + value: number; + entityId?: string; +}; + +const MoneyFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: MoneyFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/MoneyFieldInput', + component: MoneyFieldInputWithContext, + args: { + value: 1000, + isPositive: true, + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx new file mode 100644 index 0000000000000..3bba51409142b --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -0,0 +1,175 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useNumberField } from '../../../hooks/useNumberField'; +import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput'; + +const NumberFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useNumberField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type NumberFieldInputWithContextProps = NumberFieldInputProps & { + value: number; + entityId?: string; +}; + +const NumberFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: NumberFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/NumberFieldInput', + component: NumberFieldInputWithContext, + args: { + value: 1000, + isPositive: true, + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx new file mode 100644 index 0000000000000..ee231ee0b39cf --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx @@ -0,0 +1,175 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { usePhoneField } from '../../../hooks/usePhoneField'; +import { PhoneFieldInput, PhoneFieldInputProps } from '../PhoneFieldInput'; + +const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = usePhoneField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type PhoneFieldInputWithContextProps = PhoneFieldInputProps & { + value: string; + entityId?: string; +}; + +const PhoneFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: PhoneFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/PhoneFieldInput', + component: PhoneFieldInputWithContext, + args: { + value: '+1-12-123-456', + isPositive: true, + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx new file mode 100644 index 0000000000000..8e36be2effc74 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx @@ -0,0 +1,106 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useProbabilityField } from '../../../hooks/useProbabilityField'; +import { + ProbabilityFieldInput, + ProbabilityFieldInputProps, +} from '../ProbabilityFieldInput'; + +const ProbabilityFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useProbabilityField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type ProbabilityFieldInputWithContextProps = ProbabilityFieldInputProps & { + value: number; + entityId?: string; +}; + +const ProbabilityFieldInputWithContext = ({ + entityId, + value, + onSubmit, +}: ProbabilityFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( + + + + + ); +}; + +const submitJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + submitJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/ProbabilityFieldInput', + component: ProbabilityFieldInputWithContext, + args: { + value: 25, + isPositive: true, + onSubmit: submitJestFn, + }, + argTypes: { + onSubmit: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Submit: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(submitJestFn).toHaveBeenCalledTimes(0); + + const item = (await canvas.findByText('25%'))?.nextElementSibling + ?.firstElementChild; + + if (item) { + userEvent.click(item); + } + + expect(submitJestFn).toHaveBeenCalledTimes(1); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx new file mode 100644 index 0000000000000..80fb422ba50b6 --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx @@ -0,0 +1,134 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useRelationField } from '../../../hooks/useRelationField'; +import { + RelationFieldInput, + RelationFieldInputProps, +} from '../RelationFieldInput'; + +const RelationFieldValueSetterEffect = ({ value }: { value: number }) => { + const { setFieldValue } = useRelationField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type RelationFieldInputWithContextProps = RelationFieldInputProps & { + value: number; + entityId?: string; +}; + +const RelationFieldInputWithContext = ({ + entityId, + value, + onSubmit, + onCancel, +}: RelationFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const submitJestFn = jest.fn(); +const cancelJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + submitJestFn.mockClear(); + cancelJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/RelationFieldInput', + component: RelationFieldInputWithContext, + args: { + useEditButton: true, + onSubmit: submitJestFn, + onCancel: cancelJestFn, + }, + argTypes: { + onSubmit: { control: false }, + onCancel: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + msw: graphqlMocks, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + decorators: [ComponentWithRecoilScopeDecorator], +}; + +export const Submit: Story = { + decorators: [ComponentWithRecoilScopeDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(submitJestFn).toHaveBeenCalledTimes(0); + + const item = await canvas.findByText('Jane Doe'); + + userEvent.click(item); + + expect(submitJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const Cancel: Story = { + decorators: [ComponentWithRecoilScopeDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(cancelJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(cancelJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx new file mode 100644 index 0000000000000..120cc18ee3e2a --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx @@ -0,0 +1,174 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useTextField } from '../../../hooks/useTextField'; +import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput'; + +const TextFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useTextField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type TextFieldInputWithContextProps = TextFieldInputProps & { + value: string; + entityId?: string; +}; + +const TextFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: TextFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/TextFieldInput', + component: TextFieldInputWithContext, + args: { + value: 'text', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx new file mode 100644 index 0000000000000..d22865410314f --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx @@ -0,0 +1,174 @@ +import { useEffect } from 'react'; +import { expect, jest } from '@storybook/jest'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; + +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { useURLField } from '../../../hooks/useURLField'; +import { URLFieldInput, URLFieldInputProps } from '../URLFieldInput'; + +const URLFieldValueSetterEffect = ({ value }: { value: string }) => { + const { setFieldValue } = useURLField(); + + useEffect(() => { + setFieldValue(value); + }, [setFieldValue, value]); + + return <>; +}; + +type URLFieldInputWithContextProps = URLFieldInputProps & { + value: string; + entityId?: string; +}; + +const URLFieldInputWithContext = ({ + entityId, + value, + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: URLFieldInputWithContextProps) => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; + +const enterJestFn = jest.fn(); +const escapeJestfn = jest.fn(); +const clickOutsideJestFn = jest.fn(); +const tabJestFn = jest.fn(); +const shiftTabJestFn = jest.fn(); + +const clearMocksDecorator: Decorator = (Story, context) => { + if (context.parameters.clearMocks) { + enterJestFn.mockClear(); + escapeJestfn.mockClear(); + clickOutsideJestFn.mockClear(); + tabJestFn.mockClear(); + shiftTabJestFn.mockClear(); + } + return ; +}; + +const meta: Meta = { + title: 'UI/field/input/URLFieldInput', + component: URLFieldInputWithContext, + args: { + value: 'https://username.domain', + onEnter: enterJestFn, + onEscape: escapeJestfn, + onClickOutside: clickOutsideJestFn, + onTab: tabJestFn, + onShiftTab: shiftTabJestFn, + }, + argTypes: { + onEnter: { control: false }, + onEscape: { control: false }, + onClickOutside: { control: false }, + onTab: { control: false }, + onShiftTab: { control: false }, + }, + decorators: [clearMocksDecorator], + parameters: { + clearMocks: true, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Enter: Story = { + play: async () => { + expect(enterJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{enter}'); + expect(enterJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Escape: Story = { + play: async () => { + expect(escapeJestfn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{esc}'); + expect(escapeJestfn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ClickOutside: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); + + const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); + + await waitFor(() => { + userEvent.click(emptyDiv); + expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const Tab: Story = { + play: async () => { + expect(tabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{tab}'); + expect(tabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; + +export const ShiftTab: Story = { + play: async () => { + expect(shiftTabJestFn).toHaveBeenCalledTimes(0); + + await waitFor(() => { + userEvent.keyboard('{shift>}{tab}'); + expect(shiftTabJestFn).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/internal/BooleanInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/BooleanInput.tsx new file mode 100644 index 0000000000000..9cec67c9f77dd --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/BooleanInput.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { IconCheck, IconX } from '@/ui/display/icon'; + +const StyledEditableBooleanFieldContainer = styled.div` + align-items: center; + cursor: pointer; + display: flex; + + height: 100%; + width: 100%; +`; + +const StyledEditableBooleanFieldValue = styled.div` + margin-left: ${({ theme }) => theme.spacing(1)}; +`; + +type BooleanInputProps = { + value: boolean; + onToggle?: (newValue: boolean) => void; + testId?: string; +}; + +export const BooleanInput = ({ + value, + onToggle, + testId, +}: BooleanInputProps) => { + const [internalValue, setInternalValue] = useState(value); + + useEffect(() => { + setInternalValue(value); + }, [value]); + + const handleClick = () => { + setInternalValue(!internalValue); + onToggle?.(!internalValue); + }; + + const theme = useTheme(); + + return ( + + {internalValue ? ( + + ) : ( + + )} + + {internalValue ? 'True' : 'False'} + + + ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/internal/DateInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/DateInput.tsx new file mode 100644 index 0000000000000..cc46ef7680b2a --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/DateInput.tsx @@ -0,0 +1,102 @@ +import { useEffect, useRef, useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { flip, offset, useFloating } from '@floating-ui/react'; + +import { DateDisplay } from '@/ui/data/field/meta-types/display/content-display/components/DateDisplay'; +import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; +import { Nullable } from '~/types/Nullable'; + +import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents'; + +const StyledCalendarContainer = styled.div` + background: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + box-shadow: ${({ theme }) => theme.boxShadow.strong}; + + margin-top: 1px; + + position: absolute; + + z-index: 1; +`; + +const StyledInputContainer = styled.div` + padding: ${({ theme }) => theme.spacing(0)} ${({ theme }) => theme.spacing(2)}; + + width: 100%; +`; + +export type DateInputProps = { + value: Nullable; + onEnter: (newDate: Nullable) => void; + onEscape: (newDate: Nullable) => void; + onClickOutside: ( + event: MouseEvent | TouchEvent, + newDate: Nullable, + ) => void; + hotkeyScope: string; +}; + +export const DateInput = ({ + value, + hotkeyScope, + onEnter, + onEscape, + onClickOutside, +}: DateInputProps) => { + const theme = useTheme(); + + const [internalValue, setInternalValue] = useState(value); + + const wrapperRef = useRef(null); + + const { refs, floatingStyles } = useFloating({ + placement: 'bottom-start', + middleware: [ + flip(), + offset({ + mainAxis: theme.spacingMultiplicator * 2, + }), + ], + }); + + const handleChange = (newDate: Date) => { + setInternalValue(newDate); + }; + + useEffect(() => { + setInternalValue(value); + }, [value]); + + useRegisterInputEvents({ + inputRef: wrapperRef, + inputValue: internalValue, + onEnter, + onEscape, + onClickOutside, + hotkeyScope, + }); + + return ( +
+
+ + + +
+
+ + { + onEnter(newDate); + }} + /> + +
+
+ ); +}; diff --git a/front/src/modules/ui/Data/Field/meta-types/input/components/internal/DoubleTextInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/DoubleTextInput.tsx new file mode 100644 index 0000000000000..9a613c9964d2a --- /dev/null +++ b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/DoubleTextInput.tsx @@ -0,0 +1,171 @@ +import { ChangeEvent, useEffect, useRef, useState } from 'react'; +import styled from '@emotion/styled'; +import { Key } from 'ts-key-enum'; + +import { FieldDoubleText } from '@/ui/data/field/types/FieldDoubleText'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { isDefined } from '~/utils/isDefined'; + +import { StyledInput } from './TextInput'; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + + input { + width: ${({ theme }) => theme.spacing(24)}; + } + + & > input:last-child { + border-left: 1px solid ${({ theme }) => theme.border.color.medium}; + padding-left: ${({ theme }) => theme.spacing(2)}; + } +`; + +type DoubleTextInputProps = { + firstValue: string; + secondValue: string; + firstValuePlaceholder: string; + secondValuePlaceholder: string; + hotkeyScope: string; + onEnter: (newDoubleTextValue: FieldDoubleText) => void; + onEscape: (newDoubleTextValue: FieldDoubleText) => void; + onTab?: (newDoubleTextValue: FieldDoubleText) => void; + onShiftTab?: (newDoubleTextValue: FieldDoubleText) => void; + onClickOutside: ( + event: MouseEvent | TouchEvent, + newDoubleTextValue: FieldDoubleText, + ) => void; +}; + +export const DoubleTextInput = ({ + firstValue, + secondValue, + firstValuePlaceholder, + secondValuePlaceholder, + hotkeyScope, + onClickOutside, + onEnter, + onEscape, + onShiftTab, + onTab, +}: DoubleTextInputProps) => { + const [firstInternalValue, setFirstInternalValue] = useState(firstValue); + const [secondInternalValue, setSecondInternalValue] = useState(secondValue); + + const firstValueInputRef = useRef(null); + const secondValueInputRef = useRef(null); + const containerRef = useRef(null); + + useEffect(() => { + setFirstInternalValue(firstValue); + setSecondInternalValue(secondValue); + }, [firstValue, secondValue]); + + const handleChange = ( + newFirstValue: string, + newSecondValue: string, + ): void => { + setFirstInternalValue(newFirstValue); + setSecondInternalValue(newSecondValue); + }; + + const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left'); + + useScopedHotkeys( + Key.Enter, + () => { + onEnter({ + firstValue: firstInternalValue, + secondValue: secondInternalValue, + }); + }, + hotkeyScope, + [onEnter, firstInternalValue, secondInternalValue], + ); + + useScopedHotkeys( + Key.Escape, + () => { + onEscape({ + firstValue: firstInternalValue, + secondValue: secondInternalValue, + }); + }, + hotkeyScope, + [onEscape, firstInternalValue, secondInternalValue], + ); + + useScopedHotkeys( + 'tab', + () => { + if (focusPosition === 'left') { + setFocusPosition('right'); + secondValueInputRef.current?.focus(); + } else { + onTab?.({ + firstValue: firstInternalValue, + secondValue: secondInternalValue, + }); + } + }, + hotkeyScope, + [onTab, firstInternalValue, secondInternalValue, focusPosition], + ); + + useScopedHotkeys( + 'shift+tab', + () => { + if (focusPosition === 'right') { + setFocusPosition('left'); + firstValueInputRef.current?.focus(); + } else { + onShiftTab?.({ + firstValue: firstInternalValue, + secondValue: secondInternalValue, + }); + } + }, + hotkeyScope, + [onShiftTab, firstInternalValue, secondInternalValue, focusPosition], + ); + + useListenClickOutside({ + refs: [containerRef], + callback: (event) => { + onClickOutside?.(event, { + firstValue: firstInternalValue, + secondValue: secondInternalValue, + }); + }, + enabled: isDefined(onClickOutside), + }); + + return ( + + setFocusPosition('left')} + ref={firstValueInputRef} + placeholder={firstValuePlaceholder} + value={firstInternalValue} + onChange={(event: ChangeEvent) => { + handleChange(event.target.value, secondInternalValue); + }} + /> + setFocusPosition('right')} + ref={secondValueInputRef} + placeholder={secondValuePlaceholder} + value={secondInternalValue} + onChange={(event: ChangeEvent) => { + handleChange(firstInternalValue, event.target.value); + }} + /> + + ); +}; diff --git a/front/src/modules/ui/field/meta-types/input/components/internal/PhoneInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/PhoneInput.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/input/components/internal/PhoneInput.tsx rename to front/src/modules/ui/Data/Field/meta-types/input/components/internal/PhoneInput.tsx diff --git a/front/src/modules/ui/field/meta-types/input/components/internal/ProbabilityInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/ProbabilityInput.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/input/components/internal/ProbabilityInput.tsx rename to front/src/modules/ui/Data/Field/meta-types/input/components/internal/ProbabilityInput.tsx diff --git a/front/src/modules/ui/field/meta-types/input/components/internal/TextInput.tsx b/front/src/modules/ui/Data/Field/meta-types/input/components/internal/TextInput.tsx similarity index 100% rename from front/src/modules/ui/field/meta-types/input/components/internal/TextInput.tsx rename to front/src/modules/ui/Data/Field/meta-types/input/components/internal/TextInput.tsx diff --git a/front/src/modules/ui/field/meta-types/input/hooks/useRegisterInputEvents.ts b/front/src/modules/ui/Data/Field/meta-types/input/hooks/useRegisterInputEvents.ts similarity index 100% rename from front/src/modules/ui/field/meta-types/input/hooks/useRegisterInputEvents.ts rename to front/src/modules/ui/Data/Field/meta-types/input/hooks/useRegisterInputEvents.ts diff --git a/front/src/modules/ui/field/states/entityFieldsFamilyState.ts b/front/src/modules/ui/Data/Field/states/entityFieldsFamilyState.ts similarity index 100% rename from front/src/modules/ui/field/states/entityFieldsFamilyState.ts rename to front/src/modules/ui/Data/Field/states/entityFieldsFamilyState.ts diff --git a/front/src/modules/ui/field/states/isFieldEmptyScopedState.ts b/front/src/modules/ui/Data/Field/states/isFieldEmptyScopedState.ts similarity index 100% rename from front/src/modules/ui/field/states/isFieldEmptyScopedState.ts rename to front/src/modules/ui/Data/Field/states/isFieldEmptyScopedState.ts diff --git a/front/src/modules/ui/field/states/selectors/entityFieldsFamilySelector.ts b/front/src/modules/ui/Data/Field/states/selectors/entityFieldsFamilySelector.ts similarity index 100% rename from front/src/modules/ui/field/states/selectors/entityFieldsFamilySelector.ts rename to front/src/modules/ui/Data/Field/states/selectors/entityFieldsFamilySelector.ts diff --git a/front/src/modules/ui/field/states/selectors/isEntityFieldEmptyFamilySelector.ts b/front/src/modules/ui/Data/Field/states/selectors/isEntityFieldEmptyFamilySelector.ts similarity index 100% rename from front/src/modules/ui/field/states/selectors/isEntityFieldEmptyFamilySelector.ts rename to front/src/modules/ui/Data/Field/states/selectors/isEntityFieldEmptyFamilySelector.ts diff --git a/front/src/modules/ui/Data/Field/types/FieldDefinition.ts b/front/src/modules/ui/Data/Field/types/FieldDefinition.ts new file mode 100644 index 0000000000000..1963865835500 --- /dev/null +++ b/front/src/modules/ui/Data/Field/types/FieldDefinition.ts @@ -0,0 +1,21 @@ +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { AvatarType } from '@/users/components/Avatar'; + +import { FieldMetadata } from './FieldMetadata'; +import { FieldType } from './FieldType'; + +export type FieldDefinition = { + key: string; + name: string; + Icon?: IconComponent; + type: FieldType; + metadata: T; + buttonIcon?: IconComponent; + basePathToShowPage?: string; + infoTooltipContent?: string; + entityChipDisplayMapper?: (dataObject: any) => { + name: string; + pictureUrl?: string; + avatarType: AvatarType; + }; +}; diff --git a/front/src/modules/ui/field/types/FieldDefinitionWithTypeOnly.ts b/front/src/modules/ui/Data/Field/types/FieldDefinitionWithTypeOnly.ts similarity index 100% rename from front/src/modules/ui/field/types/FieldDefinitionWithTypeOnly.ts rename to front/src/modules/ui/Data/Field/types/FieldDefinitionWithTypeOnly.ts diff --git a/front/src/modules/ui/field/types/FieldDoubleText.ts b/front/src/modules/ui/Data/Field/types/FieldDoubleText.ts similarity index 100% rename from front/src/modules/ui/field/types/FieldDoubleText.ts rename to front/src/modules/ui/Data/Field/types/FieldDoubleText.ts diff --git a/front/src/modules/ui/field/types/FieldInputEvent.ts b/front/src/modules/ui/Data/Field/types/FieldInputEvent.ts similarity index 100% rename from front/src/modules/ui/field/types/FieldInputEvent.ts rename to front/src/modules/ui/Data/Field/types/FieldInputEvent.ts diff --git a/front/src/modules/ui/field/types/FieldMetadata.ts b/front/src/modules/ui/Data/Field/types/FieldMetadata.ts similarity index 100% rename from front/src/modules/ui/field/types/FieldMetadata.ts rename to front/src/modules/ui/Data/Field/types/FieldMetadata.ts diff --git a/front/src/modules/ui/field/types/FieldType.ts b/front/src/modules/ui/Data/Field/types/FieldType.ts similarity index 100% rename from front/src/modules/ui/field/types/FieldType.ts rename to front/src/modules/ui/Data/Field/types/FieldType.ts diff --git a/front/src/modules/ui/field/types/guards/assertFieldMetadata.ts b/front/src/modules/ui/Data/Field/types/guards/assertFieldMetadata.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/assertFieldMetadata.ts rename to front/src/modules/ui/Data/Field/types/guards/assertFieldMetadata.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldBoolean.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldBoolean.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldBoolean.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldBoolean.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldBooleanValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldBooleanValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldBooleanValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldBooleanValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldChip.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldChip.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldChip.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldChip.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldChipValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldChipValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldChipValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldChipValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldDate.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldDate.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldDate.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldDate.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldDateValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldDateValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldDateValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldDateValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldDoubleText.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldDoubleText.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldDoubleText.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldDoubleText.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldDoubleTextChip.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldDoubleTextChip.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldDoubleTextChip.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldDoubleTextChip.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldDoubleTextChipValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldDoubleTextChipValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldDoubleTextChipValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldDoubleTextChipValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldDoubleTextValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldDoubleTextValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldDoubleTextValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldDoubleTextValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldEmail.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldEmail.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldEmail.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldEmail.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldEmailValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldEmailValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldEmailValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldEmailValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldMoney.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldMoney.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldMoney.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldMoney.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldMoneyValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldMoneyValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldMoneyValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldMoneyValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldNumber.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldNumber.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldNumber.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldNumber.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldNumberValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldNumberValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldNumberValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldNumberValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldPhone.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldPhone.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldPhone.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldPhone.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldPhoneValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldPhoneValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldPhoneValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldPhoneValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldProbability.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldProbability.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldProbability.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldProbability.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldProbabilityValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldProbabilityValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldProbabilityValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldProbabilityValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldRelation.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldRelation.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldRelation.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldRelation.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldRelationValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldRelationValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldRelationValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldRelationValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldText.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldText.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldText.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldText.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldTextValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldTextValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldTextValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldTextValue.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldURL.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldURL.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldURL.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldURL.ts diff --git a/front/src/modules/ui/field/types/guards/isFieldURLValue.ts b/front/src/modules/ui/Data/Field/types/guards/isFieldURLValue.ts similarity index 100% rename from front/src/modules/ui/field/types/guards/isFieldURLValue.ts rename to front/src/modules/ui/Data/Field/types/guards/isFieldURLValue.ts diff --git a/front/src/modules/ui/field/types/resolvers/DoubleTextTypeResolver.ts b/front/src/modules/ui/Data/Field/types/resolvers/DoubleTextTypeResolver.ts similarity index 100% rename from front/src/modules/ui/field/types/resolvers/DoubleTextTypeResolver.ts rename to front/src/modules/ui/Data/Field/types/resolvers/DoubleTextTypeResolver.ts diff --git a/front/src/modules/ui/Data/data-table/action-bar/components/DataTableActionBar.tsx b/front/src/modules/ui/Data/data-table/action-bar/components/DataTableActionBar.tsx new file mode 100644 index 0000000000000..3dd4332ff8103 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/action-bar/components/DataTableActionBar.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { useRecoilValue } from 'recoil'; + +import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar'; + +import { selectedRowIdsSelector } from '../../states/selectors/selectedRowIdsSelector'; + +export const DataTableActionBar = () => { + const selectedRowIds = useRecoilValue(selectedRowIdsSelector); + + return ; +}; diff --git a/front/src/modules/ui/Data/data-table/components/CheckboxCell.tsx b/front/src/modules/ui/Data/data-table/components/CheckboxCell.tsx new file mode 100644 index 0000000000000..340d53ff914ec --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/CheckboxCell.tsx @@ -0,0 +1,34 @@ +import { useCallback } from 'react'; +import styled from '@emotion/styled'; +import { useSetRecoilState } from 'recoil'; + +import { Checkbox } from '@/ui/input/components/Checkbox'; +import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState'; + +import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected'; + +const StyledContainer = styled.div` + align-items: center; + cursor: pointer; + + display: flex; + height: 32px; + + justify-content: center; +`; + +export const CheckboxCell = () => { + const setActionBarOpenState = useSetRecoilState(actionBarOpenState); + const { currentRowSelected, setCurrentRowSelected } = useCurrentRowSelected(); + + const handleClick = useCallback(() => { + setCurrentRowSelected(!currentRowSelected); + setActionBarOpenState(true); + }, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]); + + return ( + + + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/components/ColumnHead.tsx b/front/src/modules/ui/Data/data-table/components/ColumnHead.tsx new file mode 100644 index 0000000000000..ccc8075de5842 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/ColumnHead.tsx @@ -0,0 +1,51 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../types/ColumnDefinition'; + +type ColumnHeadProps = { + column: ColumnDefinition; +}; + +const StyledTitle = styled.div` + align-items: center; + display: flex; + flex-direction: row; + font-weight: ${({ theme }) => theme.font.weight.medium}; + gap: ${({ theme }) => theme.spacing(1)}; + height: ${({ theme }) => theme.spacing(8)}; + padding-left: ${({ theme }) => theme.spacing(2)}; + padding-right: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledIcon = styled.div` + display: flex; + + & > svg { + height: ${({ theme }) => theme.icon.size.md}px; + width: ${({ theme }) => theme.icon.size.md}px; + } +`; + +const StyledText = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const ColumnHead = ({ column }: ColumnHeadProps) => { + const theme = useTheme(); + + return ( + <> + + + {column.Icon && } + + {column.name} + + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/components/ColumnHeadWithDropdown.tsx b/front/src/modules/ui/Data/data-table/components/ColumnHeadWithDropdown.tsx new file mode 100644 index 0000000000000..a1d4250d4b2ba --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/ColumnHeadWithDropdown.tsx @@ -0,0 +1,40 @@ +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; + +import { ColumnDefinition } from '../types/ColumnDefinition'; + +import { ColumnHead } from './ColumnHead'; +import { DataTableColumnDropdownMenu } from './DataTableColumnDropdownMenu'; + +type ColumnHeadWithDropdownProps = { + column: ColumnDefinition; + isFirstColumn: boolean; + isLastColumn: boolean; + primaryColumnKey: string; +}; + +export const ColumnHeadWithDropdown = ({ + column, + isFirstColumn, + isLastColumn, + primaryColumnKey, +}: ColumnHeadWithDropdownProps) => { + return ( + + } + dropdownComponents={ + + } + dropdownHotkeyScope={{ scope: column.key + '-header' }} + dropdownOffset={{ x: 0, y: -8 }} + /> + + ); +}; diff --git a/front/src/modules/ui/data-table/components/DataTable.tsx b/front/src/modules/ui/Data/data-table/components/DataTable.tsx similarity index 100% rename from front/src/modules/ui/data-table/components/DataTable.tsx rename to front/src/modules/ui/Data/data-table/components/DataTable.tsx diff --git a/front/src/modules/ui/data-table/components/DataTableBody.tsx b/front/src/modules/ui/Data/data-table/components/DataTableBody.tsx similarity index 100% rename from front/src/modules/ui/data-table/components/DataTableBody.tsx rename to front/src/modules/ui/Data/data-table/components/DataTableBody.tsx diff --git a/front/src/modules/ui/Data/data-table/components/DataTableCell.tsx b/front/src/modules/ui/Data/data-table/components/DataTableCell.tsx new file mode 100644 index 0000000000000..0b3340fb41e49 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/DataTableCell.tsx @@ -0,0 +1,70 @@ +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; + +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { isFieldRelation } from '@/ui/data/field/types/guards/isFieldRelation'; +import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; +import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState'; +import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState'; +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; + +import { ColumnContext } from '../contexts/ColumnContext'; +import { ColumnIndexContext } from '../contexts/ColumnIndexContext'; +import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext'; +import { RowIdContext } from '../contexts/RowIdContext'; +import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected'; +import { TableCell } from '../table-cell/components/TableCell'; +import { TableHotkeyScope } from '../types/TableHotkeyScope'; + +export const DataTableCell = ({ cellIndex }: { cellIndex: number }) => { + const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); + const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); + const currentRowId = useContext(RowIdContext); + + const { setCurrentRowSelected } = useCurrentRowSelected(); + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + setCurrentRowSelected(true); + setContextMenuPosition({ + x: event.clientX, + y: event.clientY, + }); + setContextMenuOpenState(true); + }; + + const columnDefinition = useContext(ColumnContext); + + const updateEntityMutation = useContext(EntityUpdateMutationContext); + + // eslint-disable-next-line no-console + console.log({ columnDefinition, currentRowId }); + + if (!columnDefinition || !currentRowId) { + return null; + } + + const customHotkeyScope = isFieldRelation(columnDefinition) + ? RelationPickerHotkeyScope.RelationPicker + : TableHotkeyScope.CellEditMode; + + return ( + + + handleContextMenu(event)}> + [updateEntityMutation, {}], + hotkeyScope: customHotkeyScope, + }} + > + + + + + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/components/DataTableColumnDropdownMenu.tsx b/front/src/modules/ui/Data/data-table/components/DataTableColumnDropdownMenu.tsx new file mode 100644 index 0000000000000..6cadbf25d6778 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/DataTableColumnDropdownMenu.tsx @@ -0,0 +1,79 @@ +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; +import { IconArrowLeft, IconArrowRight, IconEyeOff } from '@/ui/display/icon'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; + +import { ColumnHeadDropdownId } from '../constants/ColumnHeadDropdownId'; +import { useTableColumns } from '../hooks/useTableColumns'; +import { ColumnDefinition } from '../types/ColumnDefinition'; + +export type DataTableColumnDropdownMenuProps = { + column: ColumnDefinition; + isFirstColumn: boolean; + isLastColumn: boolean; + primaryColumnKey: string; +}; + +export const DataTableColumnDropdownMenu = ({ + column, + isFirstColumn, + isLastColumn, + primaryColumnKey, +}: DataTableColumnDropdownMenuProps) => { + const { handleColumnVisibilityChange, handleMoveTableColumn } = + useTableColumns(); + + const { closeDropdown } = useDropdown({ + dropdownScopeId: ColumnHeadDropdownId, + }); + + const handleColumnMoveLeft = () => { + closeDropdown(); + if (isFirstColumn) { + return; + } + handleMoveTableColumn('left', column); + }; + + const handleColumnMoveRight = () => { + closeDropdown(); + if (isLastColumn) { + return; + } + handleMoveTableColumn('right', column); + }; + + const handleColumnVisibility = () => { + handleColumnVisibilityChange(column); + }; + + return column.key === primaryColumnKey ? ( + <> + ) : ( + + + {!isFirstColumn && ( + + )} + {!isLastColumn && ( + + )} + + + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/components/DataTableEffect.tsx b/front/src/modules/ui/Data/data-table/components/DataTableEffect.tsx new file mode 100644 index 0000000000000..74a235d8dc8c2 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/DataTableEffect.tsx @@ -0,0 +1,106 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useRecoilCallback } from 'recoil'; + +import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; +import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition'; +import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState'; +import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState'; +import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState'; +import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState'; +import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState'; +import { FilterDefinition } from '@/ui/data/view-bar/types/FilterDefinition'; +import { SortDefinition } from '@/ui/data/view-bar/types/SortDefinition'; +import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; +import { SortOrder } from '~/generated/graphql'; + +import { useSetDataTableData } from '../hooks/useSetDataTableData'; +import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext'; + +export const DataTableEffect = ({ + useGetRequest, + getRequestResultKey, + getRequestOptimisticEffectDefinition, + orderBy = [ + { + createdAt: SortOrder.Desc, + }, + ], + whereFilters, + filterDefinitionArray, + setActionBarEntries, + setContextMenuEntries, + sortDefinitionArray, +}: { + // TODO: type this + useGetRequest: any; + getRequestResultKey: string; + getRequestOptimisticEffectDefinition: OptimisticEffectDefinition; + // TODO: type this and replace with defaultSorts reduce should be applied to defaultSorts in this component not before + orderBy?: any; + // TODO: type this and replace with defaultFilters reduce should be applied to defaultFilters in this component not before + whereFilters?: any; + filterDefinitionArray: FilterDefinition[]; + sortDefinitionArray: SortDefinition[]; + setActionBarEntries?: () => void; + setContextMenuEntries?: () => void; +}) => { + const setDataTableData = useSetDataTableData(); + const { registerOptimisticEffect } = useOptimisticEffect(); + + useGetRequest({ + variables: { orderBy, where: whereFilters }, + onCompleted: (data: any) => { + const entities = data[getRequestResultKey] ?? []; + + setDataTableData(entities, filterDefinitionArray, sortDefinitionArray); + + registerOptimisticEffect({ + variables: { orderBy, where: whereFilters }, + definition: getRequestOptimisticEffectDefinition, + }); + }, + }); + + const [searchParams] = useSearchParams(); + const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext); + const handleViewSelect = useRecoilCallback( + ({ set, snapshot }) => + async (viewId: string) => { + const currentView = await snapshot.getPromise( + currentViewIdScopedState(tableRecoilScopeId), + ); + if (currentView === viewId) { + return; + } + + const savedFilters = await snapshot.getPromise( + savedFiltersFamilyState(viewId), + ); + const savedSorts = await snapshot.getPromise( + savedSortsFamilyState(viewId), + ); + + set(filtersScopedState(tableRecoilScopeId), savedFilters); + set(sortsScopedState(tableRecoilScopeId), savedSorts); + set(currentViewIdScopedState(tableRecoilScopeId), viewId); + }, + [tableRecoilScopeId], + ); + + useEffect(() => { + const viewId = searchParams.get('view'); + if (viewId) { + handleViewSelect(viewId); + } + setActionBarEntries?.(); + setContextMenuEntries?.(); + }, [ + handleViewSelect, + searchParams, + setActionBarEntries, + setContextMenuEntries, + ]); + + return <>; +}; diff --git a/front/src/modules/ui/Data/data-table/components/DataTableHeader.tsx b/front/src/modules/ui/Data/data-table/components/DataTableHeader.tsx new file mode 100644 index 0000000000000..6eb944b128918 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/DataTableHeader.tsx @@ -0,0 +1,228 @@ +import { useCallback, useState } from 'react'; +import styled from '@emotion/styled'; +import { useRecoilCallback, useRecoilState } from 'recoil'; + +import { IconPlus } from '@/ui/display/icon'; +import { IconButton } from '@/ui/input/button/components/IconButton'; +import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; +import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; + +import { useTableColumns } from '../hooks/useTableColumns'; +import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState'; +import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector'; +import { tableColumnsByKeyScopedSelector } from '../states/selectors/tableColumnsByKeyScopedSelector'; +import { visibleTableColumnsScopedSelector } from '../states/selectors/visibleTableColumnsScopedSelector'; +import { tableColumnsScopedState } from '../states/tableColumnsScopedState'; + +import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown'; +import { DataTableHeaderPlusButton } from './DataTableHeaderPlusButton'; +import { SelectAllCheckbox } from './SelectAllCheckbox'; + +const COLUMN_MIN_WIDTH = 75; + +const StyledColumnHeaderCell = styled.th<{ + columnWidth: number; + isResizing?: boolean; +}>` + ${({ columnWidth }) => ` + min-width: ${columnWidth}px; + width: ${columnWidth}px; + `} + position: relative; + user-select: none; + ${({ isResizing, theme }) => { + if (isResizing) { + return `&:after { + background-color: ${theme.color.blue}; + bottom: 0; + content: ''; + display: block; + position: absolute; + right: -1px; + top: 0; + width: 2px; + }`; + } + }}; +`; + +const StyledResizeHandler = styled.div` + bottom: 0; + cursor: col-resize; + padding: 0 ${({ theme }) => theme.spacing(2)}; + position: absolute; + right: -9px; + top: 0; + width: 3px; + z-index: 1; +`; + +const StyledAddIconButtonWrapper = styled.div` + display: inline-flex; + position: relative; +`; + +const StyledDataTableColumnMenu = styled(DataTableHeaderPlusButton)` + position: absolute; + right: 0; + top: 100%; + z-index: ${({ theme }) => theme.lastLayerZIndex}; +`; + +const StyledTableHead = styled.thead` + cursor: pointer; +`; + +const StyledColumnHeadContainer = styled.div` + position: relative; + z-index: 1; +`; + +export const DataTableHeader = () => { + const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState( + resizeFieldOffsetState, + ); + const tableColumns = useRecoilScopedValue( + tableColumnsScopedState, + TableRecoilScopeContext, + ); + const tableColumnsByKey = useRecoilScopedValue( + tableColumnsByKeyScopedSelector, + TableRecoilScopeContext, + ); + const hiddenTableColumns = useRecoilScopedValue( + hiddenTableColumnsScopedSelector, + TableRecoilScopeContext, + ); + const visibleTableColumns = useRecoilScopedValue( + visibleTableColumnsScopedSelector, + TableRecoilScopeContext, + ); + + const [initialPointerPositionX, setInitialPointerPositionX] = useState< + number | null + >(null); + const [resizedFieldKey, setResizedFieldKey] = useState(null); + const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false); + + const { handleColumnsChange } = useTableColumns(); + + const handleResizeHandlerStart = useCallback((positionX: number) => { + setInitialPointerPositionX(positionX); + }, []); + + const handleResizeHandlerMove = useCallback( + (positionX: number) => { + if (!initialPointerPositionX) return; + setResizeFieldOffset(positionX - initialPointerPositionX); + }, + [setResizeFieldOffset, initialPointerPositionX], + ); + + const handleResizeHandlerEnd = useRecoilCallback( + ({ snapshot, set }) => + async () => { + if (!resizedFieldKey) return; + + const nextWidth = Math.round( + Math.max( + tableColumnsByKey[resizedFieldKey].size + + snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(), + COLUMN_MIN_WIDTH, + ), + ); + + set(resizeFieldOffsetState, 0); + setInitialPointerPositionX(null); + setResizedFieldKey(null); + + if (nextWidth !== tableColumnsByKey[resizedFieldKey].size) { + const nextColumns = tableColumns.map((column) => + column.key === resizedFieldKey + ? { ...column, size: nextWidth } + : column, + ); + + await handleColumnsChange(nextColumns); + } + }, + [resizedFieldKey, tableColumnsByKey, tableColumns, handleColumnsChange], + ); + + useTrackPointer({ + shouldTrackPointer: resizedFieldKey !== null, + onMouseDown: handleResizeHandlerStart, + onMouseMove: handleResizeHandlerMove, + onMouseUp: handleResizeHandlerEnd, + }); + + const toggleColumnMenu = useCallback(() => { + setIsColumnMenuOpen((previousValue) => !previousValue); + }, []); + + const primaryColumn = visibleTableColumns[0]; + + return ( + + + + + + {visibleTableColumns.map((column, index) => ( + + + + + { + setResizedFieldKey(column.key); + }} + /> + + ))} + + + {hiddenTableColumns.length > 0 && ( + + + {isColumnMenuOpen && ( + + )} + + )} + + + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/components/DataTableHeaderPlusButton.tsx b/front/src/modules/ui/Data/data-table/components/DataTableHeaderPlusButton.tsx new file mode 100644 index 0000000000000..18e5bbe8becb4 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/components/DataTableHeaderPlusButton.tsx @@ -0,0 +1,71 @@ +import { ComponentProps, useCallback, useRef } from 'react'; +import styled from '@emotion/styled'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; +import { IconPlus } from '@/ui/display/icon'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; + +import { useTableColumns } from '../hooks/useTableColumns'; +import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector'; +import { ColumnDefinition } from '../types/ColumnDefinition'; + +const StyledHeaderPlusButton = styled(StyledDropdownMenu)` + font-weight: ${({ theme }) => theme.font.weight.regular}; +`; + +type DataTableHeaderPlusButtonProps = { + onAddColumn?: () => void; + onClickOutside?: () => void; +} & ComponentProps<'div'>; + +export const DataTableHeaderPlusButton = ({ + onAddColumn, + onClickOutside = () => undefined, +}: DataTableHeaderPlusButtonProps) => { + const ref = useRef(null); + + const hiddenTableColumns = useRecoilScopedValue( + hiddenTableColumnsScopedSelector, + TableRecoilScopeContext, + ); + + useListenClickOutside({ + refs: [ref], + callback: onClickOutside, + }); + + const { handleColumnVisibilityChange } = useTableColumns(); + + const handleAddColumn = useCallback( + (column: ColumnDefinition) => { + onAddColumn?.(); + handleColumnVisibilityChange(column); + }, + [handleColumnVisibilityChange, onAddColumn], + ); + + return ( + + + {hiddenTableColumns.map((column) => ( + handleAddColumn(column), + }, + ]} + LeftIcon={column.Icon} + text={column.name} + /> + ))} + + + ); +}; diff --git a/front/src/modules/ui/data-table/components/DataTableRow.tsx b/front/src/modules/ui/Data/data-table/components/DataTableRow.tsx similarity index 100% rename from front/src/modules/ui/data-table/components/DataTableRow.tsx rename to front/src/modules/ui/Data/data-table/components/DataTableRow.tsx diff --git a/front/src/modules/ui/data-table/components/SelectAllCheckbox.tsx b/front/src/modules/ui/Data/data-table/components/SelectAllCheckbox.tsx similarity index 100% rename from front/src/modules/ui/data-table/components/SelectAllCheckbox.tsx rename to front/src/modules/ui/Data/data-table/components/SelectAllCheckbox.tsx diff --git a/front/src/modules/ui/data-table/constants/ColumnHeadDropdownId.ts b/front/src/modules/ui/Data/data-table/constants/ColumnHeadDropdownId.ts similarity index 100% rename from front/src/modules/ui/data-table/constants/ColumnHeadDropdownId.ts rename to front/src/modules/ui/Data/data-table/constants/ColumnHeadDropdownId.ts diff --git a/front/src/modules/ui/data-table/constants/TableOptionsDropdownId.ts b/front/src/modules/ui/Data/data-table/constants/TableOptionsDropdownId.ts similarity index 100% rename from front/src/modules/ui/data-table/constants/TableOptionsDropdownId.ts rename to front/src/modules/ui/Data/data-table/constants/TableOptionsDropdownId.ts diff --git a/front/src/modules/ui/data-table/constants/countries.json b/front/src/modules/ui/Data/data-table/constants/countries.json similarity index 100% rename from front/src/modules/ui/data-table/constants/countries.json rename to front/src/modules/ui/Data/data-table/constants/countries.json diff --git a/front/src/modules/ui/Data/data-table/context-menu/components/DataTableContextMenu.tsx b/front/src/modules/ui/Data/data-table/context-menu/components/DataTableContextMenu.tsx new file mode 100644 index 0000000000000..60c3924cd9f3b --- /dev/null +++ b/front/src/modules/ui/Data/data-table/context-menu/components/DataTableContextMenu.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { useRecoilValue } from 'recoil'; + +import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu'; + +import { selectedRowIdsSelector } from '../../states/selectors/selectedRowIdsSelector'; + +export const DataTableContextMenu = () => { + const selectedRowIds = useRecoilValue(selectedRowIdsSelector); + return ; +}; diff --git a/front/src/modules/ui/data-table/contexts/CellHotkeyScopeContext.ts b/front/src/modules/ui/Data/data-table/contexts/CellHotkeyScopeContext.ts similarity index 100% rename from front/src/modules/ui/data-table/contexts/CellHotkeyScopeContext.ts rename to front/src/modules/ui/Data/data-table/contexts/CellHotkeyScopeContext.ts diff --git a/front/src/modules/ui/Data/data-table/contexts/ColumnContext.ts b/front/src/modules/ui/Data/data-table/contexts/ColumnContext.ts new file mode 100644 index 0000000000000..499ca58e1e217 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/contexts/ColumnContext.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../types/ColumnDefinition'; + +export const ColumnContext = + createContext | null>(null); diff --git a/front/src/modules/ui/data-table/contexts/ColumnIndexContext.ts b/front/src/modules/ui/Data/data-table/contexts/ColumnIndexContext.ts similarity index 100% rename from front/src/modules/ui/data-table/contexts/ColumnIndexContext.ts rename to front/src/modules/ui/Data/data-table/contexts/ColumnIndexContext.ts diff --git a/front/src/modules/ui/data-table/contexts/EntityUpdateMutationHookContext.ts b/front/src/modules/ui/Data/data-table/contexts/EntityUpdateMutationHookContext.ts similarity index 100% rename from front/src/modules/ui/data-table/contexts/EntityUpdateMutationHookContext.ts rename to front/src/modules/ui/Data/data-table/contexts/EntityUpdateMutationHookContext.ts diff --git a/front/src/modules/ui/data-table/contexts/RowIdContext.ts b/front/src/modules/ui/Data/data-table/contexts/RowIdContext.ts similarity index 100% rename from front/src/modules/ui/data-table/contexts/RowIdContext.ts rename to front/src/modules/ui/Data/data-table/contexts/RowIdContext.ts diff --git a/front/src/modules/ui/data-table/contexts/RowIndexContext.ts b/front/src/modules/ui/Data/data-table/contexts/RowIndexContext.ts similarity index 100% rename from front/src/modules/ui/data-table/contexts/RowIndexContext.ts rename to front/src/modules/ui/Data/data-table/contexts/RowIndexContext.ts diff --git a/front/src/modules/ui/Data/data-table/contexts/TableContext.ts b/front/src/modules/ui/Data/data-table/contexts/TableContext.ts new file mode 100644 index 0000000000000..fc116d1272eb0 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/contexts/TableContext.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../types/ColumnDefinition'; + +export const TableContext = createContext<{ + onColumnsChange?: ( + columns: ColumnDefinition[], + ) => void | Promise; +}>({}); diff --git a/front/src/modules/ui/data-table/hooks/useCellInputEventHandlers.ts b/front/src/modules/ui/Data/data-table/hooks/useCellInputEventHandlers.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useCellInputEventHandlers.ts rename to front/src/modules/ui/Data/data-table/hooks/useCellInputEventHandlers.ts diff --git a/front/src/modules/ui/data-table/hooks/useCloseCurrentTableCellInEditMode.ts b/front/src/modules/ui/Data/data-table/hooks/useCloseCurrentTableCellInEditMode.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useCloseCurrentTableCellInEditMode.ts rename to front/src/modules/ui/Data/data-table/hooks/useCloseCurrentTableCellInEditMode.ts diff --git a/front/src/modules/ui/data-table/hooks/useCurrentEntityId.ts b/front/src/modules/ui/Data/data-table/hooks/useCurrentEntityId.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useCurrentEntityId.ts rename to front/src/modules/ui/Data/data-table/hooks/useCurrentEntityId.ts diff --git a/front/src/modules/ui/data-table/hooks/useCurrentRowSelected.ts b/front/src/modules/ui/Data/data-table/hooks/useCurrentRowSelected.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useCurrentRowSelected.ts rename to front/src/modules/ui/Data/data-table/hooks/useCurrentRowSelected.ts diff --git a/front/src/modules/ui/data-table/hooks/useDisableSoftFocus.ts b/front/src/modules/ui/Data/data-table/hooks/useDisableSoftFocus.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useDisableSoftFocus.ts rename to front/src/modules/ui/Data/data-table/hooks/useDisableSoftFocus.ts diff --git a/front/src/modules/ui/data-table/hooks/useGetIsSomeCellInEditMode.ts b/front/src/modules/ui/Data/data-table/hooks/useGetIsSomeCellInEditMode.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useGetIsSomeCellInEditMode.ts rename to front/src/modules/ui/Data/data-table/hooks/useGetIsSomeCellInEditMode.ts diff --git a/front/src/modules/ui/data-table/hooks/useLeaveTableFocus.ts b/front/src/modules/ui/Data/data-table/hooks/useLeaveTableFocus.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useLeaveTableFocus.ts rename to front/src/modules/ui/Data/data-table/hooks/useLeaveTableFocus.ts diff --git a/front/src/modules/ui/data-table/hooks/useMapKeyboardToSoftFocus.ts b/front/src/modules/ui/Data/data-table/hooks/useMapKeyboardToSoftFocus.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useMapKeyboardToSoftFocus.ts rename to front/src/modules/ui/Data/data-table/hooks/useMapKeyboardToSoftFocus.ts diff --git a/front/src/modules/ui/data-table/hooks/useMoveEditModeToCellPosition.ts b/front/src/modules/ui/Data/data-table/hooks/useMoveEditModeToCellPosition.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useMoveEditModeToCellPosition.ts rename to front/src/modules/ui/Data/data-table/hooks/useMoveEditModeToCellPosition.ts diff --git a/front/src/modules/ui/data-table/hooks/useMoveSoftFocus.ts b/front/src/modules/ui/Data/data-table/hooks/useMoveSoftFocus.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useMoveSoftFocus.ts rename to front/src/modules/ui/Data/data-table/hooks/useMoveSoftFocus.ts diff --git a/front/src/modules/ui/data-table/hooks/useMoveSoftFocusToCurrentCellOnHover.ts b/front/src/modules/ui/Data/data-table/hooks/useMoveSoftFocusToCurrentCellOnHover.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useMoveSoftFocusToCurrentCellOnHover.ts rename to front/src/modules/ui/Data/data-table/hooks/useMoveSoftFocusToCurrentCellOnHover.ts diff --git a/front/src/modules/ui/data-table/hooks/useResetTableRowSelection.ts b/front/src/modules/ui/Data/data-table/hooks/useResetTableRowSelection.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useResetTableRowSelection.ts rename to front/src/modules/ui/Data/data-table/hooks/useResetTableRowSelection.ts diff --git a/front/src/modules/ui/data-table/hooks/useSelectAllRows.ts b/front/src/modules/ui/Data/data-table/hooks/useSelectAllRows.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useSelectAllRows.ts rename to front/src/modules/ui/Data/data-table/hooks/useSelectAllRows.ts diff --git a/front/src/modules/ui/Data/data-table/hooks/useSetDataTableData.ts b/front/src/modules/ui/Data/data-table/hooks/useSetDataTableData.ts new file mode 100644 index 0000000000000..aea4af97dcd73 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/hooks/useSetDataTableData.ts @@ -0,0 +1,70 @@ +import { useRecoilCallback } from 'recoil'; + +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; +import { availableFiltersScopedState } from '@/ui/data/view-bar/states/availableFiltersScopedState'; +import { availableSortsScopedState } from '@/ui/data/view-bar/states/availableSortsScopedState'; +import { entityCountInCurrentViewState } from '@/ui/data/view-bar/states/entityCountInCurrentViewState'; +import { FilterDefinition } from '@/ui/data/view-bar/types/FilterDefinition'; +import { SortDefinition } from '@/ui/data/view-bar/types/SortDefinition'; +import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; + +import { isFetchingDataTableDataState } from '../states/isFetchingDataTableDataState'; +import { numberOfTableRowsState } from '../states/numberOfTableRowsState'; +import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { tableRowIdsState } from '../states/tableRowIdsState'; + +import { useResetTableRowSelection } from './useResetTableRowSelection'; + +export const useSetDataTableData = () => { + const resetTableRowSelection = useResetTableRowSelection(); + + const tableContextScopeId = useRecoilScopeId(TableRecoilScopeContext); + + return useRecoilCallback( + ({ set, snapshot }) => + ( + newEntityArray: T[], + filterDefinitionArray: FilterDefinition[], + sortDefinitionArray: SortDefinition[], + ) => { + for (const entity of newEntityArray) { + const currentEntity = snapshot + .getLoadable(entityFieldsFamilyState(entity.id)) + .valueOrThrow(); + + if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) { + set(entityFieldsFamilyState(entity.id), entity); + } + } + + const entityIds = newEntityArray.map((entity) => entity.id); + + set(tableRowIdsState, (currentRowIds) => { + if (JSON.stringify(currentRowIds) !== JSON.stringify(entityIds)) { + return entityIds; + } + + return currentRowIds; + }); + + resetTableRowSelection(); + + set(numberOfTableRowsState, entityIds.length); + + set(entityCountInCurrentViewState, entityIds.length); + + set( + availableFiltersScopedState(tableContextScopeId), + filterDefinitionArray, + ); + + set( + availableSortsScopedState(tableContextScopeId), + sortDefinitionArray, + ); + + set(isFetchingDataTableDataState, false); + }, + [resetTableRowSelection, tableContextScopeId], + ); +}; diff --git a/front/src/modules/ui/data-table/hooks/useSetRowSelectedState.ts b/front/src/modules/ui/Data/data-table/hooks/useSetRowSelectedState.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useSetRowSelectedState.ts rename to front/src/modules/ui/Data/data-table/hooks/useSetRowSelectedState.ts diff --git a/front/src/modules/ui/data-table/hooks/useSetSoftFocusPosition.ts b/front/src/modules/ui/Data/data-table/hooks/useSetSoftFocusPosition.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useSetSoftFocusPosition.ts rename to front/src/modules/ui/Data/data-table/hooks/useSetSoftFocusPosition.ts diff --git a/front/src/modules/ui/Data/data-table/hooks/useTableColumns.ts b/front/src/modules/ui/Data/data-table/hooks/useTableColumns.ts new file mode 100644 index 0000000000000..2aa248217cfa7 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/hooks/useTableColumns.ts @@ -0,0 +1,115 @@ +import { useCallback, useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; +import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState'; +import { ViewFieldForVisibility } from '@/ui/data/view-bar/types/ViewFieldForVisibility'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns'; + +import { TableContext } from '../contexts/TableContext'; +import { availableTableColumnsScopedState } from '../states/availableTableColumnsScopedState'; +import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { savedTableColumnsFamilyState } from '../states/savedTableColumnsFamilyState'; +import { tableColumnsScopedState } from '../states/tableColumnsScopedState'; +import { ColumnDefinition } from '../types/ColumnDefinition'; + +export const useTableColumns = () => { + const { onColumnsChange } = useContext(TableContext); + + const [availableTableColumns] = useRecoilScopedState( + availableTableColumnsScopedState, + TableRecoilScopeContext, + ); + + const currentViewId = useRecoilScopedValue( + currentViewIdScopedState, + TableRecoilScopeContext, + ); + const setSavedTableColumns = useSetRecoilState( + savedTableColumnsFamilyState(currentViewId), + ); + const [tableColumns, setTableColumns] = useRecoilScopedState( + tableColumnsScopedState, + TableRecoilScopeContext, + ); + + const { handleColumnMove } = useMoveViewColumns(); + + const handleColumnsChange = useCallback( + async (columns: ColumnDefinition[]) => { + setSavedTableColumns(columns); + setTableColumns(columns); + + await onColumnsChange?.(columns); + }, + [onColumnsChange, setSavedTableColumns, setTableColumns], + ); + + const handleColumnReorder = useCallback( + async (columns: ColumnDefinition[]) => { + const updatedColumns = columns.map((column, index) => ({ + ...column, + index, + })); + + await handleColumnsChange(updatedColumns); + }, + [handleColumnsChange], + ); + + const handleColumnVisibilityChange = useCallback( + async (viewField: ViewFieldForVisibility) => { + const isNewColumn = !tableColumns.some( + (tableColumns) => tableColumns.key === viewField.key, + ); + + if (isNewColumn) { + const newColumn = availableTableColumns.find( + (availableTableColumn) => availableTableColumn.key === viewField.key, + ); + if (!newColumn) return; + + const nextColumns = [ + ...tableColumns, + { ...newColumn, isVisible: true }, + ]; + + await handleColumnsChange(nextColumns); + } else { + const nextColumns = tableColumns.map((previousColumn) => + previousColumn.key === viewField.key + ? { ...previousColumn, isVisible: !viewField.isVisible } + : previousColumn, + ); + + await handleColumnsChange(nextColumns); + } + }, + [tableColumns, availableTableColumns, handleColumnsChange], + ); + + const handleMoveTableColumn = useCallback( + (direction: 'left' | 'right', column: ColumnDefinition) => { + const currentColumnArrayIndex = tableColumns.findIndex( + (tableColumn) => tableColumn.key === column.key, + ); + const columns = handleColumnMove( + direction, + currentColumnArrayIndex, + tableColumns, + ); + + setTableColumns(columns); + }, + [tableColumns, setTableColumns, handleColumnMove], + ); + + return { + handleColumnVisibilityChange, + handleMoveTableColumn, + handleColumnReorder, + handleColumnsChange, + }; +}; diff --git a/front/src/modules/ui/Data/data-table/hooks/useUpsertDataTableItem.ts b/front/src/modules/ui/Data/data-table/hooks/useUpsertDataTableItem.ts new file mode 100644 index 0000000000000..4eb84488c7590 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/hooks/useUpsertDataTableItem.ts @@ -0,0 +1,18 @@ +import { useRecoilCallback } from 'recoil'; + +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; + +export const useUpsertDataTableItem = () => + useRecoilCallback( + ({ set, snapshot }) => + (entity: T) => { + const currentEntity = snapshot + .getLoadable(entityFieldsFamilyState(entity.id)) + .valueOrThrow(); + + if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) { + set(entityFieldsFamilyState(entity.id), entity); + } + }, + [], + ); diff --git a/front/src/modules/ui/Data/data-table/hooks/useUpsertDataTableItems.ts b/front/src/modules/ui/Data/data-table/hooks/useUpsertDataTableItems.ts new file mode 100644 index 0000000000000..2d259d56ed579 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/hooks/useUpsertDataTableItems.ts @@ -0,0 +1,33 @@ +import { useRecoilCallback } from 'recoil'; + +import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState'; + +export const useUpsertDataTableItems = () => + useRecoilCallback( + ({ set, snapshot }) => + (entities: T[]) => { + // Create a map of new entities for quick lookup. + const newEntityMap = new Map( + entities.map((entity) => [entity.id, entity]), + ); + + // Filter out entities that are already the same in the state. + const entitiesToUpdate = entities.filter((entity) => { + const currentEntity = snapshot + .getLoadable(entityFieldsFamilyState(entity.id)) + .valueMaybe(); + + return ( + !currentEntity || + JSON.stringify(currentEntity) !== + JSON.stringify(newEntityMap.get(entity.id)) + ); + }); + + // Batch set state for the filtered entities. + for (const entity of entitiesToUpdate) { + set(entityFieldsFamilyState(entity.id), entity); + } + }, + [], + ); diff --git a/front/src/modules/ui/data-table/hooks/useUpsertTableRowId.ts b/front/src/modules/ui/Data/data-table/hooks/useUpsertTableRowId.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useUpsertTableRowId.ts rename to front/src/modules/ui/Data/data-table/hooks/useUpsertTableRowId.ts diff --git a/front/src/modules/ui/data-table/hooks/useUpsertTableRowIds.ts b/front/src/modules/ui/Data/data-table/hooks/useUpsertTableRowIds.ts similarity index 100% rename from front/src/modules/ui/data-table/hooks/useUpsertTableRowIds.ts rename to front/src/modules/ui/Data/data-table/hooks/useUpsertTableRowIds.ts diff --git a/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdown.tsx b/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdown.tsx new file mode 100644 index 0000000000000..f6d2c062d103a --- /dev/null +++ b/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdown.tsx @@ -0,0 +1,30 @@ +import { useResetRecoilState } from 'recoil'; + +import { ViewBarDropdownButton } from '@/ui/data/view-bar/components/ViewBarDropdownButton'; +import { viewEditModeState } from '@/ui/data/view-bar/states/viewEditModeState'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; + +import { TableOptionsDropdownButton } from './TableOptionsDropdownButton'; +import { TableOptionsDropdownContent } from './TableOptionsDropdownContent'; + +type TableOptionsDropdownProps = { + customHotkeyScope: HotkeyScope; +}; + +export const TableOptionsDropdown = ({ + customHotkeyScope, +}: TableOptionsDropdownProps) => { + const resetViewEditMode = useResetRecoilState(viewEditModeState); + + return ( + } + dropdownHotkeyScope={customHotkeyScope} + dropdownId={TableOptionsDropdownId} + dropdownComponents={} + onClickOutside={resetViewEditMode} + /> + ); +}; diff --git a/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdownButton.tsx b/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdownButton.tsx new file mode 100644 index 0000000000000..96d1ce4998899 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdownButton.tsx @@ -0,0 +1,18 @@ +import { TableOptionsDropdownId } from '@/ui/data/data-table/constants/TableOptionsDropdownId'; +import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; + +export const TableOptionsDropdownButton = () => { + const { isDropdownOpen, toggleDropdown } = useDropdown({ + dropdownScopeId: TableOptionsDropdownId, + }); + + return ( + + Options + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdownContent.tsx b/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdownContent.tsx new file mode 100644 index 0000000000000..5a45cf672f094 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/options/components/TableOptionsDropdownContent.tsx @@ -0,0 +1,197 @@ +import { useCallback, useContext, useRef, useState } from 'react'; +import { OnDragEndResponder } from '@hello-pangea/dnd'; +import { useRecoilCallback, useRecoilValue, useResetRecoilState } from 'recoil'; +import { Key } from 'ts-key-enum'; + +import { ViewFieldsVisibilityDropdownSection } from '@/ui/data/view-bar/components/ViewFieldsVisibilityDropdownSection'; +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; +import { useUpsertView } from '@/ui/data/view-bar/hooks/useUpsertView'; +import { currentViewScopedSelector } from '@/ui/data/view-bar/states/selectors/currentViewScopedSelector'; +import { viewsByIdScopedSelector } from '@/ui/data/view-bar/states/selectors/viewsByIdScopedSelector'; +import { viewEditModeState } from '@/ui/data/view-bar/states/viewEditModeState'; +import { IconChevronLeft, IconFileImport, IconTag } from '@/ui/display/icon'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; +import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; +import { DropdownMenuInputContainer } from '@/ui/layout/dropdown/components/DropdownMenuInputContainer'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; + +import { useTableColumns } from '../../hooks/useTableColumns'; +import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState'; +import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector'; +import { visibleTableColumnsScopedSelector } from '../../states/selectors/visibleTableColumnsScopedSelector'; +import { tableColumnsScopedState } from '../../states/tableColumnsScopedState'; +import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; + +type TableOptionsMenu = 'fields'; + +export const TableOptionsDropdownContent = () => { + const scopeId = useRecoilScopeId(TableRecoilScopeContext); + + const { onImport } = useContext(ViewBarContext); + const { closeDropdown } = useDropdown(); + + const [currentMenu, setCurrentMenu] = useState( + undefined, + ); + + const viewEditInputRef = useRef(null); + + const currentView = useRecoilScopedValue( + currentViewScopedSelector, + TableRecoilScopeContext, + ); + const viewEditMode = useRecoilValue(viewEditModeState); + const resetViewEditMode = useResetRecoilState(viewEditModeState); + const visibleTableColumns = useRecoilScopedValue( + visibleTableColumnsScopedSelector, + TableRecoilScopeContext, + ); + const hiddenTableColumns = useRecoilScopedValue( + hiddenTableColumnsScopedSelector, + TableRecoilScopeContext, + ); + const viewsById = useRecoilScopedValue( + viewsByIdScopedSelector, + TableRecoilScopeContext, + ); + + const { handleColumnVisibilityChange, handleColumnReorder } = + useTableColumns(); + + const { upsertView } = useUpsertView(); + + const handleViewNameSubmit = useRecoilCallback( + ({ set, snapshot }) => + async () => { + const tableColumns = await snapshot.getPromise( + tableColumnsScopedState(scopeId), + ); + const isCreateMode = viewEditMode.mode === 'create'; + const name = viewEditInputRef.current?.value; + const view = await upsertView(name); + + if (view && isCreateMode) { + set(savedTableColumnsFamilyState(view.id), tableColumns); + } + }, + [scopeId, upsertView, viewEditMode.mode], + ); + + const handleSelectMenu = (option: TableOptionsMenu) => { + handleViewNameSubmit(); + setCurrentMenu(option); + }; + + const handleReorderField: OnDragEndResponder = useCallback( + (result) => { + if (!result.destination || result.destination.index === 0) { + return; + } + + const reorderFields = Array.from(visibleTableColumns); + const [removed] = reorderFields.splice(result.source.index, 1); + reorderFields.splice(result.destination.index, 0, removed); + + handleColumnReorder(reorderFields); + }, + [visibleTableColumns, handleColumnReorder], + ); + + const resetMenu = () => setCurrentMenu(undefined); + + useScopedHotkeys( + Key.Escape, + () => { + resetViewEditMode(); + closeDropdown(); + }, + TableOptionsHotkeyScope.Dropdown, + ); + + useScopedHotkeys( + Key.Enter, + () => { + handleViewNameSubmit(); + resetMenu(); + resetViewEditMode(); + closeDropdown(); + }, + TableOptionsHotkeyScope.Dropdown, + ); + + return ( + + {!currentMenu && ( + <> + + + + + + handleSelectMenu('fields')} + LeftIcon={IconTag} + text="Fields" + /> + {onImport && ( + + )} + + + )} + {currentMenu === 'fields' && ( + <> + + Fields + + + + {hiddenTableColumns.length > 0 && ( + <> + + + + )} + + )} + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/options/components/__stories__/TableOptionsDropdown.stories.tsx b/front/src/modules/ui/Data/data-table/options/components/__stories__/TableOptionsDropdown.stories.tsx new file mode 100644 index 0000000000000..b6df874ef188e --- /dev/null +++ b/front/src/modules/ui/Data/data-table/options/components/__stories__/TableOptionsDropdown.stories.tsx @@ -0,0 +1,44 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { TableRecoilScopeContext } from '../../../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { TableOptionsDropdown } from '../TableOptionsDropdown'; + +const meta: Meta = { + title: 'UI/Table/Options/TableOptionsDropdown', + component: TableOptionsDropdown, + decorators: [ + (Story) => ( + + + + + + ), + ComponentDecorator, + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + customHotkeyScope: { scope: 'options' }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const dropdownButton = canvas.getByText('Options'); + + await userEvent.click(dropdownButton); + }, +}; diff --git a/front/src/modules/ui/Data/data-table/states/availableTableColumnsScopedState.ts b/front/src/modules/ui/Data/data-table/states/availableTableColumnsScopedState.ts new file mode 100644 index 0000000000000..2a037a2605639 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/states/availableTableColumnsScopedState.ts @@ -0,0 +1,13 @@ +import { atomFamily } from 'recoil'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../types/ColumnDefinition'; + +export const availableTableColumnsScopedState = atomFamily< + ColumnDefinition[], + string +>({ + key: 'availableTableColumnsScopedState', + default: [], +}); diff --git a/front/src/modules/ui/data-table/states/currentTableCellInEditModePositionState.ts b/front/src/modules/ui/Data/data-table/states/currentTableCellInEditModePositionState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/currentTableCellInEditModePositionState.ts rename to front/src/modules/ui/Data/data-table/states/currentTableCellInEditModePositionState.ts diff --git a/front/src/modules/ui/data-table/states/isFetchingDataTableDataState.ts b/front/src/modules/ui/Data/data-table/states/isFetchingDataTableDataState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/isFetchingDataTableDataState.ts rename to front/src/modules/ui/Data/data-table/states/isFetchingDataTableDataState.ts diff --git a/front/src/modules/ui/data-table/states/isRowSelectedFamilyState.ts b/front/src/modules/ui/Data/data-table/states/isRowSelectedFamilyState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/isRowSelectedFamilyState.ts rename to front/src/modules/ui/Data/data-table/states/isRowSelectedFamilyState.ts diff --git a/front/src/modules/ui/data-table/states/isSoftFocusActiveState.ts b/front/src/modules/ui/Data/data-table/states/isSoftFocusActiveState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/isSoftFocusActiveState.ts rename to front/src/modules/ui/Data/data-table/states/isSoftFocusActiveState.ts diff --git a/front/src/modules/ui/data-table/states/isSoftFocusOnTableCellFamilyState.ts b/front/src/modules/ui/Data/data-table/states/isSoftFocusOnTableCellFamilyState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/isSoftFocusOnTableCellFamilyState.ts rename to front/src/modules/ui/Data/data-table/states/isSoftFocusOnTableCellFamilyState.ts diff --git a/front/src/modules/ui/data-table/states/isTableCellInEditModeFamilyState.ts b/front/src/modules/ui/Data/data-table/states/isTableCellInEditModeFamilyState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/isTableCellInEditModeFamilyState.ts rename to front/src/modules/ui/Data/data-table/states/isTableCellInEditModeFamilyState.ts diff --git a/front/src/modules/ui/data-table/states/numberOfTableRowsState.ts b/front/src/modules/ui/Data/data-table/states/numberOfTableRowsState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/numberOfTableRowsState.ts rename to front/src/modules/ui/Data/data-table/states/numberOfTableRowsState.ts diff --git a/front/src/modules/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext.ts b/front/src/modules/ui/Data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext.ts similarity index 100% rename from front/src/modules/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext.ts rename to front/src/modules/ui/Data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext.ts diff --git a/front/src/modules/ui/data-table/states/resizeFieldOffsetState.ts b/front/src/modules/ui/Data/data-table/states/resizeFieldOffsetState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/resizeFieldOffsetState.ts rename to front/src/modules/ui/Data/data-table/states/resizeFieldOffsetState.ts diff --git a/front/src/modules/ui/Data/data-table/states/savedTableColumnsFamilyState.ts b/front/src/modules/ui/Data/data-table/states/savedTableColumnsFamilyState.ts new file mode 100644 index 0000000000000..3cd0e04cc8fcf --- /dev/null +++ b/front/src/modules/ui/Data/data-table/states/savedTableColumnsFamilyState.ts @@ -0,0 +1,13 @@ +import { atomFamily } from 'recoil'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../types/ColumnDefinition'; + +export const savedTableColumnsFamilyState = atomFamily< + ColumnDefinition[], + string | undefined +>({ + key: 'savedTableColumnsFamilyState', + default: [], +}); diff --git a/front/src/modules/ui/data-table/states/selectors/allRowsSelectedStatusSelector.ts b/front/src/modules/ui/Data/data-table/states/selectors/allRowsSelectedStatusSelector.ts similarity index 100% rename from front/src/modules/ui/data-table/states/selectors/allRowsSelectedStatusSelector.ts rename to front/src/modules/ui/Data/data-table/states/selectors/allRowsSelectedStatusSelector.ts diff --git a/front/src/modules/ui/data-table/states/selectors/hiddenTableColumnsScopedSelector.ts b/front/src/modules/ui/Data/data-table/states/selectors/hiddenTableColumnsScopedSelector.ts similarity index 100% rename from front/src/modules/ui/data-table/states/selectors/hiddenTableColumnsScopedSelector.ts rename to front/src/modules/ui/Data/data-table/states/selectors/hiddenTableColumnsScopedSelector.ts diff --git a/front/src/modules/ui/data-table/states/selectors/numberOfTableColumnsScopedSelector.ts b/front/src/modules/ui/Data/data-table/states/selectors/numberOfTableColumnsScopedSelector.ts similarity index 100% rename from front/src/modules/ui/data-table/states/selectors/numberOfTableColumnsScopedSelector.ts rename to front/src/modules/ui/Data/data-table/states/selectors/numberOfTableColumnsScopedSelector.ts diff --git a/front/src/modules/ui/Data/data-table/states/selectors/savedTableColumnsByKeyFamilySelector.ts b/front/src/modules/ui/Data/data-table/states/selectors/savedTableColumnsByKeyFamilySelector.ts new file mode 100644 index 0000000000000..a16413b9a5ed4 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/states/selectors/savedTableColumnsByKeyFamilySelector.ts @@ -0,0 +1,16 @@ +import { selectorFamily } from 'recoil'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../../types/ColumnDefinition'; +import { savedTableColumnsFamilyState } from '../savedTableColumnsFamilyState'; + +export const savedTableColumnsByKeyFamilySelector = selectorFamily({ + key: 'savedTableColumnsByKeyFamilySelector', + get: + (viewId: string | undefined) => + ({ get }) => + get(savedTableColumnsFamilyState(viewId)).reduce< + Record> + >((result, column) => ({ ...result, [column.key]: column }), {}), +}); diff --git a/front/src/modules/ui/data-table/states/selectors/selectedRowIdsSelector.ts b/front/src/modules/ui/Data/data-table/states/selectors/selectedRowIdsSelector.ts similarity index 100% rename from front/src/modules/ui/data-table/states/selectors/selectedRowIdsSelector.ts rename to front/src/modules/ui/Data/data-table/states/selectors/selectedRowIdsSelector.ts diff --git a/front/src/modules/ui/Data/data-table/states/selectors/tableColumnsByKeyScopedSelector.ts b/front/src/modules/ui/Data/data-table/states/selectors/tableColumnsByKeyScopedSelector.ts new file mode 100644 index 0000000000000..3acfd290db7af --- /dev/null +++ b/front/src/modules/ui/Data/data-table/states/selectors/tableColumnsByKeyScopedSelector.ts @@ -0,0 +1,16 @@ +import { selectorFamily } from 'recoil'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../../types/ColumnDefinition'; +import { tableColumnsScopedState } from '../tableColumnsScopedState'; + +export const tableColumnsByKeyScopedSelector = selectorFamily({ + key: 'tableColumnsByKeyScopedSelector', + get: + (scopeId: string) => + ({ get }) => + get(tableColumnsScopedState(scopeId)).reduce< + Record> + >((result, column) => ({ ...result, [column.key]: column }), {}), +}); diff --git a/front/src/modules/ui/data-table/states/selectors/visibleTableColumnsScopedSelector.ts b/front/src/modules/ui/Data/data-table/states/selectors/visibleTableColumnsScopedSelector.ts similarity index 100% rename from front/src/modules/ui/data-table/states/selectors/visibleTableColumnsScopedSelector.ts rename to front/src/modules/ui/Data/data-table/states/selectors/visibleTableColumnsScopedSelector.ts diff --git a/front/src/modules/ui/data-table/states/softFocusPositionState.ts b/front/src/modules/ui/Data/data-table/states/softFocusPositionState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/softFocusPositionState.ts rename to front/src/modules/ui/Data/data-table/states/softFocusPositionState.ts diff --git a/front/src/modules/ui/Data/data-table/states/tableColumnsScopedState.ts b/front/src/modules/ui/Data/data-table/states/tableColumnsScopedState.ts new file mode 100644 index 0000000000000..9ff30b5bda4ae --- /dev/null +++ b/front/src/modules/ui/Data/data-table/states/tableColumnsScopedState.ts @@ -0,0 +1,13 @@ +import { atomFamily } from 'recoil'; + +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +import { ColumnDefinition } from '../types/ColumnDefinition'; + +export const tableColumnsScopedState = atomFamily< + ColumnDefinition[], + string +>({ + key: 'tableColumnsScopedState', + default: [], +}); diff --git a/front/src/modules/ui/data-table/states/tableRowIdsState.ts b/front/src/modules/ui/Data/data-table/states/tableRowIdsState.ts similarity index 100% rename from front/src/modules/ui/data-table/states/tableRowIdsState.ts rename to front/src/modules/ui/Data/data-table/states/tableRowIdsState.ts diff --git a/front/src/modules/ui/Data/data-table/table-cell/components/TableCell.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCell.tsx new file mode 100644 index 0000000000000..e48383083d7ea --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-cell/components/TableCell.tsx @@ -0,0 +1,77 @@ +import { useContext } from 'react'; + +import { FieldDisplay } from '@/ui/data/field/components/FieldDisplay'; +import { FieldInput } from '@/ui/data/field/components/FieldInput'; +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { FieldInputEvent } from '@/ui/data/field/types/FieldInputEvent'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus'; +import { useTableCell } from '../hooks/useTableCell'; + +import { TableCellContainer } from './TableCellContainer'; + +export const TableCell = ({ + customHotkeyScope, +}: { + customHotkeyScope: HotkeyScope; +}) => { + const { fieldDefinition } = useContext(FieldContext); + + // eslint-disable-next-line no-console + console.log({ fieldDefinition }); + + const { closeTableCell } = useTableCell(); + + const { moveLeft, moveRight, moveDown } = useMoveSoftFocus(); + + const handleEnter: FieldInputEvent = (persistField) => { + persistField(); + closeTableCell(); + moveDown(); + }; + + const handleSubmit: FieldInputEvent = (persistField) => { + persistField(); + closeTableCell(); + }; + + const handleCancel = () => { + closeTableCell(); + }; + + const handleEscape = () => { + closeTableCell(); + }; + + const handleTab: FieldInputEvent = (persistField) => { + persistField(); + closeTableCell(); + moveRight(); + }; + + const handleShiftTab: FieldInputEvent = (persistField) => { + persistField(); + closeTableCell(); + moveLeft(); + }; + + return ( + + } + nonEditModeContent={} + buttonIcon={fieldDefinition.buttonIcon} + > + ); +}; diff --git a/front/src/modules/ui/Data/data-table/table-cell/components/TableCellButton.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellButton.tsx new file mode 100644 index 0000000000000..1cdc93b8ffc88 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellButton.tsx @@ -0,0 +1,26 @@ +import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; + +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; + +const StyledEditButtonContainer = styled(motion.div)` + position: absolute; + right: 5px; +`; + +type TableCellButtonProps = { + onClick?: () => void; + Icon: IconComponent; +}; + +export const TableCellButton = ({ onClick, Icon }: TableCellButtonProps) => ( + + + +); diff --git a/front/src/modules/ui/Data/data-table/table-cell/components/TableCellContainer.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellContainer.tsx new file mode 100644 index 0000000000000..ee5f460bc563f --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellContainer.tsx @@ -0,0 +1,146 @@ +import { ReactElement, useContext, useState } from 'react'; +import styled from '@emotion/styled'; + +import { useIsFieldEmpty } from '@/ui/data/field/hooks/useIsFieldEmpty'; +import { useIsFieldInputOnly } from '@/ui/data/field/hooks/useIsFieldInputOnly'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext'; +import { ColumnIndexContext } from '../../contexts/ColumnIndexContext'; +import { useGetIsSomeCellInEditMode } from '../../hooks/useGetIsSomeCellInEditMode'; +import { useMoveSoftFocusToCurrentCellOnHover } from '../../hooks/useMoveSoftFocusToCurrentCellOnHover'; +import { TableHotkeyScope } from '../../types/TableHotkeyScope'; +import { useCurrentTableCellEditMode } from '../hooks/useCurrentTableCellEditMode'; +import { useIsSoftFocusOnCurrentTableCell } from '../hooks/useIsSoftFocusOnCurrentTableCell'; +import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell'; +import { useTableCell } from '../hooks/useTableCell'; + +import { TableCellButton } from './TableCellButton'; +import { TableCellDisplayMode } from './TableCellDisplayMode'; +import { TableCellEditMode } from './TableCellEditMode'; +import { TableCellSoftFocusMode } from './TableCellSoftFocusMode'; + +const StyledCellBaseContainer = styled.div` + align-items: center; + box-sizing: border-box; + cursor: pointer; + display: flex; + height: 32px; + position: relative; + user-select: none; +`; + +export type TableCellContainerProps = { + editModeContent: ReactElement; + nonEditModeContent: ReactElement; + editModeHorizontalAlign?: 'left' | 'right'; + editModeVerticalPosition?: 'over' | 'below'; + editHotkeyScope?: HotkeyScope; + transparent?: boolean; + maxContentWidth?: number; + buttonIcon?: IconComponent; + onSubmit?: () => void; + onCancel?: () => void; +}; + +const DEFAULT_CELL_SCOPE: HotkeyScope = { + scope: TableHotkeyScope.CellEditMode, +}; + +export const TableCellContainer = ({ + editModeHorizontalAlign = 'left', + editModeVerticalPosition = 'over', + editModeContent, + nonEditModeContent, + editHotkeyScope, + transparent = false, + maxContentWidth, + buttonIcon, +}: TableCellContainerProps) => { + const { isCurrentTableCellInEditMode } = useCurrentTableCellEditMode(); + + const getIsSomeCellInEditMode = useGetIsSomeCellInEditMode(); + + const [isHovered, setIsHovered] = useState(false); + + const moveSoftFocusToCurrentCellOnHover = + useMoveSoftFocusToCurrentCellOnHover(); + + const hasSoftFocus = useIsSoftFocusOnCurrentTableCell(); + + const setSoftFocusOnCurrentTableCell = useSetSoftFocusOnCurrentTableCell(); + + const { openTableCell } = useTableCell(); + + const handleButtonClick = () => { + setSoftFocusOnCurrentTableCell(); + openTableCell(); + }; + + const handleContainerMouseEnter = () => { + const isSomeCellInEditMode = getIsSomeCellInEditMode(); + + if (!isHovered && !isSomeCellInEditMode) { + setIsHovered(true); + moveSoftFocusToCurrentCellOnHover(); + } + }; + + const handleContainerMouseLeave = () => { + setIsHovered(false); + }; + + const editModeContentOnly = useIsFieldInputOnly(); + + const isFirstColumnCell = useContext(ColumnIndexContext) === 0; + + const isEmpty = useIsFieldEmpty(); + + const showButton = + !!buttonIcon && + hasSoftFocus && + !isCurrentTableCellInEditMode && + !editModeContentOnly && + (!isFirstColumnCell || !isEmpty); + + return ( + + + {isCurrentTableCellInEditMode ? ( + + {editModeContent} + + ) : hasSoftFocus ? ( + <> + {showButton && ( + + )} + + {editModeContentOnly ? editModeContent : nonEditModeContent} + + + ) : ( + <> + {showButton && ( + + )} + + {editModeContentOnly ? editModeContent : nonEditModeContent} + + + )} + + + ); +}; diff --git a/front/src/modules/ui/data-table/table-cell/components/TableCellDisplayContainer.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellDisplayContainer.tsx similarity index 100% rename from front/src/modules/ui/data-table/table-cell/components/TableCellDisplayContainer.tsx rename to front/src/modules/ui/Data/data-table/table-cell/components/TableCellDisplayContainer.tsx diff --git a/front/src/modules/ui/Data/data-table/table-cell/components/TableCellDisplayMode.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellDisplayMode.tsx new file mode 100644 index 0000000000000..0f117663f4d20 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellDisplayMode.tsx @@ -0,0 +1,30 @@ +import { useIsFieldInputOnly } from '@/ui/data/field/hooks/useIsFieldInputOnly'; + +import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell'; +import { useTableCell } from '../hooks/useTableCell'; + +import { TableCellDisplayContainer } from './TableCellDisplayContainer'; + +export const TableCellDisplayMode = ({ + children, +}: React.PropsWithChildren) => { + const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentTableCell(); + + const isFieldInputOnly = useIsFieldInputOnly(); + + const { openTableCell } = useTableCell(); + + const handleClick = () => { + setSoftFocusOnCurrentCell(); + + if (!isFieldInputOnly) { + openTableCell(); + } + }; + + return ( + + {children} + + ); +}; diff --git a/front/src/modules/ui/Data/data-table/table-cell/components/TableCellEditButton.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellEditButton.tsx new file mode 100644 index 0000000000000..1cdc93b8ffc88 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellEditButton.tsx @@ -0,0 +1,26 @@ +import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; + +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; + +const StyledEditButtonContainer = styled(motion.div)` + position: absolute; + right: 5px; +`; + +type TableCellButtonProps = { + onClick?: () => void; + Icon: IconComponent; +}; + +export const TableCellButton = ({ onClick, Icon }: TableCellButtonProps) => ( + + + +); diff --git a/front/src/modules/ui/data-table/table-cell/components/TableCellEditMode.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellEditMode.tsx similarity index 100% rename from front/src/modules/ui/data-table/table-cell/components/TableCellEditMode.tsx rename to front/src/modules/ui/Data/data-table/table-cell/components/TableCellEditMode.tsx diff --git a/front/src/modules/ui/Data/data-table/table-cell/components/TableCellSoftFocusMode.tsx b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellSoftFocusMode.tsx new file mode 100644 index 0000000000000..1a2acbb59676d --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-cell/components/TableCellSoftFocusMode.tsx @@ -0,0 +1,76 @@ +import { PropsWithChildren, useEffect, useRef } from 'react'; + +import { useIsFieldInputOnly } from '@/ui/data/field/hooks/useIsFieldInputOnly'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey'; + +import { TableHotkeyScope } from '../../types/TableHotkeyScope'; +import { useTableCell } from '../hooks/useTableCell'; + +import { TableCellDisplayContainer } from './TableCellDisplayContainer'; + +type TableCellSoftFocusModeProps = PropsWithChildren; + +export const TableCellSoftFocusMode = ({ + children, +}: TableCellSoftFocusModeProps) => { + const { openTableCell } = useTableCell(); + + const isFieldInputOnly = useIsFieldInputOnly(); + + const scrollRef = useRef(null); + + useEffect(() => { + scrollRef.current?.scrollIntoView({ block: 'nearest' }); + }, []); + + useScopedHotkeys( + 'enter', + () => { + openTableCell(); + }, + TableHotkeyScope.TableSoftFocus, + [openTableCell], + { + enabled: !isFieldInputOnly, + }, + ); + + useScopedHotkeys( + '*', + (keyboardEvent) => { + const isWritingText = + !isNonTextWritingKey(keyboardEvent.key) && + !keyboardEvent.ctrlKey && + !keyboardEvent.metaKey; + + if (!isWritingText) { + return; + } + + openTableCell(); + }, + TableHotkeyScope.TableSoftFocus, + [openTableCell], + { + preventDefault: false, + enabled: !isFieldInputOnly, + }, + ); + + const handleClick = () => { + if (!isFieldInputOnly) { + openTableCell(); + } + }; + + return ( + + {children} + + ); +}; diff --git a/front/src/modules/ui/data-table/table-cell/hooks/useCurrentCellPosition.ts b/front/src/modules/ui/Data/data-table/table-cell/hooks/useCurrentCellPosition.ts similarity index 100% rename from front/src/modules/ui/data-table/table-cell/hooks/useCurrentCellPosition.ts rename to front/src/modules/ui/Data/data-table/table-cell/hooks/useCurrentCellPosition.ts diff --git a/front/src/modules/ui/data-table/table-cell/hooks/useCurrentTableCellEditMode.ts b/front/src/modules/ui/Data/data-table/table-cell/hooks/useCurrentTableCellEditMode.ts similarity index 100% rename from front/src/modules/ui/data-table/table-cell/hooks/useCurrentTableCellEditMode.ts rename to front/src/modules/ui/Data/data-table/table-cell/hooks/useCurrentTableCellEditMode.ts diff --git a/front/src/modules/ui/data-table/table-cell/hooks/useIsSoftFocusOnCurrentTableCell.ts b/front/src/modules/ui/Data/data-table/table-cell/hooks/useIsSoftFocusOnCurrentTableCell.ts similarity index 100% rename from front/src/modules/ui/data-table/table-cell/hooks/useIsSoftFocusOnCurrentTableCell.ts rename to front/src/modules/ui/Data/data-table/table-cell/hooks/useIsSoftFocusOnCurrentTableCell.ts diff --git a/front/src/modules/ui/data-table/table-cell/hooks/useSetSoftFocusOnCurrentTableCell.ts b/front/src/modules/ui/Data/data-table/table-cell/hooks/useSetSoftFocusOnCurrentTableCell.ts similarity index 100% rename from front/src/modules/ui/data-table/table-cell/hooks/useSetSoftFocusOnCurrentTableCell.ts rename to front/src/modules/ui/Data/data-table/table-cell/hooks/useSetSoftFocusOnCurrentTableCell.ts diff --git a/front/src/modules/ui/Data/data-table/table-cell/hooks/useTableCell.ts b/front/src/modules/ui/Data/data-table/table-cell/hooks/useTableCell.ts new file mode 100644 index 0000000000000..a1b7c8d5acea7 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-cell/hooks/useTableCell.ts @@ -0,0 +1,68 @@ +import { useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { useIsFieldEmpty } from '@/ui/data/field/hooks/useIsFieldEmpty'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext'; +import { ColumnIndexContext } from '../../contexts/ColumnIndexContext'; +import { useCloseCurrentTableCellInEditMode } from '../../hooks/useCloseCurrentTableCellInEditMode'; +import { TableHotkeyScope } from '../../types/TableHotkeyScope'; + +import { useCurrentTableCellEditMode } from './useCurrentTableCellEditMode'; + +const DEFAULT_CELL_SCOPE: HotkeyScope = { + scope: TableHotkeyScope.CellEditMode, +}; + +export const useTableCell = () => { + const { setCurrentTableCellInEditMode } = useCurrentTableCellEditMode(); + + const setHotkeyScope = useSetHotkeyScope(); + const { setDragSelectionStartEnabled } = useDragSelect(); + + const closeCurrentTableCellInEditMode = useCloseCurrentTableCellInEditMode(); + + const customCellHotkeyScope = useContext(CellHotkeyScopeContext); + + const closeTableCell = () => { + setDragSelectionStartEnabled(true); + closeCurrentTableCellInEditMode(); + setHotkeyScope(TableHotkeyScope.TableSoftFocus); + }; + + const navigate = useNavigate(); + + const isFirstColumnCell = useContext(ColumnIndexContext) === 0; + + const isEmpty = useIsFieldEmpty(); + + const { entityId, fieldDefinition } = useContext(FieldContext); + + const openTableCell = () => { + if (isFirstColumnCell && !isEmpty && fieldDefinition.basePathToShowPage) { + navigate(`${fieldDefinition.basePathToShowPage}${entityId}`); + return; + } + + setDragSelectionStartEnabled(false); + setCurrentTableCellInEditMode(); + + if (customCellHotkeyScope) { + setHotkeyScope( + customCellHotkeyScope.scope, + customCellHotkeyScope.customScopes, + ); + } else { + setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes); + } + }; + + return { + closeTableCell, + openTableCell, + }; +}; diff --git a/front/src/modules/ui/data-table/table-cell/states/isCreateModeScopedState.ts b/front/src/modules/ui/Data/data-table/table-cell/states/isCreateModeScopedState.ts similarity index 100% rename from front/src/modules/ui/data-table/table-cell/states/isCreateModeScopedState.ts rename to front/src/modules/ui/Data/data-table/table-cell/states/isCreateModeScopedState.ts diff --git a/front/src/modules/ui/Data/data-table/table-header/components/TableHeader.tsx b/front/src/modules/ui/Data/data-table/table-header/components/TableHeader.tsx new file mode 100644 index 0000000000000..37c7b4c2797cd --- /dev/null +++ b/front/src/modules/ui/Data/data-table/table-header/components/TableHeader.tsx @@ -0,0 +1,52 @@ +import { useContext } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useRecoilCallback } from 'recoil'; + +import { ViewBar } from '@/ui/data/view-bar/components/ViewBar'; +import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; +import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; + +import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; +import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown'; +import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; +import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState'; +import { tableColumnsScopedState } from '../../states/tableColumnsScopedState'; +import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; + +export const TableHeader = () => { + const { onCurrentViewSubmit, ...viewBarContextProps } = + useContext(ViewBarContext); + const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext); + const [_, setSearchParams] = useSearchParams(); + + const handleViewSelect = useRecoilCallback( + ({ set, snapshot }) => + async (viewId: string) => { + const savedTableColumns = await snapshot.getPromise( + savedTableColumnsFamilyState(viewId), + ); + set(tableColumnsScopedState(tableRecoilScopeId), savedTableColumns); + setSearchParams({ view: viewId }); + }, + [tableRecoilScopeId, setSearchParams], + ); + + return ( + + + } + optionsDropdownScopeId={TableOptionsDropdownId} + /> + + ); +}; diff --git a/front/src/modules/ui/data-table/types/AllRowSelectedStatus.ts b/front/src/modules/ui/Data/data-table/types/AllRowSelectedStatus.ts similarity index 100% rename from front/src/modules/ui/data-table/types/AllRowSelectedStatus.ts rename to front/src/modules/ui/Data/data-table/types/AllRowSelectedStatus.ts diff --git a/front/src/modules/ui/Data/data-table/types/ColumnDefinition.ts b/front/src/modules/ui/Data/data-table/types/ColumnDefinition.ts new file mode 100644 index 0000000000000..bbefe0eeddb61 --- /dev/null +++ b/front/src/modules/ui/Data/data-table/types/ColumnDefinition.ts @@ -0,0 +1,8 @@ +import { FieldDefinition } from '@/ui/data/field/types/FieldDefinition'; +import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata'; + +export type ColumnDefinition = FieldDefinition & { + size: number; + index: number; + isVisible?: boolean; +}; diff --git a/front/src/modules/ui/data-table/types/TableCellPosition.ts b/front/src/modules/ui/Data/data-table/types/TableCellPosition.ts similarity index 100% rename from front/src/modules/ui/data-table/types/TableCellPosition.ts rename to front/src/modules/ui/Data/data-table/types/TableCellPosition.ts diff --git a/front/src/modules/ui/data-table/types/TableDimensions.ts b/front/src/modules/ui/Data/data-table/types/TableDimensions.ts similarity index 100% rename from front/src/modules/ui/data-table/types/TableDimensions.ts rename to front/src/modules/ui/Data/data-table/types/TableDimensions.ts diff --git a/front/src/modules/ui/data-table/types/TableHotkeyScope.ts b/front/src/modules/ui/Data/data-table/types/TableHotkeyScope.ts similarity index 100% rename from front/src/modules/ui/data-table/types/TableHotkeyScope.ts rename to front/src/modules/ui/Data/data-table/types/TableHotkeyScope.ts diff --git a/front/src/modules/ui/data-table/types/TableOptionsHotkeyScope.ts b/front/src/modules/ui/Data/data-table/types/TableOptionsHotkeyScope.ts similarity index 100% rename from front/src/modules/ui/data-table/types/TableOptionsHotkeyScope.ts rename to front/src/modules/ui/Data/data-table/types/TableOptionsHotkeyScope.ts diff --git a/front/src/modules/ui/Data/inline-cell/components/InlineCell.tsx b/front/src/modules/ui/Data/inline-cell/components/InlineCell.tsx new file mode 100644 index 0000000000000..89343616d9f49 --- /dev/null +++ b/front/src/modules/ui/Data/inline-cell/components/InlineCell.tsx @@ -0,0 +1,86 @@ +import { useContext } from 'react'; + +import { FieldDisplay } from '@/ui/data/field/components/FieldDisplay'; +import { FieldInput } from '@/ui/data/field/components/FieldInput'; +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { useIsFieldEmpty } from '@/ui/data/field/hooks/useIsFieldEmpty'; +import { useIsFieldInputOnly } from '@/ui/data/field/hooks/useIsFieldInputOnly'; +import { FieldInputEvent } from '@/ui/data/field/types/FieldInputEvent'; +import { isFieldRelation } from '@/ui/data/field/types/guards/isFieldRelation'; +import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; + +import { useInlineCell } from '../hooks/useInlineCell'; + +import { InlineCellContainer } from './InlineCellContainer'; + +export const InlineCell = () => { + const { fieldDefinition } = useContext(FieldContext); + + const isFieldEmpty = useIsFieldEmpty(); + + const isFieldInputOnly = useIsFieldInputOnly(); + + const { closeInlineCell } = useInlineCell(); + + const handleEnter: FieldInputEvent = (persistField) => { + persistField(); + closeInlineCell(); + }; + + const handleSubmit: FieldInputEvent = (persistField) => { + persistField(); + closeInlineCell(); + }; + + const handleCancel = () => { + closeInlineCell(); + }; + + const handleEscape = () => { + closeInlineCell(); + }; + + const handleTab: FieldInputEvent = (persistField) => { + persistField(); + closeInlineCell(); + }; + + const handleShiftTab: FieldInputEvent = (persistField) => { + persistField(); + closeInlineCell(); + }; + + const handleClickOutside: FieldInputEvent = (persistField) => { + persistField(); + closeInlineCell(); + }; + + return ( + + } + displayModeContent={} + isDisplayModeContentEmpty={isFieldEmpty} + isDisplayModeFixHeight + editModeContentOnly={isFieldInputOnly} + /> + ); +}; diff --git a/front/src/modules/ui/Data/inline-cell/components/InlineCellContainer.tsx b/front/src/modules/ui/Data/inline-cell/components/InlineCellContainer.tsx new file mode 100644 index 0000000000000..bec3102b6fe5f --- /dev/null +++ b/front/src/modules/ui/Data/inline-cell/components/InlineCellContainer.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; + +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { useInlineCell } from '../hooks/useInlineCell'; + +import { InlineCellDisplayMode } from './InlineCellDisplayMode'; +import { InlineCellButton } from './InlineCellEditButton'; +import { InlineCellEditMode } from './InlineCellEditMode'; + +const StyledIconContainer = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + width: 16px; + + svg { + align-items: center; + display: flex; + height: 16px; + justify-content: center; + width: 16px; + } +`; + +const StyledLabelAndIconContainer = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledValueContainer = styled.div` + display: flex; + max-width: calc(100% - ${({ theme }) => theme.spacing(4)}); +`; + +const StyledLabel = styled.div< + Pick +>` + align-items: center; + + width: ${({ labelFixedWidth }) => + labelFixedWidth ? `${labelFixedWidth}px` : 'fit-content'}; +`; + +const StyledEditButtonContainer = styled(motion.div)` + align-items: center; + display: flex; +`; + +const StyledClickableContainer = styled.div` + cursor: pointer; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + width: 100%; +`; + +const StyledInlineCellBaseContainer = styled.div` + align-items: center; + box-sizing: border-box; + + display: flex; + + gap: ${({ theme }) => theme.spacing(1)}; + + position: relative; + user-select: none; + + width: 100%; +`; + +type InlineCellContainerProps = { + IconLabel?: IconComponent; + label?: string; + labelFixedWidth?: number; + buttonIcon?: IconComponent; + editModeContent?: React.ReactNode; + editModeContentOnly?: boolean; + displayModeContent: React.ReactNode; + customEditHotkeyScope?: HotkeyScope; + isDisplayModeContentEmpty?: boolean; + isDisplayModeFixHeight?: boolean; + disableHoverEffect?: boolean; +}; + +export const InlineCellContainer = ({ + IconLabel, + label, + labelFixedWidth, + buttonIcon, + editModeContent, + displayModeContent, + customEditHotkeyScope, + isDisplayModeContentEmpty, + editModeContentOnly, + isDisplayModeFixHeight, + disableHoverEffect, +}: InlineCellContainerProps) => { + const [isHovered, setIsHovered] = useState(false); + + const handleContainerMouseEnter = () => { + setIsHovered(true); + }; + + const handleContainerMouseLeave = () => { + setIsHovered(false); + }; + + const { isInlineCellInEditMode, openInlineCell } = useInlineCell(); + + const handleDisplayModeClick = () => { + if (!editModeContentOnly) { + openInlineCell(customEditHotkeyScope); + } + }; + + const showEditButton = + buttonIcon && !isInlineCellInEditMode && isHovered && !editModeContentOnly; + + const theme = useTheme(); + + return ( + + + {IconLabel && ( + + + + )} + {label && ( + {label} + )} + + + {isInlineCellInEditMode ? ( + {editModeContent} + ) : editModeContentOnly ? ( + + + {editModeContent} + + + ) : ( + + + {displayModeContent} + + {showEditButton && ( + + + + )} + + )} + + + ); +}; diff --git a/front/src/modules/ui/inline-cell/components/InlineCellDisplayMode.tsx b/front/src/modules/ui/Data/inline-cell/components/InlineCellDisplayMode.tsx similarity index 100% rename from front/src/modules/ui/inline-cell/components/InlineCellDisplayMode.tsx rename to front/src/modules/ui/Data/inline-cell/components/InlineCellDisplayMode.tsx diff --git a/front/src/modules/ui/Data/inline-cell/components/InlineCellEditButton.tsx b/front/src/modules/ui/Data/inline-cell/components/InlineCellEditButton.tsx new file mode 100644 index 0000000000000..63d7eb4708cbd --- /dev/null +++ b/front/src/modules/ui/Data/inline-cell/components/InlineCellEditButton.tsx @@ -0,0 +1,21 @@ +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; + +import { useInlineCell } from '../hooks/useInlineCell'; + +export const InlineCellButton = ({ Icon }: { Icon: IconComponent }) => { + const { openInlineCell } = useInlineCell(); + + const handleClick = () => { + openInlineCell(); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/inline-cell/components/InlineCellEditMode.tsx b/front/src/modules/ui/Data/inline-cell/components/InlineCellEditMode.tsx similarity index 100% rename from front/src/modules/ui/inline-cell/components/InlineCellEditMode.tsx rename to front/src/modules/ui/Data/inline-cell/components/InlineCellEditMode.tsx diff --git a/front/src/modules/ui/inline-cell/contexts/InlineCellMutationContext.ts b/front/src/modules/ui/Data/inline-cell/contexts/InlineCellMutationContext.ts similarity index 100% rename from front/src/modules/ui/inline-cell/contexts/InlineCellMutationContext.ts rename to front/src/modules/ui/Data/inline-cell/contexts/InlineCellMutationContext.ts diff --git a/front/src/modules/ui/Data/inline-cell/hooks/useInlineCell.ts b/front/src/modules/ui/Data/inline-cell/hooks/useInlineCell.ts new file mode 100644 index 0000000000000..bf4834d56446a --- /dev/null +++ b/front/src/modules/ui/Data/inline-cell/hooks/useInlineCell.ts @@ -0,0 +1,47 @@ +import { useContext } from 'react'; +import { useRecoilState } from 'recoil'; + +import { FieldContext } from '@/ui/data/field/contexts/FieldContext'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; + +import { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState'; +import { InlineCellHotkeyScope } from '../types/InlineCellHotkeyScope'; + +export const useInlineCell = () => { + const { recoilScopeId } = useContext(FieldContext); + + const [isInlineCellInEditMode, setIsInlineCellInEditMode] = useRecoilState( + isInlineCellInEditModeScopedState(recoilScopeId), + ); + + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + + const closeInlineCell = () => { + setIsInlineCellInEditMode(false); + + goBackToPreviousHotkeyScope(); + }; + + const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => { + setIsInlineCellInEditMode(true); + + if (customEditHotkeyScopeForField) { + setHotkeyScopeAndMemorizePreviousScope( + customEditHotkeyScopeForField.scope, + customEditHotkeyScopeForField.customScopes, + ); + } else { + setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell); + } + }; + + return { + isInlineCellInEditMode, + closeInlineCell, + openInlineCell, + }; +}; diff --git a/front/src/modules/ui/inline-cell/property-box/components/PropertyBox.tsx b/front/src/modules/ui/Data/inline-cell/property-box/components/PropertyBox.tsx similarity index 100% rename from front/src/modules/ui/inline-cell/property-box/components/PropertyBox.tsx rename to front/src/modules/ui/Data/inline-cell/property-box/components/PropertyBox.tsx diff --git a/front/src/modules/ui/inline-cell/states/customEditHotkeyScopeForFieldScopedState.ts b/front/src/modules/ui/Data/inline-cell/states/customEditHotkeyScopeForFieldScopedState.ts similarity index 100% rename from front/src/modules/ui/inline-cell/states/customEditHotkeyScopeForFieldScopedState.ts rename to front/src/modules/ui/Data/inline-cell/states/customEditHotkeyScopeForFieldScopedState.ts diff --git a/front/src/modules/ui/inline-cell/states/isInlineCellInEditModeScopedState.ts b/front/src/modules/ui/Data/inline-cell/states/isInlineCellInEditModeScopedState.ts similarity index 100% rename from front/src/modules/ui/inline-cell/states/isInlineCellInEditModeScopedState.ts rename to front/src/modules/ui/Data/inline-cell/states/isInlineCellInEditModeScopedState.ts diff --git a/front/src/modules/ui/inline-cell/states/parentHotkeyScopeForFieldScopedState.ts b/front/src/modules/ui/Data/inline-cell/states/parentHotkeyScopeForFieldScopedState.ts similarity index 100% rename from front/src/modules/ui/inline-cell/states/parentHotkeyScopeForFieldScopedState.ts rename to front/src/modules/ui/Data/inline-cell/states/parentHotkeyScopeForFieldScopedState.ts diff --git a/front/src/modules/ui/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext.ts b/front/src/modules/ui/Data/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext.ts similarity index 100% rename from front/src/modules/ui/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext.ts rename to front/src/modules/ui/Data/inline-cell/states/recoil-scope-contexts/FieldRecoilScopeContext.ts diff --git a/front/src/modules/ui/inline-cell/types/InlineCellHotkeyScope.ts b/front/src/modules/ui/Data/inline-cell/types/InlineCellHotkeyScope.ts similarity index 100% rename from front/src/modules/ui/inline-cell/types/InlineCellHotkeyScope.ts rename to front/src/modules/ui/Data/inline-cell/types/InlineCellHotkeyScope.ts diff --git a/front/src/modules/ui/Data/view-bar/components/AddFilterFromDetailsButton.tsx b/front/src/modules/ui/Data/view-bar/components/AddFilterFromDetailsButton.tsx new file mode 100644 index 0000000000000..a1f3f01ce2292 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/AddFilterFromDetailsButton.tsx @@ -0,0 +1,24 @@ +import { IconPlus } from '@/ui/display/icon'; +import { LightButton } from '@/ui/input/button/components/LightButton'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; + +import { FilterDropdownId } from '../constants/FilterDropdownId'; + +export const AddFilterFromDropdownButton = () => { + const { toggleDropdown } = useDropdown({ + dropdownScopeId: FilterDropdownId, + }); + + const handleClick = () => { + toggleDropdown(); + }; + + return ( + } + title="Add filter" + accent="tertiary" + /> + ); +}; diff --git a/front/src/modules/ui/view-bar/components/FilterDropdownButton.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownButton.tsx similarity index 100% rename from front/src/modules/ui/view-bar/components/FilterDropdownButton.tsx rename to front/src/modules/ui/Data/view-bar/components/FilterDropdownButton.tsx diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownDateSearchInput.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownDateSearchInput.tsx new file mode 100644 index 0000000000000..0541e0223cbc3 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownDateSearchInput.tsx @@ -0,0 +1,51 @@ +import { useUpsertFilter } from '@/ui/data/view-bar/hooks/useUpsertFilter'; +import { filterDefinitionUsedInDropdownScopedState } from '@/ui/data/view-bar/states/filterDefinitionUsedInDropdownScopedState'; +import { selectedOperandInDropdownScopedState } from '@/ui/data/view-bar/states/selectedOperandInDropdownScopedState'; +import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState'; + +export const FilterDropdownDateSearchInput = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [selectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setIsFilterDropdownUnfolded] = useRecoilScopedState( + isFilterDropdownUnfoldedScopedState, + ViewBarRecoilScopeContext, + ); + + const upsertFilter = useUpsertFilter(); + + const handleChange = (date: Date) => { + if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return; + + upsertFilter({ + key: filterDefinitionUsedInDropdown.key, + type: filterDefinitionUsedInDropdown.type, + value: date.toISOString(), + operand: selectedOperandInDropdown, + displayValue: date.toLocaleDateString(), + }); + + setIsFilterDropdownUnfolded(false); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySearchInput.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySearchInput.tsx new file mode 100644 index 0000000000000..d6b118b38f61d --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySearchInput.tsx @@ -0,0 +1,44 @@ +import { ChangeEvent } from 'react'; + +import { filterDefinitionUsedInDropdownScopedState } from '@/ui/data/view-bar/states/filterDefinitionUsedInDropdownScopedState'; +import { filterDropdownSearchInputScopedState } from '@/ui/data/view-bar/states/filterDropdownSearchInputScopedState'; +import { selectedOperandInDropdownScopedState } from '@/ui/data/view-bar/states/selectedOperandInDropdownScopedState'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; + +export const FilterDropdownEntitySearchInput = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [selectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [filterDropdownSearchInput, setFilterDropdownSearchInput] = + useRecoilScopedState( + filterDropdownSearchInputScopedState, + ViewBarRecoilScopeContext, + ); + + return ( + filterDefinitionUsedInDropdown && + selectedOperandInDropdown && ( + ) => { + setFilterDropdownSearchInput(event.target.value); + }} + /> + ) + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySearchSelect.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySearchSelect.tsx new file mode 100644 index 0000000000000..e044464d2d83f --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySearchSelect.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react'; + +import { useFilterCurrentlyEdited } from '@/ui/data/view-bar/hooks/useFilterCurrentlyEdited'; +import { useRemoveFilter } from '@/ui/data/view-bar/hooks/useRemoveFilter'; +import { useUpsertFilter } from '@/ui/data/view-bar/hooks/useUpsertFilter'; +import { filterDefinitionUsedInDropdownScopedState } from '@/ui/data/view-bar/states/filterDefinitionUsedInDropdownScopedState'; +import { filterDropdownSelectedEntityIdScopedState } from '@/ui/data/view-bar/states/filterDropdownSelectedEntityIdScopedState'; +import { selectedOperandInDropdownScopedState } from '@/ui/data/view-bar/states/selectedOperandInDropdownScopedState'; +import { EntitiesForMultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect'; +import { SingleEntitySelectBase } from '@/ui/input/relation-picker/components/SingleEntitySelectBase'; +import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { ViewFilterOperand } from '~/generated/graphql'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState'; + +export const FilterDropdownEntitySearchSelect = ({ + entitiesForSelect, +}: { + entitiesForSelect: EntitiesForMultipleEntitySelect; +}) => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [isAllEntitySelected, setIsAllEntitySelected] = useState(false); + + const [filterDropdownSelectedEntityId, setFilterDropdownSelectedEntityId] = + useRecoilScopedState( + filterDropdownSelectedEntityIdScopedState, + ViewBarRecoilScopeContext, + ); + + const [selectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const upsertFilter = useUpsertFilter(); + const removeFilter = useRemoveFilter(); + + const filterCurrentlyEdited = useFilterCurrentlyEdited(); + + const handleUserSelected = ( + selectedEntity: EntityForSelect | null | undefined, + ) => { + if ( + !filterDefinitionUsedInDropdown || + !selectedOperandInDropdown || + !selectedEntity + ) { + return; + } + + if (isAllEntitySelected) { + setIsAllEntitySelected(false); + } + + const clickedOnAlreadySelectedEntity = + selectedEntity.id === filterDropdownSelectedEntityId; + + if (clickedOnAlreadySelectedEntity) { + removeFilter(filterDefinitionUsedInDropdown.key); + setFilterDropdownSelectedEntityId(null); + } else { + setFilterDropdownSelectedEntityId(selectedEntity.id); + + upsertFilter({ + displayValue: selectedEntity.name, + key: filterDefinitionUsedInDropdown.key, + operand: selectedOperandInDropdown, + type: filterDefinitionUsedInDropdown.type, + value: selectedEntity.id, + displayAvatarUrl: selectedEntity.avatarUrl, + }); + } + }; + + const [filterDropdownSearchInput] = useRecoilScopedState( + filterDropdownSearchInputScopedState, + ViewBarRecoilScopeContext, + ); + + const isAllEntitySelectShown = + !!filterDefinitionUsedInDropdown?.selectAllLabel && + !!filterDefinitionUsedInDropdown?.SelectAllIcon && + (isAllEntitySelected || + filterDefinitionUsedInDropdown?.selectAllLabel + .toLocaleLowerCase() + .includes(filterDropdownSearchInput.toLocaleLowerCase())); + + const handleAllEntitySelectClick = () => { + if ( + !filterDefinitionUsedInDropdown || + !selectedOperandInDropdown || + !filterDefinitionUsedInDropdown.selectAllLabel + ) { + return; + } + if (isAllEntitySelected) { + setIsAllEntitySelected(false); + + removeFilter(filterDefinitionUsedInDropdown.key); + } else { + setIsAllEntitySelected(true); + + setFilterDropdownSelectedEntityId(null); + + upsertFilter({ + displayValue: filterDefinitionUsedInDropdown.selectAllLabel, + key: filterDefinitionUsedInDropdown.key, + operand: ViewFilterOperand.IsNotNull, + type: filterDefinitionUsedInDropdown.type, + value: '', + }); + } + }; + + useEffect(() => { + if (!filterCurrentlyEdited) { + setFilterDropdownSelectedEntityId(null); + } else { + setFilterDropdownSelectedEntityId(filterCurrentlyEdited.value); + setIsAllEntitySelected( + filterCurrentlyEdited.operand === ViewFilterOperand.IsNotNull, + ); + } + }, [ + filterCurrentlyEdited, + setFilterDropdownSelectedEntityId, + entitiesForSelect.selectedEntities, + ]); + + return ( + <> + + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySelect.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySelect.tsx new file mode 100644 index 0000000000000..59df710714883 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownEntitySelect.tsx @@ -0,0 +1,28 @@ +import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator'; +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; + +export const FilterDropdownEntitySelect = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + if (filterDefinitionUsedInDropdown?.type !== 'entity') { + return null; + } + + return ( + <> + + + {filterDefinitionUsedInDropdown.entitySelectComponent} + + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownFilterSelect.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownFilterSelect.tsx new file mode 100644 index 0000000000000..105dd38ced002 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownFilterSelect.tsx @@ -0,0 +1,65 @@ +import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { availableFiltersScopedState } from '../states/availableFiltersScopedState'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; +import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; +import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; + +export const FilterDropdownFilterSelect = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setSelectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setFilterDropdownSearchInput] = useRecoilScopedState( + filterDropdownSearchInputScopedState, + ViewBarRecoilScopeContext, + ); + + const availableFilters = useRecoilScopedValue( + availableFiltersScopedState, + ViewBarRecoilScopeContext, + ); + + const setHotkeyScope = useSetHotkeyScope(); + + return ( + + {availableFilters.map((availableFilter, index) => ( + { + setFilterDefinitionUsedInDropdown(availableFilter); + + if (availableFilter.type === 'entity') { + setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); + } + + setSelectedOperandInDropdown( + getOperandsForFilterType(availableFilter.type)?.[0], + ); + + setFilterDropdownSearchInput(''); + }} + LeftIcon={availableFilter.Icon} + text={availableFilter.label} + /> + ))} + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownNumberSearchInput.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownNumberSearchInput.tsx new file mode 100644 index 0000000000000..11cb6593f0ec9 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownNumberSearchInput.tsx @@ -0,0 +1,51 @@ +import { ChangeEvent } from 'react'; + +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useRemoveFilter } from '../hooks/useRemoveFilter'; +import { useUpsertFilter } from '../hooks/useUpsertFilter'; +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; + +export const FilterDropdownNumberSearchInput = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [selectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const upsertFilter = useUpsertFilter(); + const removeFilter = useRemoveFilter(); + + return ( + filterDefinitionUsedInDropdown && + selectedOperandInDropdown && ( + ) => { + if (event.target.value === '') { + removeFilter(filterDefinitionUsedInDropdown.key); + } else { + upsertFilter({ + key: filterDefinitionUsedInDropdown.key, + type: filterDefinitionUsedInDropdown.type, + value: event.target.value, + operand: selectedOperandInDropdown, + displayValue: event.target.value, + }); + } + }} + /> + ) + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownOperandButton.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownOperandButton.tsx new file mode 100644 index 0000000000000..33eb3fd770122 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownOperandButton.tsx @@ -0,0 +1,39 @@ +import { IconChevronDown } from '@/ui/display/icon'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; +import { getOperandLabel } from '../utils/getOperandLabel'; + +export const FilterDropdownOperandButton = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [selectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [ + isFilterDropdownOperandSelectUnfolded, + setIsFilterDropdownOperandSelectUnfolded, + ] = useRecoilScopedState( + isFilterDropdownOperandSelectUnfoldedScopedState, + ViewBarRecoilScopeContext, + ); + + if (isFilterDropdownOperandSelectUnfolded) { + return null; + } + + return ( + setIsFilterDropdownOperandSelectUnfolded(true)} + > + {getOperandLabel(selectedOperandInDropdown)} + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownOperandSelect.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownOperandSelect.tsx new file mode 100644 index 0000000000000..abd243504cb73 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownOperandSelect.tsx @@ -0,0 +1,76 @@ +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { ViewFilterOperand } from '~/generated/graphql'; + +import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited'; +import { useUpsertFilter } from '../hooks/useUpsertFilter'; +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; +import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; +import { getOperandLabel } from '../utils/getOperandLabel'; +import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; + +export const FilterDropdownOperandSelect = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setSelectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const operandsForFilterType = getOperandsForFilterType( + filterDefinitionUsedInDropdown?.type, + ); + + const [ + isFilterDropdownOperandSelectUnfolded, + setIsFilterDropdownOperandSelectUnfolded, + ] = useRecoilScopedState( + isFilterDropdownOperandSelectUnfoldedScopedState, + ViewBarRecoilScopeContext, + ); + + const filterCurrentlyEdited = useFilterCurrentlyEdited(); + + const upsertFilter = useUpsertFilter(); + + const handleOperangeChange = (newOperand: ViewFilterOperand) => { + setSelectedOperandInDropdown(newOperand); + setIsFilterDropdownOperandSelectUnfolded(false); + + if (filterDefinitionUsedInDropdown && filterCurrentlyEdited) { + upsertFilter({ + key: filterCurrentlyEdited.key, + displayValue: filterCurrentlyEdited.displayValue, + operand: newOperand, + type: filterCurrentlyEdited.type, + value: filterCurrentlyEdited.value, + }); + } + }; + + if (!isFilterDropdownOperandSelectUnfolded) { + return <>; + } + + return ( + + {operandsForFilterType.map((filterOperand, index) => ( + { + handleOperangeChange(filterOperand); + }} + text={getOperandLabel(filterOperand)} + /> + ))} + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/FilterDropdownTextSearchInput.tsx b/front/src/modules/ui/Data/view-bar/components/FilterDropdownTextSearchInput.tsx new file mode 100644 index 0000000000000..fb1e0b0ef5416 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/FilterDropdownTextSearchInput.tsx @@ -0,0 +1,64 @@ +import { ChangeEvent } from 'react'; + +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited'; +import { useRemoveFilter } from '../hooks/useRemoveFilter'; +import { useUpsertFilter } from '../hooks/useUpsertFilter'; +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; +import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; + +export const FilterDropdownTextSearchInput = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [selectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [filterDropdownSearchInput, setFilterDropdownSearchInput] = + useRecoilScopedState( + filterDropdownSearchInputScopedState, + ViewBarRecoilScopeContext, + ); + + const upsertFilter = useUpsertFilter(); + const removeFilter = useRemoveFilter(); + + const filterCurrentlyEdited = useFilterCurrentlyEdited(); + + return ( + filterDefinitionUsedInDropdown && + selectedOperandInDropdown && ( + ) => { + setFilterDropdownSearchInput(event.target.value); + + if (event.target.value === '') { + removeFilter(filterDefinitionUsedInDropdown.key); + } else { + upsertFilter({ + key: filterDefinitionUsedInDropdown.key, + type: filterDefinitionUsedInDropdown.type, + value: event.target.value, + operand: selectedOperandInDropdown, + displayValue: event.target.value, + }); + } + }} + /> + ) + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/GenericEntityFilterChip.tsx b/front/src/modules/ui/Data/view-bar/components/GenericEntityFilterChip.tsx new file mode 100644 index 0000000000000..41df2e51addda --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/GenericEntityFilterChip.tsx @@ -0,0 +1,22 @@ +import { EntityChip } from '@/ui/display/chip/components/EntityChip'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; + +import { Filter } from '../types/Filter'; + +type GenericEntityFilterChipProps = { + filter: Filter; + Icon?: IconComponent; +}; + +export const GenericEntityFilterChip = ({ + filter, + Icon, +}: GenericEntityFilterChipProps) => ( + +); diff --git a/front/src/modules/ui/Data/view-bar/components/MultipleFiltersButton.tsx b/front/src/modules/ui/Data/view-bar/components/MultipleFiltersButton.tsx new file mode 100644 index 0000000000000..ad5cce81c416f --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/MultipleFiltersButton.tsx @@ -0,0 +1,59 @@ +import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { FilterDropdownId } from '../constants/FilterDropdownId'; +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; +import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState'; +import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; + +export const MultipleFiltersButton = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const { isDropdownOpen, toggleDropdown } = useDropdown({ + dropdownScopeId: FilterDropdownId, + }); + + const [, setIsFilterDropdownOperandSelectUnfolded] = useRecoilScopedState( + isFilterDropdownOperandSelectUnfoldedScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setFilterDropdownSearchInput] = useRecoilScopedState( + filterDropdownSearchInputScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setSelectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const resetState = () => { + setIsFilterDropdownOperandSelectUnfolded(false); + setFilterDefinitionUsedInDropdown(null); + setSelectedOperandInDropdown(null); + setFilterDropdownSearchInput(''); + }; + + const handleClick = () => { + toggleDropdown(); + resetState(); + }; + + return ( + + Filter + + ); +}; diff --git a/front/src/modules/ui/view-bar/components/MultipleFiltersDropdownButton.tsx b/front/src/modules/ui/Data/view-bar/components/MultipleFiltersDropdownButton.tsx similarity index 100% rename from front/src/modules/ui/view-bar/components/MultipleFiltersDropdownButton.tsx rename to front/src/modules/ui/Data/view-bar/components/MultipleFiltersDropdownButton.tsx diff --git a/front/src/modules/ui/Data/view-bar/components/MultipleFiltersDropdownContent.tsx b/front/src/modules/ui/Data/view-bar/components/MultipleFiltersDropdownContent.tsx new file mode 100644 index 0000000000000..4b9275ede8ef5 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/MultipleFiltersDropdownContent.tsx @@ -0,0 +1,70 @@ +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; +import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; + +import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput'; +import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput'; +import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect'; +import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect'; +import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput'; +import { FilterDropdownOperandButton } from './FilterDropdownOperandButton'; +import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect'; +import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput'; + +export const MultipleFiltersDropdownContent = () => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [isFilterDropdownOperandSelectUnfolded] = useRecoilScopedState( + isFilterDropdownOperandSelectUnfoldedScopedState, + ViewBarRecoilScopeContext, + ); + + const [filterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [selectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + return ( + + <> + {!filterDefinitionUsedInDropdown ? ( + + ) : isFilterDropdownOperandSelectUnfolded ? ( + + ) : ( + selectedOperandInDropdown && ( + <> + + + {filterDefinitionUsedInDropdown.type === 'text' && ( + + )} + {filterDefinitionUsedInDropdown.type === 'number' && ( + + )} + {filterDefinitionUsedInDropdown.type === 'date' && ( + + )} + {filterDefinitionUsedInDropdown.type === 'entity' && ( + + )} + {filterDefinitionUsedInDropdown.type === 'entity' && ( + + )} + + ) + )} + + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/SingleEntityFilterDropdownButton.tsx b/front/src/modules/ui/Data/view-bar/components/SingleEntityFilterDropdownButton.tsx new file mode 100644 index 0000000000000..346640b589444 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/SingleEntityFilterDropdownButton.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { useTheme } from '@emotion/react'; + +import { IconChevronDown } from '@/ui/display/icon/index'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuContainer } from '@/ui/layout/dropdown/components/DropdownMenuContainer'; +import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; +import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { ViewFilterOperand } from '~/generated/graphql'; + +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { availableFiltersScopedState } from '../states/availableFiltersScopedState'; +import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; +import { filtersScopedState } from '../states/filtersScopedState'; +import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; +import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; + +import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput'; +import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect'; +import { GenericEntityFilterChip } from './GenericEntityFilterChip'; + +export const SingleEntityFilterDropdownButton = ({ + hotkeyScope, +}: { + hotkeyScope: HotkeyScope; +}) => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [availableFilters] = useRecoilScopedState( + availableFiltersScopedState, + ViewBarRecoilScopeContext, + ); + const availableFilter = availableFilters[0]; + + const [filters] = useRecoilScopedState( + filtersScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState( + filterDefinitionUsedInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + const [, setSelectedOperandInDropdown] = useRecoilScopedState( + selectedOperandInDropdownScopedState, + ViewBarRecoilScopeContext, + ); + + React.useEffect(() => { + setFilterDefinitionUsedInDropdown(availableFilter); + const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0]; + setSelectedOperandInDropdown(defaultOperand); + }, [ + availableFilter, + setFilterDefinitionUsedInDropdown, + setSelectedOperandInDropdown, + ]); + + const theme = useTheme(); + + return ( + + + {filters[0] ? ( + + ) : ( + 'Filter' + )} + + + } + dropdownComponents={ + + + + + } + /> + + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/SortDropdownButton.tsx b/front/src/modules/ui/Data/view-bar/components/SortDropdownButton.tsx new file mode 100644 index 0000000000000..40731451970df --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/SortDropdownButton.tsx @@ -0,0 +1,146 @@ +import { useCallback, useState } from 'react'; +import { produce } from 'immer'; + +import { IconChevronDown } from '@/ui/display/icon'; +import { LightButton } from '@/ui/input/button/components/LightButton'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu'; +import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { SortDropdownId } from '../constants/SortDropdownId'; +import { useViewBarContext } from '../hooks/useViewBarContext'; +import { availableSortsScopedState } from '../states/availableSortsScopedState'; +import { sortsScopedState } from '../states/sortsScopedState'; +import { SortDefinition } from '../types/SortDefinition'; +import { SORT_DIRECTIONS, SortDirection } from '../types/SortDirection'; + +import { ViewBarDropdownButton } from './ViewBarDropdownButton'; + +export type SortDropdownButtonProps = { + hotkeyScope: HotkeyScope; + isPrimaryButton?: boolean; +}; + +export const SortDropdownButton = ({ + hotkeyScope, +}: SortDropdownButtonProps) => { + const { ViewBarRecoilScopeContext } = useViewBarContext(); + + const [isSortDirectionMenuUnfolded, setIsSortDirectionMenuUnfolded] = + useState(false); + + const [selectedSortDirection, setSelectedSortDirection] = + useState('asc'); + + const resetState = useCallback(() => { + setIsSortDirectionMenuUnfolded(false); + setSelectedSortDirection('asc'); + }, []); + + const [availableSorts] = useRecoilScopedState( + availableSortsScopedState, + ViewBarRecoilScopeContext, + ); + + const [sorts, setSorts] = useRecoilScopedState( + sortsScopedState, + ViewBarRecoilScopeContext, + ); + + const isSortSelected = sorts.length > 0; + + const { toggleDropdown } = useDropdown({ + dropdownScopeId: SortDropdownId, + }); + + const handleButtonClick = () => { + toggleDropdown(); + resetState(); + }; + + const handleAddSort = (selectedSortDefinition: SortDefinition) => { + toggleDropdown(); + + setSorts( + produce(sorts, (existingSortsDraft) => { + const foundExistingSortIndex = existingSortsDraft.findIndex( + (existingSort) => existingSort.key === selectedSortDefinition.key, + ); + + if (foundExistingSortIndex !== -1) { + existingSortsDraft[foundExistingSortIndex].direction = + selectedSortDirection; + } else { + existingSortsDraft.push({ + key: selectedSortDefinition.key, + direction: selectedSortDirection, + definition: selectedSortDefinition, + }); + } + }), + ); + }; + + const handleDropdownButtonClose = () => { + resetState(); + }; + + return ( + + } + dropdownComponents={ + + {isSortDirectionMenuUnfolded ? ( + + {SORT_DIRECTIONS.map((sortOrder, index) => ( + { + setSelectedSortDirection(sortOrder); + setIsSortDirectionMenuUnfolded(false); + }} + text={sortOrder === 'asc' ? 'Ascending' : 'Descending'} + /> + ))} + + ) : ( + <> + setIsSortDirectionMenuUnfolded(true)} + > + {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} + + + + {availableSorts.map((availableSort, index) => ( + handleAddSort(availableSort)} + LeftIcon={availableSort.Icon} + text={availableSort.label} + /> + ))} + + + )} + + } + onClose={handleDropdownButtonClose} + > + ); +}; diff --git a/front/src/modules/ui/Data/view-bar/components/SortOrFilterChip.tsx b/front/src/modules/ui/Data/view-bar/components/SortOrFilterChip.tsx new file mode 100644 index 0000000000000..070d0c4cae342 --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/SortOrFilterChip.tsx @@ -0,0 +1,82 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { IconX } from '@/ui/display/icon/index'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; + +type SortOrFilterChipProps = { + labelKey?: string; + labelValue: string; + Icon?: IconComponent; + onRemove: () => void; + isSort?: boolean; + testId?: string; +}; + +type StyledChipProps = { + isSort?: boolean; +}; + +const StyledChip = styled.div` + align-items: center; + background-color: ${({ theme }) => theme.accent.quaternary}; + border: 1px solid ${({ theme }) => theme.accent.tertiary}; + border-radius: 4px; + color: ${({ theme }) => theme.color.blue}; + display: flex; + flex-direction: row; + flex-shrink: 0; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ isSort }) => (isSort ? 'bold' : 'normal')}; + padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)}; +`; +const StyledIcon = styled.div` + align-items: center; + display: flex; + margin-right: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledDelete = styled.div` + align-items: center; + cursor: pointer; + display: flex; + font-size: ${({ theme }) => theme.font.size.sm}; + margin-left: ${({ theme }) => theme.spacing(2)}; + margin-top: 1px; + user-select: none; + &:hover { + background-color: ${({ theme }) => theme.accent.secondary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + } +`; + +const StyledLabelKey = styled.div` + font-weight: ${({ theme }) => theme.font.weight.medium}; +`; + +const SortOrFilterChip = ({ + labelKey, + labelValue, + Icon, + onRemove, + isSort, + testId, +}: SortOrFilterChipProps) => { + const theme = useTheme(); + return ( + + {Icon && ( + + + + )} + {labelKey && {labelKey}} + {labelValue} + + + + + ); +}; + +export default SortOrFilterChip; diff --git a/front/src/modules/ui/Data/view-bar/components/UpdateViewButtonGroup.tsx b/front/src/modules/ui/Data/view-bar/components/UpdateViewButtonGroup.tsx new file mode 100644 index 0000000000000..ba7880533dd2b --- /dev/null +++ b/front/src/modules/ui/Data/view-bar/components/UpdateViewButtonGroup.tsx @@ -0,0 +1,141 @@ +import { useCallback, useContext, useState } from 'react'; +import styled from '@emotion/styled'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { Key } from 'ts-key-enum'; + +import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState'; +import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState'; +import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState'; +import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState'; +import { canPersistFiltersScopedFamilySelector } from '@/ui/data/view-bar/states/selectors/canPersistFiltersScopedFamilySelector'; +import { canPersistSortsScopedFamilySelector } from '@/ui/data/view-bar/states/selectors/canPersistSortsScopedFamilySelector'; +import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState'; +import { viewEditModeState } from '@/ui/data/view-bar/states/viewEditModeState'; +import { IconChevronDown, IconPlus } from '@/ui/display/icon'; +import { Button } from '@/ui/input/button/components/Button'; +import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; +import { DropdownMenuContainer } from '@/ui/layout/dropdown/components/DropdownMenuContainer'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; + +import { ViewBarContext } from '../contexts/ViewBarContext'; + +const StyledContainer = styled.div` + display: inline-flex; + margin-right: ${({ theme }) => theme.spacing(2)}; + position: relative; +`; +export type UpdateViewButtonGroupProps = { + hotkeyScope: string; + onViewEditModeChange?: () => void; +}; + +export const UpdateViewButtonGroup = ({ + hotkeyScope, + onViewEditModeChange, +}: UpdateViewButtonGroupProps) => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const { + canPersistViewFields, + onCurrentViewSubmit, + ViewBarRecoilScopeContext, + } = useContext(ViewBarContext); + + const recoilScopeId = useRecoilScopeId(ViewBarRecoilScopeContext); + + const currentViewId = useRecoilScopedValue( + currentViewIdScopedState, + ViewBarRecoilScopeContext, + ); + + const filters = useRecoilScopedValue( + filtersScopedState, + ViewBarRecoilScopeContext, + ); + const setSavedFilters = useSetRecoilState( + savedFiltersFamilyState(currentViewId), + ); + const canPersistFilters = useRecoilValue( + canPersistFiltersScopedFamilySelector({ + recoilScopeId, + viewId: currentViewId, + }), + ); + + const sorts = useRecoilScopedValue( + sortsScopedState, + ViewBarRecoilScopeContext, + ); + const setSavedSorts = useSetRecoilState(savedSortsFamilyState(currentViewId)); + const canPersistSorts = useRecoilValue( + canPersistSortsScopedFamilySelector({ + recoilScopeId, + viewId: currentViewId, + }), + ); + + const setViewEditMode = useSetRecoilState(viewEditModeState); + + const canPersistView = + currentViewId && + (canPersistViewFields || canPersistFilters || canPersistSorts); + + const handleArrowDownButtonClick = useCallback(() => { + setIsDropdownOpen((previousIsOpen) => !previousIsOpen); + }, []); + + const handleCreateViewButtonClick = useCallback(() => { + setViewEditMode({ mode: 'create', viewId: undefined }); + onViewEditModeChange?.(); + setIsDropdownOpen(false); + }, [setViewEditMode, onViewEditModeChange]); + + const handleDropdownClose = useCallback(() => { + setIsDropdownOpen(false); + }, []); + + const handleViewSubmit = async () => { + if (canPersistFilters) setSavedFilters(filters); + if (canPersistSorts) setSavedSorts(sorts); + + await onCurrentViewSubmit?.(); + }; + + useScopedHotkeys( + [Key.Enter, Key.Escape], + handleDropdownClose, + hotkeyScope, + [], + ); + + if (!canPersistView) return null; + + return ( + + +