Skip to content

Commit

Permalink
feat(explorer): create and link a new record to link attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
emile committed Jan 17, 2025
1 parent 412fb2f commit 1d496c7
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 42 deletions.
10 changes: 5 additions & 5 deletions libs/ui/src/_gqlTypes/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +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 {IPreviewScalar} from '@leav/utils';
import {gql} from '@apollo/client';
import {IPreviewScalar} from '@leav/utils'
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
Expand Down Expand Up @@ -284,7 +284,6 @@ export enum FormsSortableFields {
system = 'system'
}


export type GlobalSettingsFileInput = {
library: Scalars['String'];
recordId: Scalars['String'];
Expand Down Expand Up @@ -1363,7 +1362,7 @@ export type ExplorerLinkAttributeQueryVariables = Exact<{
}>;


export type ExplorerLinkAttributeQuery = { attributes?: { list: Array<{ label?: any | null, id: string, linked_library?: { id: string } | null } | { id: string }> } | null };
export type ExplorerLinkAttributeQuery = { attributes?: { list: Array<{ label?: any | null, id: string, multiple_values: boolean, linked_library?: { id: string } | null } | { id: string, multiple_values: boolean }> } | null };

export type ExplorerLibraryDataQueryVariables = Exact<{
libraryId: Scalars['ID'];
Expand Down Expand Up @@ -4093,6 +4092,7 @@ export const ExplorerLinkAttributeDocument = gql`
attributes(filters: {ids: [$id]}) {
list {
id
multiple_values
...LinkAttributeDetails
}
}
Expand Down Expand Up @@ -4313,4 +4313,4 @@ export function useTreeDataQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti
}
export type TreeDataQueryQueryHookResult = ReturnType<typeof useTreeDataQueryQuery>;
export type TreeDataQueryLazyQueryHookResult = ReturnType<typeof useTreeDataQueryLazyQuery>;
export type TreeDataQueryQueryResult = Apollo.QueryResult<TreeDataQueryQuery, TreeDataQueryQueryVariables>;
export type TreeDataQueryQueryResult = Apollo.QueryResult<TreeDataQueryQuery, TreeDataQueryQueryVariables>;
107 changes: 93 additions & 14 deletions libs/ui/src/components/Explorer/Explorer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ import * as gqlTypes from '_ui/_gqlTypes';
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';

const EditRecordModalMock = 'EditRecordModal';

jest.mock('_ui/components/RecordEdition/EditRecordModal', () => ({
EditRecordModal: () => <div>{EditRecordModalMock}</div>
EditRecordModal: ({onCreate}) => (
<div>
{EditRecordModalMock}
<button onClick={() => onCreate({})}>create-record</button>
</div>
)
}));

jest.mock('@uidotdev/usehooks', () => ({
Expand Down Expand Up @@ -513,6 +520,7 @@ describe('Explorer', () => {

const explorerLinkAttribute = {
id: 'link_attribute',
multiple_values: true,
label: {
en: 'Delivery Platforms',
fr: 'Plateformes de diffusion'
Expand Down Expand Up @@ -541,6 +549,23 @@ describe('Explorer', () => {
}
};

const ExplorerLinkAttributeMonoValueQueryMock = {
request: {
query: gqlTypes.ExplorerLinkAttributeDocument,
variables: {
id: linkEntrypoint.linkAttributeId
}
},
result: {
data: {
attributes: {
list: [explorerLinkAttribute],
__typename: 'AttributesList'
}
}
}
};

beforeEach(() => {
spyUseExplorerLibraryDataQuery = jest
.spyOn(gqlTypes, 'useExplorerLibraryDataQuery')
Expand Down Expand Up @@ -573,6 +598,11 @@ describe('Explorer', () => {
user = userEvent.setup();
});

afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});

describe('props title', () => {
test('Should display library label as title', () => {
render(<Explorer entrypoint={libraryEntrypoint} />);
Expand Down Expand Up @@ -788,12 +818,61 @@ describe('Explorer', () => {
expect(screen.getByText(EditRecordModalMock)).toBeVisible();
});

test('should not try to link created record if entrypoint is not a link', async () => {
const saveValues = jest.fn();
jest.spyOn(useExecuteSaveValueBatchMutation, 'default').mockReturnValue({
loading: false,
saveValues
});

jest.spyOn(useExplorerData, 'useExplorerData').mockReturnValue({
refetch: jest.fn(),
loading: false,
data: null
});

render(<Explorer entrypoint={libraryEntrypoint} />);

await user.click(screen.getByRole('button', {name: 'explorer.create-one'}));

expect(screen.getByText(EditRecordModalMock)).toBeVisible();
const createButtonLibrary = screen.getByRole('button', {name: 'create-record'});
await user.click(createButtonLibrary);

expect(saveValues).not.toHaveBeenCalled();
});

test('Should be able to link a new record', async () => {
const saveValues = jest.fn();
jest.spyOn(useExecuteSaveValueBatchMutation, 'default').mockReturnValue({
loading: false,
saveValues
});

const spiedModule = jest.spyOn(useExplorerData, 'useExplorerData').mockReturnValue({
refetch: jest.fn(),
loading: false,
data: null
});

render(<Explorer entrypoint={linkEntrypoint} />, {
mocks: [ExplorerLinkAttributeQueryMock]
});

await user.click(screen.getByRole('button', {name: 'explorer.create-one'}));

expect(screen.getByText(EditRecordModalMock)).toBeVisible();

const createButton = screen.getByRole('button', {name: 'create-record'});
await user.click(createButton);

expect(saveValues).toHaveBeenCalled();
});

test('Should be able to display custom primary actions', async () => {
render(<Explorer entrypoint={libraryEntrypoint} primaryActions={customPrimaryActions} />);

const createButton = screen.getByRole('button', {name: 'explorer.create-one'});
const dropdownButton = createButton.nextElementSibling; // Not nice, but no way to get the dropdown button directly

const dropdownButton = screen.getByTestId('actions-dropdown');
expect(screen.queryByText(customPrimaryAction1.label)).not.toBeInTheDocument();
expect(screen.queryByText(customPrimaryAction2.label)).not.toBeInTheDocument();

Expand Down Expand Up @@ -822,7 +901,7 @@ describe('Explorer', () => {
await user.click(firstActionButton);
expect(customPrimaryActions[0].callback).toHaveBeenCalled();

const dropdownButton = firstActionButton.nextElementSibling; // Not nice, but no way to get the dropdown button directly
const dropdownButton = screen.getByTestId('actions-dropdown');
expect(screen.queryByText(customPrimaryAction2.label)).not.toBeInTheDocument();

await user.click(dropdownButton!);
Expand Down Expand Up @@ -916,16 +995,16 @@ describe('Explorer', () => {
}
};

const spy = jest
.spyOn(gqlTypes, 'useExplorerLibraryDataQuery')
.mockImplementation(
({variables}) =>
(Array.isArray(variables?.filters) && variables.filters.length
? mockExplorerLibraryDataQueryResultWithFilters
: mockExplorerLibraryDataQueryResult) as gqlTypes.ExplorerLibraryDataQueryResult
);

test('should handle filters for the request and for the display', async () => {
const spy = jest
.spyOn(gqlTypes, 'useExplorerLibraryDataQuery')
.mockImplementation(
({variables}) =>
(Array.isArray(variables?.filters) && variables.filters.length
? mockExplorerLibraryDataQueryResultWithFilters
: mockExplorerLibraryDataQueryResult) as gqlTypes.ExplorerLibraryDataQueryResult
);

render(
<Explorer
entrypoint={{type: 'library', libraryId: 'campaigns'}}
Expand Down
3 changes: 3 additions & 0 deletions libs/ui/src/components/Explorer/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const ExplorerHeaderDivStyled = styled.div`
justify-content: space-between;
align-items: center;
padding-bottom: calc(var(--general-spacing-xs) * 1px);
padding-right: calc(var(--general-spacing-xxs) * 1px);
`;

const ExplorerPageDivStyled = styled.div`
Expand Down Expand Up @@ -101,6 +102,8 @@ export const Explorer: FunctionComponent<IExplorerProps> = ({
const {createAction, createModal} = useCreateAction({
isEnabled: isNotEmpty(defaultPrimaryActions) && defaultPrimaryActions.includes('create'),
library: view.libraryId,
entrypoint: view.entrypoint,
itemsNumber: data?.totalCount ?? 0,
refetch
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ query ExplorerLinkAttribute($id: ID!) {
attributes(filters: { ids: [$id] }) {
list {
id
multiple_values
...LinkAttributeDetails
}
}
Expand Down
1 change: 1 addition & 0 deletions libs/ui/src/components/Explorer/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface IItemAction {

export interface IPrimaryAction {
callback: () => void;
disabled?: boolean;
icon: ReactElement;
label: string;
}
Expand Down
46 changes: 44 additions & 2 deletions libs/ui/src/components/Explorer/useCreateAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {useState} from 'react';
import {FaPlus} from 'react-icons/fa';
import {EditRecordModal} from '_ui/components';
import {useSharedTranslation} from '_ui/hooks/useSharedTranslation';
import {ActionHook, IPrimaryAction} from './_types';
import {ActionHook, Entrypoint, IEntrypointLink, IPrimaryAction} from './_types';
import useSaveValueBatchMutation from '../RecordEdition/EditRecordContent/hooks/useExecuteSaveValueBatchMutation';
import {useExplorerLinkAttributeQuery} from '_ui/_gqlTypes';

/**
* Hook used to get the action for `<DataView />` component.
Expand All @@ -21,20 +23,43 @@ import {ActionHook, IPrimaryAction} from './_types';
export const useCreateAction = ({
isEnabled,
library,
entrypoint,
itemsNumber,
refetch
}: ActionHook<{
library: string;
entrypoint: Entrypoint;
itemsNumber: number;
refetch: () => void;
}>) => {
const {t} = useSharedTranslation();

const [isRecordCreationVisible, setRecordCreationVisible] = useState(false);
const [multipleValues, setIsMultivalues] = useState(false);
const {saveValues} = useSaveValueBatchMutation();

useExplorerLinkAttributeQuery({
skip: entrypoint.type !== 'link',
variables: {
id: (entrypoint as IEntrypointLink).linkAttributeId
},
onCompleted: data => {
const attributeData = data?.attributes?.list?.[0];
if (!attributeData) {
throw new Error('Unknown link attribute');
}
setIsMultivalues(attributeData.multiple_values);
}
});

const canCreateRecord = entrypoint.type === 'library' ? true : multipleValues || itemsNumber === 0;

const createAction: IPrimaryAction = {
callback: () => {
setRecordCreationVisible(true);
},
icon: <FaPlus />,
disabled: !canCreateRecord,
label: t('explorer.create-one')
};

Expand All @@ -48,9 +73,26 @@ export const useCreateAction = ({
onClose={() => {
setRecordCreationVisible(false);
}}
onCreate={() => {
onCreate={newRecord => {
refetch();
setRecordCreationVisible(false);
if (entrypoint.type === 'link') {
saveValues(
{
id: entrypoint.parentRecordId,
library: {
id: entrypoint.parentLibraryId
}
},
[
{
attribute: entrypoint.linkAttributeId,
idValue: null,
value: newRecord.id
}
]
);
}
}}
submitButtons={['create']}
/>
Expand Down
52 changes: 35 additions & 17 deletions libs/ui/src/components/Explorer/usePrimaryActions.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// 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, KitSpace} from 'aristid-ds';
import {KitButton, KitDropDown, KitSpace} from 'aristid-ds';

/**
* Hook used to get the primary actions for `<DataView />` component.
Expand All @@ -18,22 +19,39 @@ export const usePrimaryActionsButton = (actions: IPrimaryAction[]) => {
return {
primaryButton:
actions.length === 0 ? null : (
<KitButton
type="primary"
icon={firstAction.icon}
onClick={firstAction.callback}
items={dropdownActions.map((action, index) => ({
key: index,
label: (
<KitSpace size={8}>
{action.icon} {action.label}
</KitSpace>
),
onClick: action.callback
}))}
>
{firstAction.label}
</KitButton>
<>
<KitButton
type="primary"
icon={firstAction.icon}
disabled={firstAction.disabled}
onClick={firstAction.callback}
>
{firstAction.label}
</KitButton>
{dropdownActions.length > 0 && (
<KitDropDown
trigger={['click']}
menu={{
items: dropdownActions.map((action, index) => ({
key: index,
label: (
<KitSpace size={8}>
{action.icon} {action.label}
</KitSpace>
),
disabled: action.disabled,
onClick: action.callback
}))
}}
>
<KitButton
data-testid="actions-dropdown"
type="secondary"
icon={<FaEllipsisV />}
></KitButton>
</KitDropDown>
)}
</>
)
};
};
Loading

0 comments on commit 1d496c7

Please sign in to comment.