diff --git a/apps/data-studio/src/components/LibraryHome/LibraryHome.tsx b/apps/data-studio/src/components/LibraryHome/LibraryHome.tsx index 5ca53e889..1d79be0f2 100644 --- a/apps/data-studio/src/components/LibraryHome/LibraryHome.tsx +++ b/apps/data-studio/src/components/LibraryHome/LibraryHome.tsx @@ -152,9 +152,6 @@ const LibraryHome: FunctionComponent = ({library}) => { defaultActionsForItem={['edit', 'remove']} defaultPrimaryActions={['create']} defaultMassActions={['deactivate']} - defaultViewSettings={{ - pageSize: 3 - }} itemActions={[ { label: 'Test 1', diff --git a/libs/ui/src/_gqlTypes/index.ts b/libs/ui/src/_gqlTypes/index.ts index a0afdb6f3..885d8a927 100644 --- a/libs/ui/src/_gqlTypes/index.ts +++ b/libs/ui/src/_gqlTypes/index.ts @@ -386,7 +386,9 @@ export enum LogAction { VALUE_DELETE = 'VALUE_DELETE', VALUE_SAVE = 'VALUE_SAVE', VERSION_PROFILE_DELETE = 'VERSION_PROFILE_DELETE', - VERSION_PROFILE_SAVE = 'VERSION_PROFILE_SAVE' + VERSION_PROFILE_SAVE = 'VERSION_PROFILE_SAVE', + fakeplugin_FAKE_PLUGIN_ACTION = 'fakeplugin_FAKE_PLUGIN_ACTION', + fakeplugin_FAKE_PLUGIN_ACTION2 = 'fakeplugin_FAKE_PLUGIN_ACTION2' } export type LogFilterInput = { @@ -520,7 +522,8 @@ export enum PermissionsActions { detach = 'detach', edit_children = 'edit_children', edit_record = 'edit_record', - edit_value = 'edit_value' + edit_value = 'edit_value', + fake_plugin_permission = 'fake_plugin_permission' } export enum PermissionsRelation { @@ -1386,22 +1389,20 @@ export type ExplorerLinkDataQueryVariables = Exact<{ export type ExplorerLinkDataQuery = { records: { list: Array<{ id: string, whoAmI: { id: string, library: { id: string } }, property: Array<{ id_value?: string | null, payload?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } }, properties: Array<{ attributeId: string, attributeProperties: { id: string, label?: any | null, type: AttributeType, format?: AttributeFormat | null, multiple_values: boolean }, values: Array<{ linkPayload?: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } | null } | { treePayload?: { record: { id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } } } | null } | { valuePayload?: any | null, valueRawPayload?: any | null }> }> } | null } | { id_value?: string | null }> }> } }; -export type ExplorerAllKeysQueryVariables = Exact<{ +export type ExplorerLibraryDetailsQueryVariables = Exact<{ libraryId: Scalars['ID']; - filters?: InputMaybe> | InputMaybe>; - multipleSort?: InputMaybe | RecordSortInput>; - searchQuery?: InputMaybe; }>; -export type ExplorerAllKeysQuery = { records: { list: Array<{ id: string, whoAmI: { id: string, label?: string | null, subLabel?: string | null, color?: string | null, preview?: IPreviewScalar | null, library: { id: string, label?: any | null } } }> } }; +export type ExplorerLibraryDetailsQuery = { libraries?: { list: Array<{ id: string, label?: any | null }> } | null }; -export type ExplorerLibraryDetailsQueryVariables = Exact<{ +export type ExplorerSelectionIdsQueryVariables = Exact<{ libraryId: Scalars['ID']; + filters?: InputMaybe> | InputMaybe>; }>; -export type ExplorerLibraryDetailsQuery = { libraries?: { list: Array<{ id: string, label?: any | null }> } | null }; +export type ExplorerSelectionIdsQuery = { records: { list: Array<{ id: string }> } }; export type TreeDataQueryQueryVariables = Exact<{ treeId: Scalars['ID']; @@ -4249,89 +4250,82 @@ export function useExplorerLinkDataLazyQuery(baseOptions?: Apollo.LazyQueryHookO export type ExplorerLinkDataQueryHookResult = ReturnType; export type ExplorerLinkDataLazyQueryHookResult = ReturnType; export type ExplorerLinkDataQueryResult = Apollo.QueryResult; -export const ExplorerAllKeysDocument = gql` - query ExplorerAllKeys($libraryId: ID!, $filters: [RecordFilterInput], $multipleSort: [RecordSortInput!], $searchQuery: String) { - records( - library: $libraryId - filters: $filters - multipleSort: $multipleSort - searchQuery: $searchQuery - ) { +export const ExplorerLibraryDetailsDocument = gql` + query ExplorerLibraryDetails($libraryId: ID!) { + libraries(filters: {id: [$libraryId]}) { list { - ...RecordIdentity + id + label } } } - ${RecordIdentityFragmentDoc}`; + `; /** - * __useExplorerAllKeysQuery__ + * __useExplorerLibraryDetailsQuery__ * - * To run a query within a React component, call `useExplorerAllKeysQuery` and pass it any options that fit your needs. - * When your component renders, `useExplorerAllKeysQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useExplorerLibraryDetailsQuery` and pass it any options that fit your needs. + * When your component renders, `useExplorerLibraryDetailsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useExplorerAllKeysQuery({ + * const { data, loading, error } = useExplorerLibraryDetailsQuery({ * variables: { * libraryId: // value for 'libraryId' - * filters: // value for 'filters' - * multipleSort: // value for 'multipleSort' - * searchQuery: // value for 'searchQuery' * }, * }); */ -export function useExplorerAllKeysQuery(baseOptions: Apollo.QueryHookOptions) { +export function useExplorerLibraryDetailsQuery(baseOptions: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ExplorerAllKeysDocument, options); + return Apollo.useQuery(ExplorerLibraryDetailsDocument, options); } -export function useExplorerAllKeysLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useExplorerLibraryDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ExplorerAllKeysDocument, options); + return Apollo.useLazyQuery(ExplorerLibraryDetailsDocument, options); } -export type ExplorerAllKeysQueryHookResult = ReturnType; -export type ExplorerAllKeysLazyQueryHookResult = ReturnType; -export type ExplorerAllKeysQueryResult = Apollo.QueryResult; -export const ExplorerLibraryDetailsDocument = gql` - query ExplorerLibraryDetails($libraryId: ID!) { - libraries(filters: {id: [$libraryId]}) { +export type ExplorerLibraryDetailsQueryHookResult = ReturnType; +export type ExplorerLibraryDetailsLazyQueryHookResult = ReturnType; +export type ExplorerLibraryDetailsQueryResult = Apollo.QueryResult; +export const ExplorerSelectionIdsDocument = gql` + query ExplorerSelectionIds($libraryId: ID!, $filters: [RecordFilterInput]) { + records(library: $libraryId, filters: $filters) { list { id - label } } } `; /** - * __useExplorerLibraryDetailsQuery__ + * __useExplorerSelectionIdsQuery__ * - * To run a query within a React component, call `useExplorerLibraryDetailsQuery` and pass it any options that fit your needs. - * When your component renders, `useExplorerLibraryDetailsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useExplorerSelectionIdsQuery` and pass it any options that fit your needs. + * When your component renders, `useExplorerSelectionIdsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useExplorerLibraryDetailsQuery({ + * const { data, loading, error } = useExplorerSelectionIdsQuery({ * variables: { * libraryId: // value for 'libraryId' + * filters: // value for 'filters' * }, * }); */ -export function useExplorerLibraryDetailsQuery(baseOptions: Apollo.QueryHookOptions) { +export function useExplorerSelectionIdsQuery(baseOptions: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ExplorerLibraryDetailsDocument, options); + return Apollo.useQuery(ExplorerSelectionIdsDocument, options); } -export function useExplorerLibraryDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useExplorerSelectionIdsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ExplorerLibraryDetailsDocument, options); + return Apollo.useLazyQuery(ExplorerSelectionIdsDocument, options); } -export type ExplorerLibraryDetailsQueryHookResult = ReturnType; -export type ExplorerLibraryDetailsLazyQueryHookResult = ReturnType; -export type ExplorerLibraryDetailsQueryResult = Apollo.QueryResult; +export type ExplorerSelectionIdsQueryHookResult = ReturnType; +export type ExplorerSelectionIdsLazyQueryHookResult = ReturnType; +export type ExplorerSelectionIdsQueryResult = Apollo.QueryResult; export const TreeDataQueryDocument = gql` query TreeDataQuery($treeId: ID!) { trees(filters: {id: [$treeId]}) { diff --git a/libs/ui/src/components/Explorer/DataView.tsx b/libs/ui/src/components/Explorer/DataView.tsx index 51214a041..c28ed95d4 100644 --- a/libs/ui/src/components/Explorer/DataView.tsx +++ b/libs/ui/src/components/Explorer/DataView.tsx @@ -13,7 +13,6 @@ import {IExplorerData, IItemAction, IItemData} from './_types'; import {TableCell} from './TableCell'; import {IdCard} from './IdCard'; import {defaultPaginationHeight, useTableScrollableHeight} from './useTableScrollableHeight'; -import {MASS_SELECTION_ALL} from '_ui/components/Explorer/_constants'; import {useColumnWidth} from './useColumnWidth'; const USELESS = ''; @@ -54,10 +53,16 @@ const StyledTable = styled(KitTable)` .ant-table-cell { min-height: ${tableHeaderMinLineHeight}px; height: auto !important; + padding: 0 calc(var(--general-spacing-s) * 1px) 0 0; } } `; +const ActionsHeaderStyledDiv = styled.div` + justify-self: right; + text-align: left; +`; + interface IDataViewProps { dataGroupedFilteredSorted: IItemData[]; itemActions: IItemAction[]; @@ -101,13 +106,13 @@ export const DataView: FunctionComponent = memo( attributesProperties, paginationProps, itemActions, - selection: {onSelectionChange, selectedKeys, isMassSelectionAll} + selection: {onSelectionChange, selectedKeys, isMassSelectionAll}, iconsOnlyItemActions }) => { const {t} = useSharedTranslation(); const {containerRef, scrollHeight} = useTableScrollableHeight(!!paginationProps); - const {ref, getFieldColumnWidth, columnWidth} = useColumnWidth(); + const {ref, getFieldColumnWidth, columnWidth, actionsColumnHeaderWidth} = useColumnWidth(); const _getActionButtons = ( actions: Array void}>>, @@ -115,10 +120,11 @@ export const DataView: FunctionComponent = memo( ): ReactNode => { const isLessThanFourActions = actions.length < 4; + return ( {isLessThanFourActions ? ( <> - {actions.map(({label, icon, isDanger, callback, disabled}, actionIndex) => ( + {actions.map(({label, icon, isDanger, iconOnly, callback, disabled}, actionIndex) => ( = memo( danger={isDanger} disabled={disabled} > - {!iconsOnlyItemActions && !iconOnly && label} + {!iconsOnlyItemActions && !iconOnly && label} ))} @@ -198,8 +204,14 @@ export const DataView: FunctionComponent = memo( ? [] : [ { - title: t('explorer.actions'), + title: ( + + {t('explorer.actions')} + + ), dataIndex: USELESS, + align: 'right', + className: 'actions', shouldCellUpdate: () => false, width: columnWidth, render: (_, item, index) => @@ -235,7 +247,7 @@ export const DataView: FunctionComponent = memo( // TODO: handle columns width based on attribute type/format return ( - 0} columns={columns} tableLayout="fixed" diff --git a/libs/ui/src/components/Explorer/Explorer.test.tsx b/libs/ui/src/components/Explorer/Explorer.test.tsx index f7ce29d3a..9d34e186c 100644 --- a/libs/ui/src/components/Explorer/Explorer.test.tsx +++ b/libs/ui/src/components/Explorer/Explorer.test.tsx @@ -17,9 +17,11 @@ import {mockRecord} from '_ui/__mocks__/common/record'; import {Explorer} from '_ui/index'; import {IEntrypointLibrary, IEntrypointLink, IItemAction, IPrimaryAction} from './_types'; import * as useExecuteSaveValueBatchMutation from '../RecordEdition/EditRecordContent/hooks/useExecuteSaveValueBatchMutation'; -import * as useExplorerData from './_queries/useExplorerData'; import * as useColumnWidth from './useColumnWidth'; -import {MutableRefObject} from 'react'; +import {AddLinkModal} from './link-item/AddLinkModal'; +import {FunctionComponent} from 'react'; +import {IViewSettingsState, ViewSettingsContext, viewSettingsInitialState} from './manage-view-settings'; +import {useViewSettingsReducer} from './useViewSettingsReducer'; const EditRecordModalMock = 'EditRecordModal'; @@ -559,6 +561,14 @@ describe('Explorer', () => { } }; + const MockViewSettingsContextProvider: FunctionComponent<{viewMock: IViewSettingsState}> = ({ + viewMock, + children + }) => { + const {view, dispatch} = useViewSettingsReducer({type: 'library', libraryId: 'my_lib'}, viewMock); + return {children}; + }; + beforeEach(() => { spyUseExplorerLibraryDataQuery = jest .spyOn(gqlTypes, 'useExplorerLibraryDataQuery') @@ -756,7 +766,8 @@ describe('Explorer', () => { jest.spyOn(useColumnWidth, 'useColumnWidth').mockReturnValueOnce({ ref: {current: null}, getFieldColumnWidth: () => 500, - columnWidth: 500 + columnWidth: 500, + actionsColumnHeaderWidth: 464 }); render(, { @@ -896,7 +907,8 @@ describe('Explorer', () => { test('Should be able to display custom primary actions', async () => { render(); - const dropdownButton = screen.getByTestId('actions-dropdown'); + const firstActionButton = screen.queryByRole('button', {name: 'explorer.create-one'}); + const dropdownButton = firstActionButton?.nextElementSibling; expect(screen.queryByText(customPrimaryAction1.label)).not.toBeInTheDocument(); expect(screen.queryByText(customPrimaryAction2.label)).not.toBeInTheDocument(); @@ -925,7 +937,7 @@ describe('Explorer', () => { await user.click(firstActionButton); expect(customPrimaryActions[0].callback).toHaveBeenCalled(); - const dropdownButton = screen.getByTestId('actions-dropdown'); + const dropdownButton = firstActionButton?.nextElementSibling; expect(screen.queryByText(customPrimaryAction2.label)).not.toBeInTheDocument(); await user.click(dropdownButton!); @@ -1035,6 +1047,8 @@ describe('Explorer', () => { defaultViewSettings={{ filters: [ { + id: '', + attribute: {format: simpleMockAttribute.format, label: simpleMockAttribute.label.fr}, field: simpleMockAttribute.id, condition: gqlTypes.RecordFilterCondition.CONTAINS, value: 'Christmas' @@ -1286,6 +1300,8 @@ describe('Explorer', () => { pageSize: 1, // configuration to be in multi-pages (2 pages of 1 record) filters: [ { + id: '', + attribute: {format: simpleMockAttribute.format, label: simpleMockAttribute.label.fr}, field: simpleMockAttribute.id, condition: gqlTypes.RecordFilterCondition.CONTAINS, value: 'Christmas' @@ -1394,6 +1410,8 @@ describe('Explorer', () => { pageSize: 1, // configuration to be in multi-pages (2 pages of 1 record) filters: [ { + id: '', + attribute: {format: simpleMockAttribute.format, label: simpleMockAttribute.label.fr}, field: simpleMockAttribute.id, condition: gqlTypes.RecordFilterCondition.CONTAINS, value: 'Christmas' @@ -1577,4 +1595,61 @@ describe('Explorer', () => { await waitForElementToBeRemoved(() => screen.queryByRole('status')); }); }); + + describe('Add link modal', () => { + test('Should be able to add existing item to atribute', async () => { + const viewInitialState = { + ...viewSettingsInitialState, + entrypoint: linkEntrypoint + }; + + const fetch = jest.fn(); + const selecionIdsImplementation = [ + fetch, + { + loading: false, + data: undefined + } + ]; + + jest.spyOn(gqlTypes, 'useExplorerSelectionIdsLazyQuery').mockImplementation( + () => selecionIdsImplementation as gqlTypes.ExplorerSelectionIdsLazyQueryHookResult + ); + + render( + + + , + { + mocks: [ExplorerLinkAttributeQueryMock] + } + ); + + const rows = await screen.findAllByRole('row'); + expect(rows.length).toBe(mockRecords.length); + const checkbox = within(rows[0]).getByRole('checkbox'); + + expect(checkbox).toBeInTheDocument(); + await user.click(checkbox); + + const snackbar = screen.getByRole('status'); + expect(snackbar).toHaveTextContent('selectedItems|1'); + const addButton = screen.getByRole('button', {name: /add-link/}); + expect(addButton).toBeInTheDocument(); + await user.click(addButton); + + expect(fetch).toHaveBeenCalledWith({ + variables: { + filters: [ + { + condition: gqlTypes.RecordFilterCondition.EQUAL, + field: 'id', + value: mockRecords[0].id + } + ], + libraryId: '' + } + }); + }); + }); }); diff --git a/libs/ui/src/components/Explorer/Explorer.tsx b/libs/ui/src/components/Explorer/Explorer.tsx index cb7547e85..1b732de56 100644 --- a/libs/ui/src/components/Explorer/Explorer.tsx +++ b/libs/ui/src/components/Explorer/Explorer.tsx @@ -30,7 +30,6 @@ import {useViewSettingsReducer} from './useViewSettingsReducer'; import {useDeactivateMassAction} from './useDeactivateMassAction'; import {MASS_SELECTION_ALL} from './_constants'; import {useAddItemAction} from './useAddItemAction'; -import {concat} from '@apollo/client'; const isNotEmpty = (union: T): union is Exclude => union.length > 0; @@ -71,7 +70,6 @@ export interface IExplorerProps { export const Explorer: FunctionComponent = ({ entrypoint, - noPagination, itemActions = [], primaryActions = [], massActions = [], @@ -83,8 +81,6 @@ export const Explorer: FunctionComponent = ({ defaultPrimaryActions = ['create'], defaultMassActions = ['deactivate'], defaultViewSettings, - defaultMassActions = ['deactivate'], - defaultViewSettings, panelElement }) => { const {t} = useSharedTranslation(); @@ -135,9 +131,7 @@ export const Explorer: FunctionComponent = ({ const {addItemAction, addItemModal} = useAddItemAction({ isEnabled: entrypoint.type === 'link', library: view.libraryId, - entrypoint: view.entrypoint, - maxItemsLeft: null, - refetch + maxItemsLeft: null }); const {deactivateMassAction} = useDeactivateMassAction({ @@ -155,7 +149,7 @@ export const Explorer: FunctionComponent = ({ massActions: [deactivateMassAction, ...massActions].filter(Boolean) }); - const {primaryButton} = usePrimaryActionsButton([createAction, ...primaryActions].filter(Boolean)); + const {primaryButton} = usePrimaryActionsButton([createAction, addItemAction, ...primaryActions].filter(Boolean)); const {viewSettingsButton} = useOpenViewSettings(view.libraryId); diff --git a/libs/ui/src/components/Explorer/_queries/massSelectionIdsQuery.graphql b/libs/ui/src/components/Explorer/_queries/massSelectionIdsQuery.graphql new file mode 100644 index 000000000..70ee38776 --- /dev/null +++ b/libs/ui/src/components/Explorer/_queries/massSelectionIdsQuery.graphql @@ -0,0 +1,7 @@ +query ExplorerSelectionIds($libraryId: ID!, $filters: [RecordFilterInput],) { + records(library: $libraryId, filters: $filters) { + list { + id + } + } +} diff --git a/libs/ui/src/components/Explorer/_types.ts b/libs/ui/src/components/Explorer/_types.ts index c0c2cfbab..a2e847191 100644 --- a/libs/ui/src/components/Explorer/_types.ts +++ b/libs/ui/src/components/Explorer/_types.ts @@ -82,11 +82,7 @@ export interface IFilterDropDownProps { export type DefaultViewSettings = Override< Partial, { - filters?: Array<{ - field: string; - condition: RecordFilterCondition; - value: string | null; - }>; + filters?: IExplorerFilter[]; } >; diff --git a/libs/ui/src/components/Explorer/display-explorer-modal/DisplayExplorerModal.tsx b/libs/ui/src/components/Explorer/link-item/AddLinkModal.tsx similarity index 52% rename from libs/ui/src/components/Explorer/display-explorer-modal/DisplayExplorerModal.tsx rename to libs/ui/src/components/Explorer/link-item/AddLinkModal.tsx index c753409c8..f12997e08 100644 --- a/libs/ui/src/components/Explorer/display-explorer-modal/DisplayExplorerModal.tsx +++ b/libs/ui/src/components/Explorer/link-item/AddLinkModal.tsx @@ -2,25 +2,22 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {Modal} from 'antd'; -import {FunctionComponent, useRef} from 'react'; -import {KitButton, KitModal, KitSpace} from 'aristid-ds'; +import {ComponentProps, FunctionComponent, ReactNode, useRef} from 'react'; +import {closeKitSnackBar, KitButton, KitSpace} from 'aristid-ds'; import styled from 'styled-components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faXmark} from '@fortawesome/free-solid-svg-icons'; -import {Explorer, IExplorerProps} from '../Explorer'; -import {possibleSubmitButtons} from '_ui/components/RecordEdition/_types'; -import {IEntrypointLibrary, IEntrypointLink} from '../_types'; -import {FaPlus} from 'react-icons/fa'; -import useSaveValueBatchMutation from '_ui/components/RecordEdition/EditRecordContent/hooks/useExecuteSaveValueBatchMutation'; +import {Explorer} from '../Explorer'; +import {IEntrypointLink} from '../_types'; +import {useAddLinkMassAction} from '../useAddLinkMassAction'; +import {useViewSettingsContext} from '../manage-view-settings/store-view-settings/useViewSettingsContext'; import {EditSettingsContextProvider} from '../manage-view-settings'; -export interface IDisplayExplorerModalProps extends Omit { +interface IAddLinkModalProps { open: boolean; library: string; - entrypoint: IEntrypointLink; - onClose: () => void; - submitButtons?: possibleSubmitButtons; + onClose?: () => void; } const modalWidth = 1200; @@ -29,7 +26,7 @@ const StyledModal = styled(Modal)` .ant-modal-content { display: flex; flex-direction: column; - padding: 10px; + padding: calc(var(--general-spacing-xs) * 1px); overflow: hidden; } @@ -46,42 +43,33 @@ const ModalMainStyledDiv = styled.div` position: relative; `; -const Header = styled.div` - height: 3.5rem; - grid-area: title; - align-self: center; - font-size: 1rem; - padding: 10px 50px 10px 10px; - border-bottom: 1px solid var(--general-utilities-border); - display: flex; - justify-content: space-between; - align-items: center; -`; - const ModalFooter = styled.div` display: flex; justify-content: flex-end; - padding: 0.5rem 1rem; + padding: calc(var(--general-spacing-xs) * 1px) calc(var(--general-spacing-s) * 1px); border-top: 1px solid var(--general-utilities-border); `; -export const DisplayExplorerModal: FunctionComponent = ({ - open, - library, - entrypoint, - title, - defaultViewSettings, - onClose -}) => { +export const AddLinkModal: FunctionComponent = ({open, library, onClose}) => { const {t} = useSharedTranslation(); const explorerContainerRef = useRef(null); - const {saveValues} = useSaveValueBatchMutation(); + const {view, dispatch} = useViewSettingsContext(); - const _handleClose = () => onClose(); + const {addLinkMassAction} = useAddLinkMassAction({ + isEnabled: true, + store: {view, dispatch}, + linkAttributeId: (view.entrypoint as IEntrypointLink).linkAttributeId, + libraryId: view.libraryId + }); + + const _handleClose = () => { + closeKitSnackBar(); + onClose?.(); + }; - const _closeButtonLabel = t('global.close'); + const _closeButtonLabel: string = t('global.close'); - const _footerButtons = [ + const _footerButtons: ReactNode = [ ]; - const _footer = ( + const _footer: ComponentProps['footer'] = ( {_footerButtons} ); - const _internalEntrypoint: IEntrypointLibrary = { - type: 'library', - libraryId: library - }; - - const addItemAction = { - label: t('filters.add'), - icon: , - callback: ({itemId}) => - saveValues( - { - id: entrypoint.parentRecordId, - library: { - id: entrypoint.parentLibraryId - } - }, - [ - { - attribute: entrypoint.linkAttributeId, - idValue: null, - value: itemId - } - ] - ) - }; - return ( explorerContainerRef.current ?? document.body} - defaultViewSettings={defaultViewSettings} /> diff --git a/libs/ui/src/components/Explorer/useAddItemAction.tsx b/libs/ui/src/components/Explorer/useAddItemAction.tsx index 5d4ebd8d1..39e27762b 100644 --- a/libs/ui/src/components/Explorer/useAddItemAction.tsx +++ b/libs/ui/src/components/Explorer/useAddItemAction.tsx @@ -3,35 +3,28 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {useState} from 'react'; import {FaPlus} from 'react-icons/fa'; -import {EditRecordModal} from '_ui/components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {ActionHook, Entrypoint, IEntrypointLink, IPrimaryAction} from './_types'; -import useSaveValueBatchMutation from '../RecordEdition/EditRecordContent/hooks/useExecuteSaveValueBatchMutation'; -import {useExplorerLinkAttributeQuery} from '_ui/_gqlTypes'; -import {DisplayExplorerModal} from './display-explorer-modal/DisplayExplorerModal'; +import {ActionHook, IPrimaryAction} from './_types'; +import {AddLinkModal} from './link-item/AddLinkModal'; /** * Hook used to get the action for `` component. * - * When the creation is done, we refresh all data even if the new record will not be visible due to some filters. + * When items are linked, the view is refreshed * - * It returns also two parts : one for the call action button - one for displayed the modal required by the action. + * It returns also two parts : one for the call action button - one for displaying the modal required by the action. * * @param isEnabled - whether the action is present * @param library - the library's id to add new item - * @param refetch - method to call to refresh the list. New item will be visible if it matches filters and sorts + * @param maxItemsLeft - the number of items that can be added */ export const useAddItemAction = ({ isEnabled, - entrypoint, library, - maxItemsLeft, - refetch + maxItemsLeft }: ActionHook<{ - entrypoint: Entrypoint; library: string; maxItemsLeft: number | null; - refetch: () => void; }>) => { const {t} = useSharedTranslation(); @@ -51,9 +44,8 @@ export const useAddItemAction = ({ return { addItemAction: isEnabled ? addItemAction : null, addItemModal: isAddItemModalVisible ? ( - { setIsAddItemModalVisible(false); diff --git a/libs/ui/src/components/Explorer/useAddLinkMassAction.tsx b/libs/ui/src/components/Explorer/useAddLinkMassAction.tsx new file mode 100644 index 000000000..5927e51f4 --- /dev/null +++ b/libs/ui/src/components/Explorer/useAddLinkMassAction.tsx @@ -0,0 +1,80 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {Dispatch, useMemo} from 'react'; +import {FaTrash} from 'react-icons/fa'; +import {useExplorerSelectionIdsLazyQuery} from '_ui/_gqlTypes'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; +import {ActionHook, IEntrypointLink, IMassActions} from './_types'; +import {IViewSettingsAction, IViewSettingsState} from './manage-view-settings'; +import useSaveValueBatchMutation from '_ui/components/RecordEdition/EditRecordContent/hooks/useExecuteSaveValueBatchMutation'; + +/** + * Hook used to get the action for mass actions only available on selection. + * + * When the mutation for adding item is done, the Apollo cache will be clean (`Record` and `RecordIdentity`) + * + * @param isEnabled - whether the action is present + * @param view - represent the current view + * @param dispatch - method to change the current view + * @param libraryId - concerned library + */ +export const useAddLinkMassAction = ({ + isEnabled, + store: {view, dispatch}, + libraryId, + linkAttributeId +}: ActionHook<{ + store: { + view: IViewSettingsState; + dispatch: Dispatch; + }; + libraryId: string; + linkAttributeId: string; +}>) => { + const {t} = useSharedTranslation(); + + const {saveValues} = useSaveValueBatchMutation(); + + const [fetch] = useExplorerSelectionIdsLazyQuery({ + fetchPolicy: 'no-cache', + onCompleted: data => { + const entrypoint = view.entrypoint as IEntrypointLink; + const values = data.records.list.map(({id}) => ({ + attribute: linkAttributeId, + idValue: null, + value: id + })); + + saveValues( + { + id: entrypoint.parentRecordId, + library: { + id: entrypoint.parentLibraryId + } + }, + values + ); + } + }); + + const _addLinkMassAction: IMassActions = useMemo( + () => ({ + label: t('explorer.massAction.add-link'), + icon: , + callback: massSelectionFilter => { + fetch({ + variables: { + libraryId, + filters: massSelectionFilter + } + }); + } + }), + [t, fetch, view.massSelection, dispatch, libraryId] + ); + + return { + addLinkMassAction: isEnabled ? _addLinkMassAction : null + }; +}; diff --git a/libs/ui/src/components/Explorer/useColumnWidth.tsx b/libs/ui/src/components/Explorer/useColumnWidth.tsx index 489c5f455..ae60f23a1 100644 --- a/libs/ui/src/components/Explorer/useColumnWidth.tsx +++ b/libs/ui/src/components/Explorer/useColumnWidth.tsx @@ -31,6 +31,8 @@ const _getFieldColumWidth = (field: IExplorerData['attributes'][string]): number } }; +export const lastColumnsInlinePadding = 36; + export const useColumnWidth = () => { const ref = useRef(null); const [columnWidth, setColumnWidth] = useState(FieldColumnWidth.TINY); @@ -39,7 +41,6 @@ export const useColumnWidth = () => { if (ref.current) { const columnElement = ref.current; const columnElementtWidth = columnElement.getBoundingClientRect().width; - const lastColumnsInlinePadding = 36; if (columnElementtWidth !== columnWidth - lastColumnsInlinePadding) { setColumnWidth(columnElementtWidth + lastColumnsInlinePadding); @@ -50,6 +51,7 @@ export const useColumnWidth = () => { return { ref, getFieldColumnWidth: _getFieldColumWidth, - columnWidth + columnWidth, + actionsColumnHeaderWidth: columnWidth - lastColumnsInlinePadding }; }; diff --git a/libs/ui/src/components/Explorer/usePrimaryActions.tsx b/libs/ui/src/components/Explorer/usePrimaryActions.tsx index c0b62fe51..5c8f01255 100644 --- a/libs/ui/src/components/Explorer/usePrimaryActions.tsx +++ b/libs/ui/src/components/Explorer/usePrimaryActions.tsx @@ -1,9 +1,8 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {FaEllipsisV} from 'react-icons/fa'; import {IPrimaryAction} from './_types'; -import {KitButton, KitDropDown, KitSpace} from 'aristid-ds'; +import {KitButton, KitSpace} from 'aristid-ds'; /** * Hook used to get the primary actions for `` component. @@ -25,32 +24,19 @@ export const usePrimaryActionsButton = (actions: IPrimaryAction[]) => { icon={firstAction.icon} disabled={firstAction.disabled} onClick={firstAction.callback} + items={dropdownActions.map((action, index) => ({ + key: index, + label: ( + + {action.icon} {action.label} + + ), + disabled: action.disabled, + onClick: action.callback + }))} > {firstAction.label} - {dropdownActions.length > 0 && ( - ({ - key: index, - label: ( - - {action.icon} {action.label} - - ), - disabled: action.disabled, - onClick: action.callback - })) - }} - > - } - > - - )} ) }; diff --git a/libs/ui/src/locales/en/shared.json b/libs/ui/src/locales/en/shared.json index 23dc1e30c..94e407b3f 100644 --- a/libs/ui/src/locales/en/shared.json +++ b/libs/ui/src/locales/en/shared.json @@ -675,6 +675,7 @@ "selectedItems_one": "{{count, number}} selected", "selectedItems_other": "{{count, number}} selected", "deactivate": "Deactivate", + "add-link": "Add", "toggle_selection": { "select_page_one": "Select visible item ({{count, number}})", "select_page_other": "Select visible items ({{count, number}})", diff --git a/libs/ui/src/locales/fr/shared.json b/libs/ui/src/locales/fr/shared.json index c5022e9b2..fb79018a2 100644 --- a/libs/ui/src/locales/fr/shared.json +++ b/libs/ui/src/locales/fr/shared.json @@ -675,6 +675,7 @@ "selectedItems_one": "{{count, number}} sélectionné", "selectedItems_other": "{{count, number}} sélectionnés", "deactivate": "Supprimer", + "add-link": "Ajouter", "toggle_selection": { "select_page_one": "Sélectionner l’éléments visible ({{count, number}})", "select_page_other": "Sélectionner les éléments visibles ({{count, number}})",