From 321488ad3c90cac45b29853b3f8f8ba8b6eff73e Mon Sep 17 00:00:00 2001 From: Aditya Pimpalkar Date: Tue, 19 Sep 2023 23:27:02 +0100 Subject: [PATCH] feat: Column title menus (#1616) * view field index to float * gql codegen and package.json * list implementation * db call * reposition logic * lint fix * edge case fix * review changes * handleColumnMove refactor * dropdown recoil scope * rename props * Update server/src/database/migrations/20230727124244_add_view_fields_table/migration.sql --------- Co-authored-by: Charles Bochet --- front/src/generated/graphql.tsx | 46 ++++----- .../ui/dropdown/components/DropdownButton.tsx | 6 +- front/src/modules/ui/icon/index.ts | 4 + .../ui/table/components/ColumnHead.tsx | 43 +++++++-- .../ui/table/components/EntityTableHeader.tsx | 46 +++++---- .../components/EntityTableHeaderOptions.tsx | 96 +++++++++++++++++++ .../table/constants/ColumnHeaderDropdownId.ts | 1 + .../modules/ui/table/hooks/useTableColumns.ts | 50 +++++++++- .../modules/views/hooks/useTableViewFields.ts | 3 +- .../src/modules/views/hooks/useTableViews.ts | 13 +-- server/package.json | 1 + server/src/database/schema.prisma | 2 +- server/yarn.lock | 5 + 13 files changed, 253 insertions(+), 63 deletions(-) create mode 100644 front/src/modules/ui/table/components/EntityTableHeaderOptions.tsx create mode 100644 front/src/modules/ui/table/constants/ColumnHeaderDropdownId.ts diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index afdda833a..fbbd90b47 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1016,15 +1016,15 @@ export enum FileFolder { WorkspaceLogo = 'WorkspaceLogo' } -export type IntFilter = { - equals?: InputMaybe; - gt?: InputMaybe; - gte?: InputMaybe; - in?: InputMaybe>; - lt?: InputMaybe; - lte?: InputMaybe; - not?: InputMaybe; - notIn?: InputMaybe>; +export type FloatFilter = { + equals?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + in?: InputMaybe>; + lt?: InputMaybe; + lte?: InputMaybe; + not?: InputMaybe; + notIn?: InputMaybe>; }; export type IntNullableFilter = { @@ -1478,15 +1478,15 @@ export type NestedEnumViewTypeFilter = { notIn?: InputMaybe>; }; -export type NestedIntFilter = { - equals?: InputMaybe; - gt?: InputMaybe; - gte?: InputMaybe; - in?: InputMaybe>; - lt?: InputMaybe; - lte?: InputMaybe; - not?: InputMaybe; - notIn?: InputMaybe>; +export type NestedFloatFilter = { + equals?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + in?: InputMaybe>; + lt?: InputMaybe; + lte?: InputMaybe; + not?: InputMaybe; + notIn?: InputMaybe>; }; export type NestedIntNullableFilter = { @@ -2587,7 +2587,7 @@ export type ViewCreateNestedOneWithoutFieldsInput = { export type ViewField = { __typename?: 'ViewField'; - index: Scalars['Int']; + index: Scalars['Float']; isVisible: Scalars['Boolean']; key: Scalars['String']; name: Scalars['String']; @@ -2598,7 +2598,7 @@ export type ViewField = { }; export type ViewFieldCreateInput = { - index: Scalars['Int']; + index: Scalars['Float']; isVisible: Scalars['Boolean']; key: Scalars['String']; name: Scalars['String']; @@ -2608,7 +2608,7 @@ export type ViewFieldCreateInput = { }; export type ViewFieldCreateManyInput = { - index: Scalars['Int']; + index: Scalars['Float']; isVisible: Scalars['Boolean']; key: Scalars['String']; name: Scalars['String']; @@ -2654,7 +2654,7 @@ export enum ViewFieldScalarFieldEnum { } export type ViewFieldUpdateInput = { - index?: InputMaybe; + index?: InputMaybe; isVisible?: InputMaybe; key?: InputMaybe; name?: InputMaybe; @@ -2684,7 +2684,7 @@ export type ViewFieldWhereInput = { AND?: InputMaybe>; NOT?: InputMaybe>; OR?: InputMaybe>; - index?: InputMaybe; + index?: InputMaybe; isVisible?: InputMaybe; key?: InputMaybe; name?: InputMaybe; diff --git a/front/src/modules/ui/dropdown/components/DropdownButton.tsx b/front/src/modules/ui/dropdown/components/DropdownButton.tsx index 40fe89a6e..aae66abc6 100644 --- a/front/src/modules/ui/dropdown/components/DropdownButton.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownButton.tsx @@ -12,7 +12,7 @@ import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScop import { DropdownCloseEffect } from './DropdownCloseEffect'; type OwnProps = { - buttonComponents: JSX.Element | JSX.Element[]; + buttonComponents?: JSX.Element | JSX.Element[]; dropdownComponents: JSX.Element | JSX.Element[]; dropdownId: string; hotkey?: { @@ -75,7 +75,9 @@ export const DropdownButton = ({ onHotkeyTriggered={handleHotkeyTriggered} /> )} -
{buttonComponents}
+ {buttonComponents && ( +
{buttonComponents}
+ )} {isDropdownButtonOpen && (
{dropdownComponents} diff --git a/front/src/modules/ui/icon/index.ts b/front/src/modules/ui/icon/index.ts index b1d481510..ef34a5134 100644 --- a/front/src/modules/ui/icon/index.ts +++ b/front/src/modules/ui/icon/index.ts @@ -5,6 +5,10 @@ export { IconAlertTriangle, IconArchive, IconArrowBack, + IconArrowNarrowDown, + IconArrowNarrowLeft, + IconArrowNarrowRight, + IconArrowNarrowUp, IconArrowDown, IconArrowRight, IconArrowUp, diff --git a/front/src/modules/ui/table/components/ColumnHead.tsx b/front/src/modules/ui/table/components/ColumnHead.tsx index ee18a1841..a6302302e 100644 --- a/front/src/modules/ui/table/components/ColumnHead.tsx +++ b/front/src/modules/ui/table/components/ColumnHead.tsx @@ -1,11 +1,18 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComponent } from '@/ui/icon/types/IconComponent'; +import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; +import { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField'; + +import { ColumnHeaderDropdownId } from '../constants/ColumnHeaderDropdownId'; +import { ColumnDefinition } from '../types/ColumnDefinition'; + +import { EntityTableHeaderOptions } from './EntityTableHeaderOptions'; type OwnProps = { - viewName: string; - ViewIcon?: IconComponent; + column: ColumnDefinition; + isFirstColumn: boolean; + isLastColumn: boolean; }; const StyledTitle = styled.div` @@ -34,14 +41,30 @@ const StyledText = styled.span` white-space: nowrap; `; -export const ColumnHead = ({ viewName, ViewIcon }: OwnProps) => { +export const ColumnHead = ({ + column, + isFirstColumn, + isLastColumn, +}: OwnProps) => { const theme = useTheme(); + + const { openDropdownButton } = useDropdownButton({ + dropdownId: ColumnHeaderDropdownId, + }); + return ( - - - {ViewIcon && } - - {viewName} - + <> + + + {column.Icon && } + + {column.name} + + + ); }; diff --git a/front/src/modules/ui/table/components/EntityTableHeader.tsx b/front/src/modules/ui/table/components/EntityTableHeader.tsx index 7f7069692..d74d9106b 100644 --- a/front/src/modules/ui/table/components/EntityTableHeader.tsx +++ b/front/src/modules/ui/table/components/EntityTableHeader.tsx @@ -3,8 +3,10 @@ import styled from '@emotion/styled'; import { useRecoilCallback, useRecoilState } from 'recoil'; import { IconButton } from '@/ui/button/components/IconButton'; +import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; import { IconPlus } from '@/ui/icon'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; @@ -163,25 +165,31 @@ export const EntityTableHeader = () => { - {visibleTableColumns.map((column) => ( - - - { - setResizedFieldKey(column.key); - }} - /> - + {visibleTableColumns.map((column, index) => ( + + + + { + setResizedFieldKey(column.key); + }} + /> + + ))} {hiddenTableColumns.length > 0 && ( diff --git a/front/src/modules/ui/table/components/EntityTableHeaderOptions.tsx b/front/src/modules/ui/table/components/EntityTableHeaderOptions.tsx new file mode 100644 index 000000000..78ca7d111 --- /dev/null +++ b/front/src/modules/ui/table/components/EntityTableHeaderOptions.tsx @@ -0,0 +1,96 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { DropdownButton } from '@/ui/dropdown/components/DropdownButton'; +import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; +import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; +import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; +import { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField'; +import { + IconArrowNarrowLeft, + IconArrowNarrowRight, + IconEyeOff, +} from '@/ui/icon'; +import { MenuItem } from '@/ui/menu-item/components/MenuItem'; + +import { ColumnHeaderDropdownId } from '../constants/ColumnHeaderDropdownId'; +import { useTableColumns } from '../hooks/useTableColumns'; +import { ColumnDefinition } from '../types/ColumnDefinition'; + +const StyledDropdownContainer = styled.div` + left: 0px; + position: absolute; + top: 32px; + z-index: 1; +`; +type EntityTableHeaderOptionsProps = { + column: ColumnDefinition; + isFirstColumn: boolean; + isLastColumn: boolean; +}; +export const EntityTableHeaderOptions = ({ + column, + isFirstColumn, + isLastColumn, +}: EntityTableHeaderOptionsProps) => { + const theme = useTheme(); + + const { + handleColumnVisibilityChange, + handleColumnLeftMove, + handleColumnRightMove, + } = useTableColumns(); + + const { closeDropdownButton } = useDropdownButton({ + dropdownId: ColumnHeaderDropdownId, + }); + + const handleColumnMoveLeft = () => { + closeDropdownButton(); + if (isFirstColumn) return; + else handleColumnLeftMove(column); + }; + + const handleColumnMoveRight = () => { + closeDropdownButton(); + if (isLastColumn) return; + else handleColumnRightMove(column); + }; + + const handleColumnVisibility = () => { + handleColumnVisibilityChange(column); + }; + + return ( + + + + ( + + )} + onClick={handleColumnMoveLeft} + text="Move left" + /> + ( + + )} + onClick={handleColumnMoveRight} + text="Move right" + /> + } + onClick={handleColumnVisibility} + text="Hide" + /> + + + } + /> + + ); +}; diff --git a/front/src/modules/ui/table/constants/ColumnHeaderDropdownId.ts b/front/src/modules/ui/table/constants/ColumnHeaderDropdownId.ts new file mode 100644 index 000000000..f8149f2f2 --- /dev/null +++ b/front/src/modules/ui/table/constants/ColumnHeaderDropdownId.ts @@ -0,0 +1 @@ +export const ColumnHeaderDropdownId = 'table-header-options'; diff --git a/front/src/modules/ui/table/hooks/useTableColumns.ts b/front/src/modules/ui/table/hooks/useTableColumns.ts index 04fbefe58..89eafd977 100644 --- a/front/src/modules/ui/table/hooks/useTableColumns.ts +++ b/front/src/modules/ui/table/hooks/useTableColumns.ts @@ -36,5 +36,53 @@ export const useTableColumns = () => { [tableColumnsByKey, tableColumns, setTableColumns], ); - return { handleColumnVisibilityChange }; + const handleColumnMove = useCallback( + (direction: string, column: ColumnDefinition) => { + const tableColumnIndex = tableColumns.findIndex( + (tableColumn) => tableColumn.key === column.key, + ); + if (tableColumnIndex >= 0) { + const currentColumn = tableColumns[tableColumnIndex]; + const targetColumn = + direction === 'left' + ? tableColumns[tableColumnIndex - 1] + : tableColumns[tableColumnIndex + 1]; + const updatedColumns = tableColumns + .map((tableColumn) => { + switch (tableColumn.key) { + case targetColumn.key: + return { ...tableColumn, index: currentColumn.index }; + case currentColumn.key: + return { ...tableColumn, index: targetColumn.index }; + default: + return tableColumn; + } + }) + .sort((columnA, columnB) => columnA.index - columnB.index); + + setTableColumns(updatedColumns); + } + }, + [tableColumns, setTableColumns], + ); + + const handleColumnLeftMove = useCallback( + (column: ColumnDefinition) => { + handleColumnMove('left', column); + }, + [handleColumnMove], + ); + + const handleColumnRightMove = useCallback( + (column: ColumnDefinition) => { + handleColumnMove('right', column); + }, + [handleColumnMove], + ); + + return { + handleColumnVisibilityChange, + handleColumnLeftMove, + handleColumnRightMove, + }; }; diff --git a/front/src/modules/views/hooks/useTableViewFields.ts b/front/src/modules/views/hooks/useTableViewFields.ts index fb1805969..759aeb068 100644 --- a/front/src/modules/views/hooks/useTableViewFields.ts +++ b/front/src/modules/views/hooks/useTableViewFields.ts @@ -94,6 +94,7 @@ export const useTableViewFields = ({ data: { isVisible: column.isVisible, size: column.size, + index: column.index, }, where: { viewId_key: { key: column.key, viewId: currentViewId }, @@ -177,5 +178,5 @@ export const useTableViewFields = ({ updateViewFields, ]); - return { createViewFields, persistColumns }; + return { createViewFields, persistColumns, updateViewFields }; }; diff --git a/front/src/modules/views/hooks/useTableViews.ts b/front/src/modules/views/hooks/useTableViews.ts index 35091c9da..0714ccde0 100644 --- a/front/src/modules/views/hooks/useTableViews.ts +++ b/front/src/modules/views/hooks/useTableViews.ts @@ -41,12 +41,12 @@ export const useTableViews = ({ type: ViewType.Table, RecoilScopeContext: TableRecoilScopeContext, }); - - const { createViewFields, persistColumns } = useTableViewFields({ - objectId, - columnDefinitions, - skipFetch: isFetchingViews, - }); + const { createViewFields, persistColumns, updateViewFields } = + useTableViewFields({ + objectId, + columnDefinitions, + skipFetch: isFetchingViews, + }); const { createViewFilters, persistFilters } = useViewFilters({ RecoilScopeContext: TableRecoilScopeContext, @@ -62,6 +62,7 @@ export const useTableViews = ({ await persistColumns(); await persistFilters(); await persistSorts(); + await updateViewFields(tableColumns); }; return { createView, deleteView, submitCurrentView, updateView }; diff --git a/server/package.json b/server/package.json index 6ab6f7edc..cc4432fff 100644 --- a/server/package.json +++ b/server/package.json @@ -76,6 +76,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.11.3", + "prisma-graphql-type-decimal": "^3.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", diff --git a/server/src/database/schema.prisma b/server/src/database/schema.prisma index 900074b54..d5d8058a1 100644 --- a/server/src/database/schema.prisma +++ b/server/src/database/schema.prisma @@ -685,7 +685,7 @@ model ViewSort { } model ViewField { - index Int + index Float isVisible Boolean key String name String diff --git a/server/yarn.lock b/server/yarn.lock index 0a320716b..5d7b48fe3 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -7684,6 +7684,11 @@ pretty-format@^28.0.0, pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" +prisma-graphql-type-decimal@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/prisma-graphql-type-decimal/-/prisma-graphql-type-decimal-3.0.0.tgz#f3fa524b9e7aeab4fa0f969829bdddf199eff9fe" + integrity sha512-jrALv8ShVZoBBNyIOBCxwRoLM/DAOSD/OBKTWeJa9UigQXUTZniNXvLd4fLXwm+3v00A7cOZ1fFh3b74Ndgxhw== + prisma-nestjs-graphql@^18.0.2: version "18.0.2" resolved "https://registry.yarnpkg.com/prisma-nestjs-graphql/-/prisma-nestjs-graphql-18.0.2.tgz#852b9386d2c26bad0bd82254a5cc2e483a96d5b5"