From 7707708f33970fe9988939ac68510d0acbf7c129 Mon Sep 17 00:00:00 2001 From: Meetul Rathore Date: Sat, 2 Nov 2024 17:59:18 +0530 Subject: [PATCH] feat: Add filtering and sorting on userTag screens (GSoC) (#2398) * add people to tag functaionality * minor change * more translations * minor change * add a variable for page size * add tag actions * add tests * translations * change to infinite scroll * add subtags infinite scroll * extract common variables to utils * minor correction * more design adjustments * add tests * add scrollbar for infinite scroll * extract infinite scroll loader * minor adjustment * refactoring * exclude ManageTag from countline check * fix linting * fix linting * fix linting * make coderabbit suggested changes * more changes * more changes * minor correction * add error component for tagNode subtags query * fix translation * fix translation * remove unused css classes * refactor ManageTag file to reduce its size * update pull-request check * minor adjustments * minor correction * minor change * correction * minor changes * minor change * coderabbitai changes * more changes * fix dataGrid classes * remove getUserTagAncestors query * add search and sort * add tests * error toast on no seletion tag/user * minor correction * fix test * minor change * add useEffect * exclude TagActionsMocks from countlines check * coderabbitai suggested changes * show sort order --- .github/workflows/pull-request.yml | 2 +- public/locales/en/translation.json | 7 +- public/locales/fr/translation.json | 7 +- public/locales/hi/translation.json | 7 +- public/locales/sp/translation.json | 7 +- public/locales/zh/translation.json | 7 +- src/GraphQl/Queries/OrganizationQueries.ts | 18 +- src/GraphQl/Queries/userTagQueries.ts | 45 ++- .../AddPeopleToTag/AddPeopleToTag.module.css | 9 +- .../AddPeopleToTag/AddPeopleToTag.test.tsx | 123 +++++- .../AddPeopleToTag/AddPeopleToTag.tsx | 120 ++++-- .../AddPeopleToTag/AddPeopleToTagsMocks.ts | 106 ++++++ .../TagActions/TagActions.module.css | 10 +- src/components/TagActions/TagActions.test.tsx | 36 ++ src/components/TagActions/TagActions.tsx | 162 ++++---- src/components/TagActions/TagActionsMocks.ts | 173 +++++++++ src/components/TagActions/TagNode.tsx | 2 +- src/screens/ManageTag/ManageTag.test.tsx | 95 ++++- src/screens/ManageTag/ManageTag.tsx | 357 +++++++++--------- src/screens/ManageTag/ManageTagMocks.ts | 163 +++++--- .../OrganizationTags.test.tsx | 88 ++++- .../OrganizationTags/OrganizationTags.tsx | 198 ++++++---- .../OrganizationTags/OrganizationTagsMocks.ts | 178 +++++++++ src/screens/SubTags/SubTags.test.tsx | 142 +++++-- src/screens/SubTags/SubTags.tsx | 237 ++++++------ src/screens/SubTags/SubTagsMocks.ts | 298 +++++++++++---- src/utils/interfaces.ts | 18 +- src/utils/organizationTagsUtils.ts | 3 + 28 files changed, 1888 insertions(+), 730 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a0a542f3cf..27f18e28ab 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -38,7 +38,7 @@ jobs: - name: Count number of lines run: | chmod +x ./.github/workflows/countline.py - ./.github/workflows/countline.py --lines 600 --exclude_files src/screens/LoginPage/LoginPage.tsx src/GraphQl/Queries/Queries.ts src/screens/OrgList/OrgList.tsx src/GraphQl/Mutations/mutations.ts src/components/EventListCard/EventListCardModals.tsx + ./.github/workflows/countline.py --lines 600 --exclude_files src/screens/LoginPage/LoginPage.tsx src/GraphQl/Queries/Queries.ts src/screens/OrgList/OrgList.tsx src/GraphQl/Mutations/mutations.ts src/components/EventListCard/EventListCardModals.tsx src/components/TagActions/TagActionsMocks.ts - name: Get changed TypeScript files id: changed-files diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 7d2108bc06..55865167d5 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -320,7 +320,8 @@ "noTagsFound": "No tags found", "removeUserTag": "Delete Tag", "removeUserTagMessage": "Do you want to delete this tag?", - "addChildTag": "Add a Sub Tag" + "addChildTag": "Add a Sub Tag", + "enterTagName": "Enter Tag Name" }, "manageTag": { "title": "Tag Details", @@ -333,7 +334,6 @@ "addPeople": "Add People", "add": "Add", "subTags": "Sub Tags", - "assignedToAll": "Tag Assigned to All", "successfullyAssignedToPeople": "Tag assigned successfully", "errorOccurredWhileLoadingMembers": "Error occured while loading members", "userName": "User Name", @@ -359,7 +359,8 @@ "collapse": "Collapse", "expand": "Expand", "tagNamePlaceholder": "Write the name of the tag", - "allTags": "All Tags" + "allTags": "All Tags", + "noMoreMembersFound": "No more members found" }, "userListCard": { "addAdmin": "Add Admin", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 7f74826272..8bdb3e854a 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -320,7 +320,8 @@ "noTagsFound": "Aucune étiquette trouvée", "removeUserTag": "Supprimer l'Étiquette", "removeUserTagMessage": "Voulez-vous supprimer cette étiquette ?", - "addChildTag": "Ajouter une Sous-Étiquette" + "addChildTag": "Ajouter une Sous-Étiquette", + "enterTagName": "Entrez le nom de l'étiquette" }, "manageTag": { "title": "Détails de l'étiquette", @@ -333,7 +334,6 @@ "addPeople": "Ajouter des personnes", "add": "Ajouter", "subTags": "Sous-étiquettes", - "assignedToAll": "Étiquette attribuée à tous", "successfullyAssignedToPeople": "Étiquette attribuée avec succès", "errorOccurredWhileLoadingMembers": "Erreur survenue lors du chargement des membres", "userName": "Nom d'utilisateur", @@ -359,7 +359,8 @@ "collapse": "Réduire", "expand": "Développer", "tagNamePlaceholder": "Écrire le nom de l'étiquette", - "allTags": "Toutes les étiquettes" + "allTags": "Toutes les étiquettes", + "noMoreMembersFound": "Aucun autre membre trouvé" }, "userListCard": { "addAdmin": "Ajouter un administrateur", diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index 4384648ca3..b3b00bd989 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -320,7 +320,8 @@ "noTagsFound": "कोई टैग नहीं मिला", "removeUserTag": "टैग हटाएँ", "removeUserTagMessage": "क्या आप इस टैग को हटाना चाहते हैं?", - "addChildTag": "उप-टैग जोड़ें" + "addChildTag": "उप-टैग जोड़ें", + "enterTagName": "टैग का नाम दर्ज करें" }, "manageTag": { "title": "टैग विवरण", @@ -333,7 +334,6 @@ "addPeople": "लोगों को जोड़ें", "add": "जोड़ें", "subTags": "उप-टैग्स", - "assignedToAll": "सभी को टैग असाइन किया गया", "successfullyAssignedToPeople": "टैग सफलतापूर्वक असाइन किया गया", "errorOccurredWhileLoadingMembers": "सदस्यों को लोड करते समय त्रुटि हुई", "userName": "उपयोगकर्ता नाम", @@ -359,7 +359,8 @@ "collapse": "संक्षिप्त करें", "expand": "विस्तारित करें", "tagNamePlaceholder": "टैग का नाम लिखें", - "allTags": "सभी टैग" + "allTags": "सभी टैग", + "noMoreMembersFound": "कोई और सदस्य नहीं मिला" }, "userListCard": { "addAdmin": "व्यवस्थापक जोड़ें", diff --git a/public/locales/sp/translation.json b/public/locales/sp/translation.json index 24ce0dbdec..f698238e56 100644 --- a/public/locales/sp/translation.json +++ b/public/locales/sp/translation.json @@ -320,7 +320,8 @@ "noTagsFound": "No se encontraron etiquetas", "removeUserTag": "Eliminar Etiqueta", "removeUserTagMessage": "¿Desea eliminar esta etiqueta?", - "addChildTag": "Agregar una Sub Etiqueta" + "addChildTag": "Agregar una Sub Etiqueta", + "enterTagName": "Ingrese el nombre de la etiqueta" }, "manageTag": { "title": "Detalles de la Etiqueta", @@ -333,7 +334,6 @@ "addPeople": "Agregar Personas", "add": "Agregar", "subTags": "Subetiquetas", - "assignedToAll": "Etiqueta asignada a todos", "successfullyAssignedToPeople": "Etiqueta asignada con éxito", "errorOccurredWhileLoadingMembers": "Error al cargar los miembros", "userName": "Nombre de usuario", @@ -359,7 +359,8 @@ "collapse": "Colapsar", "expand": "Expandir", "tagNamePlaceholder": "Escribe el nombre de la etiqueta", - "allTags": "Todas las etiquetas" + "allTags": "Todas las etiquetas", + "noMoreMembersFound": "No se encontraron más miembros" }, "userListCard": { "joined": "Unido", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 0c070bcf8b..5d8cfc4826 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -320,7 +320,8 @@ "noTagsFound": "未找到标签", "removeUserTag": "删除标签", "removeUserTagMessage": "您确定要删除此标签吗?", - "addChildTag": "添加子标签" + "addChildTag": "添加子标签", + "enterTagName": "输入标签名称" }, "manageTag": { "title": "标签详情", @@ -333,7 +334,6 @@ "addPeople": "添加人员", "add": "添加", "subTags": "子标签", - "assignedToAll": "标签分配给所有人", "successfullyAssignedToPeople": "标签分配成功", "errorOccurredWhileLoadingMembers": "加载成员时出错", "userName": "用户名", @@ -359,7 +359,8 @@ "collapse": "收起", "expand": "展开", "tagNamePlaceholder": "输入标签名称", - "allTags": "所有标签" + "allTags": "所有标签", + "noMoreMembersFound": "未找到更多成员" }, "userListCard": { "addAdmin": "添加管理员", diff --git a/src/GraphQl/Queries/OrganizationQueries.ts b/src/GraphQl/Queries/OrganizationQueries.ts index 315c50ebf4..4dc7dd7a09 100644 --- a/src/GraphQl/Queries/OrganizationQueries.ts +++ b/src/GraphQl/Queries/OrganizationQueries.ts @@ -89,19 +89,35 @@ export const ORGANIZATION_USER_TAGS_LIST = gql` $before: String $first: PositiveInt $last: PositiveInt + $where: UserTagWhereInput + $sortedBy: UserTagSortedByInput ) { organizations(id: $id) { - userTags(after: $after, before: $before, first: $first, last: $last) { + userTags( + after: $after + before: $before + first: $first + last: $last + where: $where + sortedBy: $sortedBy + ) { edges { node { _id name + parentTag { + _id + } usersAssignedTo(first: $first, last: $last) { totalCount } childTags(first: $first, last: $last) { totalCount } + ancestorTags { + _id + name + } } cursor } diff --git a/src/GraphQl/Queries/userTagQueries.ts b/src/GraphQl/Queries/userTagQueries.ts index 33a9b63174..d58da19e1b 100644 --- a/src/GraphQl/Queries/userTagQueries.ts +++ b/src/GraphQl/Queries/userTagQueries.ts @@ -14,6 +14,8 @@ export const USER_TAGS_ASSIGNED_MEMBERS = gql` $before: String $first: PositiveInt $last: PositiveInt + $where: UserTagUsersAssignedToWhereInput + $sortedBy: UserTagUsersAssignedToSortedByInput ) { getAssignedUsers: getUserTag(id: $id) { name @@ -22,6 +24,8 @@ export const USER_TAGS_ASSIGNED_MEMBERS = gql` before: $before first: $first last: $last + where: $where + sortedBy: $sortedBy ) { edges { node { @@ -38,6 +42,10 @@ export const USER_TAGS_ASSIGNED_MEMBERS = gql` } totalCount } + ancestorTags { + _id + name + } } } `; @@ -56,10 +64,19 @@ export const USER_TAG_SUB_TAGS = gql` $before: String $first: PositiveInt $last: PositiveInt + $where: UserTagWhereInput + $sortedBy: UserTagSortedByInput ) { getChildTags: getUserTag(id: $id) { name - childTags(after: $after, before: $before, first: $first, last: $last) { + childTags( + after: $after + before: $before + first: $first + last: $last + where: $where + sortedBy: $sortedBy + ) { edges { node { _id @@ -70,6 +87,10 @@ export const USER_TAG_SUB_TAGS = gql` childTags(first: $first, last: $last) { totalCount } + ancestorTags { + _id + name + } } } pageInfo { @@ -80,6 +101,10 @@ export const USER_TAG_SUB_TAGS = gql` } totalCount } + ancestorTags { + _id + name + } } } `; @@ -98,6 +123,7 @@ export const USER_TAGS_MEMBERS_TO_ASSIGN_TO = gql` $before: String $first: PositiveInt $last: PositiveInt + $where: UserTagUsersToAssignToWhereInput ) { getUsersToAssignTo: getUserTag(id: $id) { name @@ -106,6 +132,7 @@ export const USER_TAGS_MEMBERS_TO_ASSIGN_TO = gql` before: $before first: $first last: $last + where: $where ) { edges { node { @@ -125,19 +152,3 @@ export const USER_TAGS_MEMBERS_TO_ASSIGN_TO = gql` } } `; - -/** - * GraphQL query to retrieve the ancestor tags of a certain tag. - * - * @param id - The ID of the current tag. - * @returns The list of ancestor tags. - */ - -export const USER_TAG_ANCESTORS = gql` - query GetUserTagAncestors($id: ID!) { - getUserTagAncestors(id: $id) { - _id - name - } - } -`; diff --git a/src/components/AddPeopleToTag/AddPeopleToTag.module.css b/src/components/AddPeopleToTag/AddPeopleToTag.module.css index c7c5c4582b..5dd04ffed5 100644 --- a/src/components/AddPeopleToTag/AddPeopleToTag.module.css +++ b/src/components/AddPeopleToTag/AddPeopleToTag.module.css @@ -28,7 +28,7 @@ } .scrollContainer { - max-height: 100px; + height: 100px; overflow-y: auto; margin-bottom: 1rem; } @@ -45,3 +45,10 @@ .removeFilterIcon { cursor: pointer; } + +.loadingDiv { + min-height: 300px; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/components/AddPeopleToTag/AddPeopleToTag.test.tsx b/src/components/AddPeopleToTag/AddPeopleToTag.test.tsx index e21ccd1a52..824bc25654 100644 --- a/src/components/AddPeopleToTag/AddPeopleToTag.test.tsx +++ b/src/components/AddPeopleToTag/AddPeopleToTag.test.tsx @@ -17,7 +17,7 @@ import { store } from 'state/store'; import userEvent from '@testing-library/user-event'; import { StaticMockLink } from 'utils/StaticMockLink'; import { toast } from 'react-toastify'; -import type { ApolloLink } from '@apollo/client'; +import { InMemoryCache, type ApolloLink } from '@apollo/client'; import type { InterfaceAddPeopleToTagProps } from './AddPeopleToTag'; import AddPeopleToTag from './AddPeopleToTag'; import i18n from 'utils/i18nForTest'; @@ -63,12 +63,39 @@ const props: InterfaceAddPeopleToTagProps = { >, }; +const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + getUserTag: { + merge(existing = {}, incoming) { + const merged = { + ...existing, + ...incoming, + usersToAssignTo: { + ...existing.usersToAssignTo, + ...incoming.usersToAssignTo, + edges: [ + ...(existing.usersToAssignTo?.edges || []), + ...(incoming.usersToAssignTo?.edges || []), + ], + }, + }; + + return merged; + }, + }, + }, + }, + }, +}); + const renderAddPeopleToTagModal = ( props: InterfaceAddPeopleToTagProps, link: ApolloLink, ): RenderResult => { return render( - + @@ -91,7 +118,7 @@ describe('Organisation Tags Page', () => { ...jest.requireActual('react-router-dom'), useParams: () => ({ orgId: 'orgId' }), })); - // cache.reset(); + cache.reset(); }); afterEach(() => { @@ -147,6 +174,72 @@ describe('Organisation Tags Page', () => { userEvent.click(screen.getAllByTestId('deselectMemberBtn')[0]); }); + test('searchs for tags where the firstName matches the provided firstName search input', async () => { + renderAddPeopleToTagModal(props, link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.firstName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.firstName); + fireEvent.change(input, { target: { value: 'usersToAssignTo' } }); + + // should render the two users from the mock data + // where firstName starts with "usersToAssignTo" + await waitFor(() => { + const members = screen.getAllByTestId('memberName'); + expect(members.length).toEqual(2); + }); + + await waitFor(() => { + expect(screen.getAllByTestId('memberName')[0]).toHaveTextContent( + 'usersToAssignTo user1', + ); + }); + + await waitFor(() => { + expect(screen.getAllByTestId('memberName')[1]).toHaveTextContent( + 'usersToAssignTo user2', + ); + }); + }); + + test('searchs for tags where the lastName matches the provided lastName search input', async () => { + renderAddPeopleToTagModal(props, link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.lastName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.lastName); + fireEvent.change(input, { target: { value: 'userToAssignTo' } }); + + // should render the two users from the mock data + // where lastName starts with "usersToAssignTo" + await waitFor(() => { + const members = screen.getAllByTestId('memberName'); + expect(members.length).toEqual(2); + }); + + await waitFor(() => { + expect(screen.getAllByTestId('memberName')[0]).toHaveTextContent( + 'first userToAssignTo', + ); + }); + + await waitFor(() => { + expect(screen.getAllByTestId('memberName')[1]).toHaveTextContent( + 'second userToAssignTo', + ); + }); + }); + test('Renders more members with infinite scroll', async () => { const { getByText } = renderAddPeopleToTagModal(props, link); @@ -157,13 +250,15 @@ describe('Organisation Tags Page', () => { }); // Find the infinite scroll div by test ID or another selector - const scrollableDiv = screen.getByTestId('scrollableDiv'); + const addPeopleToTagScrollableDiv = screen.getByTestId( + 'addPeopleToTagScrollableDiv', + ); const initialMemberDataLength = screen.getAllByTestId('memberName').length; // Set scroll position to the bottom - fireEvent.scroll(scrollableDiv, { - target: { scrollY: scrollableDiv.scrollHeight }, + fireEvent.scroll(addPeopleToTagScrollableDiv, { + target: { scrollY: addPeopleToTagScrollableDiv.scrollHeight }, }); await waitFor(() => { @@ -174,11 +269,27 @@ describe('Organisation Tags Page', () => { }); }); + test('Toasts error when no one is selected while assigning', async () => { + renderAddPeopleToTagModal(props, link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('assignPeopleBtn')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('assignPeopleBtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith(translations.noOneSelected); + }); + }); + test('Assigns tag to multiple people', async () => { renderAddPeopleToTagModal(props, link); await wait(); + // select members and assign them await waitFor(() => { expect(screen.getAllByTestId('selectMemberBtn')[0]).toBeInTheDocument(); }); diff --git a/src/components/AddPeopleToTag/AddPeopleToTag.tsx b/src/components/AddPeopleToTag/AddPeopleToTag.tsx index f5c90be096..51d21a6a1c 100644 --- a/src/components/AddPeopleToTag/AddPeopleToTag.tsx +++ b/src/components/AddPeopleToTag/AddPeopleToTag.tsx @@ -1,10 +1,9 @@ import { useMutation, useQuery } from '@apollo/client'; import type { GridCellParams, GridColDef } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; -import Loader from 'components/Loader/Loader'; import { USER_TAGS_MEMBERS_TO_ASSIGN_TO } from 'GraphQl/Queries/userTagQueries'; import type { ChangeEvent } from 'react'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Modal, Form, Button } from 'react-bootstrap'; import { useParams } from 'react-router-dom'; import type { InterfaceQueryUserTagsMembersToAssignTo } from 'utils/interfaces'; @@ -55,10 +54,16 @@ const AddPeopleToTag: React.FC = ({ [], ); + const [memberToAssignToSearchFirstName, setMemberToAssignToSearchFirstName] = + useState(''); + const [memberToAssignToSearchLastName, setMemberToAssignToSearchLastName] = + useState(''); + const { data: userTagsMembersToAssignToData, loading: userTagsMembersToAssignToLoading, error: userTagsMembersToAssignToError, + refetch: userTagsMembersToAssignToRefetch, fetchMore: fetchMoreMembersToAssignTo, }: InterfaceTagUsersToAssignToQuery = useQuery( USER_TAGS_MEMBERS_TO_ASSIGN_TO, @@ -66,12 +71,21 @@ const AddPeopleToTag: React.FC = ({ variables: { id: currentTagId, first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: memberToAssignToSearchFirstName }, + lastName: { starts_with: memberToAssignToSearchLastName }, + }, }, skip: !addPeopleToTagModalIsOpen, - fetchPolicy: 'no-cache', }, ); + useEffect(() => { + setMemberToAssignToSearchFirstName(''); + setMemberToAssignToSearchLastName(''); + userTagsMembersToAssignToRefetch(); + }, [addPeopleToTagModalIsOpen]); + const loadMoreMembersToAssignTo = (): void => { fetchMoreMembersToAssignTo({ variables: { @@ -92,7 +106,7 @@ const AddPeopleToTag: React.FC = ({ }; }, ) => { - if (!fetchMoreResult) return prevResult; + if (!fetchMoreResult) /* istanbul ignore next */ return prevResult; return { getUsersToAssignTo: { @@ -140,6 +154,11 @@ const AddPeopleToTag: React.FC = ({ ): Promise => { e.preventDefault(); + if (!assignToMembers.length) { + toast.error(t('noOneSelected')); + return; + } + try { const { data } = await addPeople({ variables: { @@ -252,39 +271,72 @@ const AddPeopleToTag: React.FC = ({
+
+ {assignToMembers.length === 0 ? ( +
+ {t('noOneSelected')} +
+ ) : ( + assignToMembers.map((member) => ( +
+ {member.firstName} {member.lastName} + removeMember(member._id)} + data-testid="clearSelectedMember" + /> +
+ )) + )} +
+ +
+
+ + + setMemberToAssignToSearchFirstName(e.target.value.trim()) + } + data-testid="searchByFirstName" + autoComplete="off" + /> +
+
+ + + setMemberToAssignToSearchLastName(e.target.value.trim()) + } + data-testid="searchByLastName" + autoComplete="off" + /> +
+
+ {userTagsMembersToAssignToLoading ? ( - +
+ +
) : ( <>
- {assignToMembers.length === 0 ? ( -
- {t('noOneSelected')} -
- ) : ( - assignToMembers.map((member) => ( -
- {member.firstName} {member.lastName} - removeMember(member._id)} - data-testid="clearSelectedMember" - /> -
- )) - )} -
- -
@@ -297,7 +349,7 @@ const AddPeopleToTag: React.FC = ({ /* istanbul ignore next */ false } loader={} - scrollableTarget="scrollableDiv" + scrollableTarget="addPeopleToTagScrollableDiv" > = ({ alignItems="center" justifyContent="center" > - {t('assignedToAll')} + {t('noMoreMembersFound')} ), }} diff --git a/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts b/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts index 223fcd3064..fbaf812186 100644 --- a/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts +++ b/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts @@ -9,6 +9,10 @@ export const MOCKS = [ variables: { id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: '' }, + lastName: { starts_with: '' }, + }, }, }, result: { @@ -117,6 +121,10 @@ export const MOCKS = [ id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, after: '10', + where: { + firstName: { starts_with: '' }, + lastName: { starts_with: '' }, + }, }, }, result: { @@ -154,6 +162,100 @@ export const MOCKS = [ }, }, }, + { + request: { + query: USER_TAGS_MEMBERS_TO_ASSIGN_TO, + variables: { + id: '1', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: 'usersToAssignTo' }, + lastName: { starts_with: '' }, + }, + }, + }, + result: { + data: { + getUsersToAssignTo: { + name: 'tag1', + usersToAssignTo: { + edges: [ + { + node: { + _id: '1', + firstName: 'usersToAssignTo', + lastName: 'user1', + }, + cursor: '1', + }, + { + node: { + _id: '2', + firstName: 'usersToAssignTo', + lastName: 'user2', + }, + cursor: '2', + }, + ], + pageInfo: { + startCursor: '1', + endCursor: '2', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + }, + }, + }, + }, + { + request: { + query: USER_TAGS_MEMBERS_TO_ASSIGN_TO, + variables: { + id: '1', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: '' }, + lastName: { starts_with: 'userToAssignTo' }, + }, + }, + }, + result: { + data: { + getUsersToAssignTo: { + name: 'tag1', + usersToAssignTo: { + edges: [ + { + node: { + _id: '1', + firstName: 'first', + lastName: 'userToAssignTo', + }, + cursor: '1', + }, + { + node: { + _id: '2', + firstName: 'second', + lastName: 'userToAssignTo', + }, + cursor: '2', + }, + ], + pageInfo: { + startCursor: '1', + endCursor: '2', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + }, + }, + }, + }, { request: { query: ADD_PEOPLE_TO_TAG, @@ -179,6 +281,10 @@ export const MOCKS_ERROR = [ variables: { id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: '' }, + lastName: { starts_with: '' }, + }, }, }, error: new Error('Mock Graphql Error'), diff --git a/src/components/TagActions/TagActions.module.css b/src/components/TagActions/TagActions.module.css index 62c5855981..079dffea65 100644 --- a/src/components/TagActions/TagActions.module.css +++ b/src/components/TagActions/TagActions.module.css @@ -17,9 +17,8 @@ } .scrollContainer { - max-height: 100px; + height: 100px; overflow-y: auto; - margin-bottom: 1rem; } .tagBadge { @@ -34,3 +33,10 @@ .removeFilterIcon { cursor: pointer; } + +.loadingDiv { + min-height: 300px; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/components/TagActions/TagActions.test.tsx b/src/components/TagActions/TagActions.test.tsx index 140504d044..d27f177ebe 100644 --- a/src/components/TagActions/TagActions.test.tsx +++ b/src/components/TagActions/TagActions.test.tsx @@ -199,6 +199,27 @@ describe('Organisation Tags Page', () => { }); }); + test('searchs for tags where the name matches the provided search input', async () => { + renderTagActionsModal(props[0], link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.searchByName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.searchByName); + fireEvent.change(input, { target: { value: 'searchUserTag' } }); + + // should render the two searched tags from the mock data + // where name starts with "searchUserTag" + await waitFor(() => { + const tags = screen.getAllByTestId('orgUserTag'); + expect(tags.length).toEqual(2); + }); + }); + test('Renders more members with infinite scroll', async () => { const { getByText } = renderTagActionsModal(props[0], link); @@ -317,6 +338,21 @@ describe('Organisation Tags Page', () => { userEvent.click(screen.getByTestId('expandSubTags1')); }); + test('Toasts error when no tag is selected while assigning', async () => { + renderTagActionsModal(props[0], link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('tagActionSubmitBtn')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('tagActionSubmitBtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith(translations.noTagSelected); + }); + }); + test('Successfully assigns to tags', async () => { renderTagActionsModal(props[0], link); diff --git a/src/components/TagActions/TagActions.tsx b/src/components/TagActions/TagActions.tsx index 60099e855d..083341372f 100644 --- a/src/components/TagActions/TagActions.tsx +++ b/src/components/TagActions/TagActions.tsx @@ -1,6 +1,4 @@ import { useMutation, useQuery } from '@apollo/client'; -import Loader from 'components/Loader/Loader'; -import { USER_TAG_ANCESTORS } from 'GraphQl/Queries/userTagQueries'; import type { FormEvent } from 'react'; import React, { useEffect, useState } from 'react'; import { Modal, Form, Button } from 'react-bootstrap'; @@ -52,6 +50,8 @@ const TagActions: React.FC = ({ }) => { const { orgId, tagId: currentTagId } = useParams(); + const [tagSearchName, setTagSearchName] = useState(''); + const { data: orgUserTagsData, loading: orgUserTagsLoading, @@ -61,6 +61,7 @@ const TagActions: React.FC = ({ variables: { id: orgId, first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: tagSearchName } }, }, skip: !tagActionsModalIsOpen, }); @@ -81,7 +82,7 @@ const TagActions: React.FC = ({ }; }, ) => { - if (!fetchMoreResult) return prevResult; + if (!fetchMoreResult) /* istanbul ignore next */ return prevResult; return { organizations: [ @@ -107,9 +108,6 @@ const TagActions: React.FC = ({ (edge) => edge.node, ) ?? /* istanbul ignore next */ []; - const [checkedTagId, setCheckedTagId] = useState(null); - const [uncheckedTagId, setUncheckedTagId] = useState(null); - // tags that we have selected to assigned const [selectedTags, setSelectedTags] = useState([]); @@ -167,22 +165,13 @@ const TagActions: React.FC = ({ setAncestorTagsDataMap(newAncestorTagsDataMap); }, [removeAncestorTagsData]); - const addAncestorTags = (tagId: string): void => { - setCheckedTagId(tagId); - setUncheckedTagId(null); - }; - - const removeAncestorTags = (tagId: string): void => { - setUncheckedTagId(tagId); - setCheckedTagId(null); - }; - const selectTag = (tag: InterfaceTagData): void => { const newCheckedTags = new Set(checkedTags); setSelectedTags((selectedTags) => [...selectedTags, tag]); newCheckedTags.add(tag._id); - addAncestorTags(tag._id); + + setAddAncestorTagsData(new Set(tag.ancestorTags)); setCheckedTags(newCheckedTags); }; @@ -199,7 +188,8 @@ const TagActions: React.FC = ({ selectedTags.filter((selectedTag) => selectedTag._id !== tag._id), ); newCheckedTags.delete(tag._id); - removeAncestorTags(tag._id); + + setRemoveAncestorTagsData(new Set(tag.ancestorTags)); setCheckedTags(newCheckedTags); }; @@ -215,20 +205,6 @@ const TagActions: React.FC = ({ } }; - useQuery(USER_TAG_ANCESTORS, { - variables: { id: checkedTagId }, - onCompleted: /* istanbul ignore next */ (data) => { - setAddAncestorTagsData(data.getUserTagAncestors.slice(0, -1)); // Update the ancestor tags data, to check the ancestor tags - }, - }); - - useQuery(USER_TAG_ANCESTORS, { - variables: { id: uncheckedTagId }, - onCompleted: /* istanbul ignore next */ (data) => { - setRemoveAncestorTagsData(data.getUserTagAncestors.slice(0, -1)); // Update the ancestor tags data, to uncheck the ancestor tags - }, - }); - const [assignToTags] = useMutation(ASSIGN_TO_TAGS); const [removeFromTags] = useMutation(REMOVE_FROM_TAGS); @@ -237,6 +213,11 @@ const TagActions: React.FC = ({ ): Promise => { e.preventDefault(); + if (!selectedTags.length) { + toast.error(t('noTagSelected')); + return; + } + const mutationObject = { variables: { currentTagId, @@ -301,44 +282,58 @@ const TagActions: React.FC = ({ +
+ {selectedTags.length === 0 ? ( +
+ {t('noTagSelected')} +
+ ) : ( + selectedTags.map((tag: InterfaceTagData) => ( +
+ {tag.name} +
+ )) + )} +
+ +
+ + setTagSearchName(e.target.value.trim())} + data-testid="searchByName" + autoComplete="off" + /> +
+ +
+ {t('allTags')} +
{orgUserTagsLoading ? ( - +
+ +
) : ( <> -
- {selectedTags.length === 0 ? ( -
- {t('noTagSelected')} -
- ) : ( - selectedTags.map((tag: InterfaceTagData) => ( -
- {tag.name} -
- )) - )} -
- -
- {t('allTags')} -
-
@@ -353,17 +348,36 @@ const TagActions: React.FC = ({ scrollableTarget="scrollableDiv" > {userTagsList?.map((tag) => ( -
- +
+
+ +
+ + {/* Ancestor tags breadcrumbs positioned at the end of TagNode */} + {tag.parentTag && ( +
+ <>{'('} + {tag.ancestorTags?.map((ancestorTag) => ( + + {ancestorTag.name} + + + ))} + <>{')'} +
+ )}
))} diff --git a/src/components/TagActions/TagActionsMocks.ts b/src/components/TagActions/TagActionsMocks.ts index a6ed89ab8b..f22458fa53 100644 --- a/src/components/TagActions/TagActionsMocks.ts +++ b/src/components/TagActions/TagActionsMocks.ts @@ -13,6 +13,7 @@ export const MOCKS = [ variables: { id: '123', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, }, }, result: { @@ -25,12 +26,14 @@ export const MOCKS = [ node: { _id: '1', name: 'userTag 1', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 11, }, + ancestorTags: [], }, cursor: '1', }, @@ -38,12 +41,14 @@ export const MOCKS = [ node: { _id: '2', name: 'userTag 2', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 0, }, + ancestorTags: [], }, cursor: '2', }, @@ -51,12 +56,14 @@ export const MOCKS = [ node: { _id: '3', name: 'userTag 3', + parentTag: null, usersAssignedTo: { totalCount: 0, }, childTags: { totalCount: 5, }, + ancestorTags: [], }, cursor: '3', }, @@ -64,12 +71,14 @@ export const MOCKS = [ node: { _id: '4', name: 'userTag 4', + parentTag: null, usersAssignedTo: { totalCount: 0, }, childTags: { totalCount: 0, }, + ancestorTags: [], }, cursor: '4', }, @@ -77,12 +86,14 @@ export const MOCKS = [ node: { _id: '5', name: 'userTag 5', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 5, }, + ancestorTags: [], }, cursor: '5', }, @@ -90,12 +101,14 @@ export const MOCKS = [ node: { _id: '6', name: 'userTag 6', + parentTag: null, usersAssignedTo: { totalCount: 6, }, childTags: { totalCount: 6, }, + ancestorTags: [], }, cursor: '6', }, @@ -103,12 +116,14 @@ export const MOCKS = [ node: { _id: '7', name: 'userTag 7', + parentTag: null, usersAssignedTo: { totalCount: 7, }, childTags: { totalCount: 7, }, + ancestorTags: [], }, cursor: '7', }, @@ -116,12 +131,14 @@ export const MOCKS = [ node: { _id: '8', name: 'userTag 8', + parentTag: null, usersAssignedTo: { totalCount: 8, }, childTags: { totalCount: 8, }, + ancestorTags: [], }, cursor: '8', }, @@ -129,12 +146,14 @@ export const MOCKS = [ node: { _id: '9', name: 'userTag 9', + parentTag: null, usersAssignedTo: { totalCount: 9, }, childTags: { totalCount: 9, }, + ancestorTags: [], }, cursor: '9', }, @@ -142,12 +161,14 @@ export const MOCKS = [ node: { _id: '10', name: 'userTag 10', + parentTag: null, usersAssignedTo: { totalCount: 10, }, childTags: { totalCount: 10, }, + ancestorTags: [], }, cursor: '10', }, @@ -172,6 +193,7 @@ export const MOCKS = [ id: '123', first: TAGS_QUERY_DATA_CHUNK_SIZE, after: '10', + where: { name: { starts_with: '' } }, }, }, result: { @@ -184,12 +206,14 @@ export const MOCKS = [ node: { _id: '11', name: 'userTag 11', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 5, }, + ancestorTags: [], }, cursor: '11', }, @@ -197,12 +221,14 @@ export const MOCKS = [ node: { _id: '12', name: 'userTag 12', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 0, }, + ancestorTags: [], }, cursor: '12', }, @@ -220,6 +246,79 @@ export const MOCKS = [ }, }, }, + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: 'searchUserTag' } }, + }, + }, + result: { + data: { + organizations: [ + { + userTags: { + edges: [ + { + node: { + _id: '1', + name: 'searchUserTag 1', + parentTag: { + _id: '1', + }, + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: '1', + }, + { + node: { + _id: '2', + name: 'searchUserTag 2', + parentTag: { + _id: '1', + }, + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: '2', + }, + ], + pageInfo: { + startCursor: '1', + endCursor: '2', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + }, + ], + }, + }, + }, { request: { query: USER_TAG_SUB_TAGS, @@ -244,6 +343,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag1', }, @@ -257,6 +362,12 @@ export const MOCKS = [ childTags: { totalCount: 0, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag2', }, @@ -270,6 +381,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag3', }, @@ -283,6 +400,12 @@ export const MOCKS = [ childTags: { totalCount: 0, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag4', }, @@ -296,6 +419,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag5', }, @@ -309,6 +438,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag6', }, @@ -322,6 +457,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag7', }, @@ -335,6 +476,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag8', }, @@ -348,6 +495,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag9', }, @@ -361,6 +514,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag10', }, @@ -373,6 +532,7 @@ export const MOCKS = [ }, totalCount: 11, }, + ancestorTags: [], }, }, }, @@ -402,6 +562,12 @@ export const MOCKS = [ childTags: { totalCount: 0, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag11', }, @@ -414,6 +580,7 @@ export const MOCKS = [ }, totalCount: 11, }, + ancestorTags: [], }, }, }, @@ -459,6 +626,7 @@ export const MOCKS_ERROR_ORGANIZATION_TAGS_QUERY = [ variables: { id: '123', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, }, }, error: new Error('Mock Graphql Error for organization root tags query'), @@ -472,6 +640,7 @@ export const MOCKS_ERROR_SUBTAGS_QUERY = [ variables: { id: '123', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, }, }, result: { @@ -484,12 +653,14 @@ export const MOCKS_ERROR_SUBTAGS_QUERY = [ node: { _id: '1', name: 'userTag 1', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 11, }, + ancestorTags: [], }, cursor: '1', }, @@ -497,12 +668,14 @@ export const MOCKS_ERROR_SUBTAGS_QUERY = [ node: { _id: '2', name: 'userTag 2', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 0, }, + ancestorTags: [], }, cursor: '2', }, diff --git a/src/components/TagActions/TagNode.tsx b/src/components/TagActions/TagNode.tsx index e05d572f30..4a085ecaf9 100644 --- a/src/components/TagActions/TagNode.tsx +++ b/src/components/TagActions/TagNode.tsx @@ -61,7 +61,7 @@ const TagNode: React.FC = ({ fetchMoreResult?: { getChildTags: InterfaceQueryUserTagChildTags }; }, ) => { - if (!fetchMoreResult) return prevResult; + if (!fetchMoreResult) /* istanbul ignore next */ return prevResult; return { getChildTags: { diff --git a/src/screens/ManageTag/ManageTag.test.tsx b/src/screens/ManageTag/ManageTag.test.tsx index c4c1375e0e..598a15cc9a 100644 --- a/src/screens/ManageTag/ManageTag.test.tsx +++ b/src/screens/ManageTag/ManageTag.test.tsx @@ -20,11 +20,7 @@ import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18n from 'utils/i18nForTest'; import ManageTag from './ManageTag'; -import { - MOCKS, - MOCKS_ERROR_ASSIGNED_MEMBERS, - MOCKS_ERROR_TAG_ANCESTORS, -} from './ManageTagMocks'; +import { MOCKS, MOCKS_ERROR_ASSIGNED_MEMBERS } from './ManageTagMocks'; import { type ApolloLink } from '@apollo/client'; const translations = { @@ -37,7 +33,6 @@ const translations = { const link = new StaticMockLink(MOCKS, true); const link2 = new StaticMockLink(MOCKS_ERROR_ASSIGNED_MEMBERS, true); -const link3 = new StaticMockLink(MOCKS_ERROR_TAG_ANCESTORS, true); async function wait(ms = 500): Promise { await act(() => { @@ -129,16 +124,6 @@ describe('Manage Tag Page', () => { }); }); - test('renders error component on unsuccessful userTag ancestors query', async () => { - const { queryByText } = renderManageTag(link3); - - await wait(); - - await waitFor(() => { - expect(queryByText(translations.addPeopleToTag)).not.toBeInTheDocument(); - }); - }); - test('opens and closes the add people to tag modal', async () => { renderManageTag(link); @@ -339,6 +324,84 @@ describe('Manage Tag Page', () => { }); }); + test('searchs for tags where the name matches the provided search input', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.searchByName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.searchByName); + fireEvent.change(input, { target: { value: 'assigned user' } }); + + // should render the two users from the mock data + // where firstName starts with "assigned" and lastName starts with "user" + await waitFor(() => { + const buttons = screen.getAllByTestId('viewProfileBtn'); + expect(buttons.length).toEqual(2); + }); + }); + + test('fetches the tags by the sort order, i.e. latest or oldest first', async () => { + renderManageTag(link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.searchByName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.searchByName); + fireEvent.change(input, { target: { value: 'assigned user' } }); + + // should render the two searched tags from the mock data + // where name starts with "searchUserTag" + await waitFor(() => { + expect(screen.getAllByTestId('memberName')[0]).toHaveTextContent( + 'assigned user1', + ); + }); + + // now change the sorting order + await waitFor(() => { + expect(screen.getByTestId('sortPeople')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('sortPeople')); + + await waitFor(() => { + expect(screen.getByTestId('oldest')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('oldest')); + + // returns the tags in reverse order + await waitFor(() => { + expect(screen.getAllByTestId('memberName')[0]).toHaveTextContent( + 'assigned user2', + ); + }); + + await waitFor(() => { + expect(screen.getByTestId('sortPeople')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('sortPeople')); + + await waitFor(() => { + expect(screen.getByTestId('latest')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('latest')); + + // reverse the order again + await waitFor(() => { + expect(screen.getAllByTestId('memberName')[0]).toHaveTextContent( + 'assigned user1', + ); + }); + }); + test('Fetches more assigned members with infinite scroll', async () => { const { getByText } = renderManageTag(link); diff --git a/src/screens/ManageTag/ManageTag.tsx b/src/screens/ManageTag/ManageTag.tsx index f04965ba4d..e8eb9bb8df 100644 --- a/src/screens/ManageTag/ManageTag.tsx +++ b/src/screens/ManageTag/ManageTag.tsx @@ -1,7 +1,7 @@ import type { FormEvent } from 'react'; import React, { useEffect, useState } from 'react'; -import { useMutation, useQuery, type ApolloError } from '@apollo/client'; -import { Search, WarningAmberRounded } from '@mui/icons-material'; +import { useMutation, useQuery } from '@apollo/client'; +import { WarningAmberRounded } from '@mui/icons-material'; import SortIcon from '@mui/icons-material/Sort'; import Loader from 'components/Loader/Loader'; import IconComponent from 'components/IconComponent/IconComponent'; @@ -17,6 +17,7 @@ import styles from './ManageTag.module.css'; import { DataGrid } from '@mui/x-data-grid'; import type { InterfaceTagAssignedMembersQuery, + SortedByType, TagActionType, } from 'utils/organizationTagsUtils'; import { @@ -30,10 +31,7 @@ import { UNASSIGN_USER_TAG, UPDATE_USER_TAG, } from 'GraphQl/Mutations/TagMutations'; -import { - USER_TAG_ANCESTORS, - USER_TAGS_ASSIGNED_MEMBERS, -} from 'GraphQl/Queries/userTagQueries'; +import { USER_TAGS_ASSIGNED_MEMBERS } from 'GraphQl/Queries/userTagQueries'; import AddPeopleToTag from 'components/AddPeopleToTag/AddPeopleToTag'; import TagActions from 'components/TagActions/TagActions'; import InfiniteScroll from 'react-infinite-scroll-component'; @@ -43,10 +41,7 @@ import RemoveUserTagModal from './RemoveUserTagModal'; import UnassignUserTagModal from './UnassignUserTagModal'; /** - * Component that renders the Manage Tag screen when the app navigates to '/orgtags/:orgId/managetag/:tagId'. - * - * This component does not accept any props and is responsible for displaying - * the content associated with the corresponding route. + * Component that renders the Manage Tag screen when the app navigates to '/orgtags/:orgId/manageTag/:tagId'. */ function ManageTag(): JSX.Element { @@ -54,6 +49,8 @@ function ManageTag(): JSX.Element { keyPrefix: 'manageTag', }); const { t: tCommon } = useTranslation('common'); + const { orgId, tagId: currentTagId } = useParams(); + const navigate = useNavigate(); const [unassignUserTagModalIsOpen, setUnassignUserTagModalIsOpen] = useState(false); @@ -63,11 +60,15 @@ function ManageTag(): JSX.Element { const [editUserTagModalIsOpen, setEditUserTagModalIsOpen] = useState(false); const [removeUserTagModalIsOpen, setRemoveUserTagModalIsOpen] = useState(false); - - const { orgId, tagId: currentTagId } = useParams(); - const navigate = useNavigate(); const [unassignUserId, setUnassignUserId] = useState(null); - + const [assignedMemberSearchInput, setAssignedMemberSearchInput] = + useState(''); + const [assignedMemberSearchFirstName, setAssignedMemberSearchFirstName] = + useState(''); + const [assignedMemberSearchLastName, setAssignedMemberSearchLastName] = + useState(''); + const [assignedMemberSortOrder, setAssignedMemberSortOrder] = + useState('DESCENDING'); // a state to specify whether we're assigning to tags or removing from tags const [tagActionType, setTagActionType] = useState('assignToTags'); @@ -75,21 +76,18 @@ function ManageTag(): JSX.Element { const toggleRemoveUserTagModal = (): void => { setRemoveUserTagModalIsOpen(!removeUserTagModalIsOpen); }; - const showAddPeopleToTagModal = (): void => { setAddPeopleToTagModalIsOpen(true); }; const hideAddPeopleToTagModal = (): void => { setAddPeopleToTagModalIsOpen(false); }; - const showTagActionsModal = (): void => { setTagActionsModalIsOpen(true); }; const hideTagActionsModal = (): void => { setTagActionsModalIsOpen(false); }; - const showEditUserTagModal = (): void => { setEditUserTagModalIsOpen(true); }; @@ -107,7 +105,13 @@ function ManageTag(): JSX.Element { variables: { id: currentTagId, first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: assignedMemberSearchFirstName }, + lastName: { starts_with: assignedMemberSearchLastName }, + }, + sortedBy: { id: assignedMemberSortOrder }, }, + fetchPolicy: 'no-cache', }); const loadMoreAssignedMembers = (): void => { @@ -128,7 +132,7 @@ function ManageTag(): JSX.Element { }; }, ) => { - if (!fetchMoreResult) return prevResult; + if (!fetchMoreResult) /* istanbul ignore next */ return prevResult; return { getAssignedUsers: { @@ -146,26 +150,14 @@ function ManageTag(): JSX.Element { }); }; - const { - data: orgUserTagAncestorsData, - loading: orgUserTagsAncestorsLoading, - refetch: orgUserTagsAncestorsRefetch, - error: orgUserTagsAncestorsError, - }: { - data?: { - getUserTagAncestors: { - _id: string; - name: string; - }[]; - }; - loading: boolean; - error?: ApolloError; - refetch: () => void; - } = useQuery(USER_TAG_ANCESTORS, { - variables: { - id: currentTagId, - }, - }); + useEffect(() => { + const [firstName, ...lastNameParts] = assignedMemberSearchInput + .trim() + .split(/\s+/); + const lastName = lastNameParts.join(' '); // Joins everything after the first word + setAssignedMemberSearchFirstName(firstName); + setAssignedMemberSearchLastName(lastName); + }, [assignedMemberSearchInput]); const [unassignUserTag] = useMutation(UNASSIGN_USER_TAG); @@ -220,7 +212,6 @@ function ManageTag(): JSX.Element { if (data) { toast.success(t('tagUpdationSuccess')); userTagAssignedMembersRefetch(); - orgUserTagsAncestorsRefetch(); setEditUserTagModalIsOpen(false); } } catch (error: unknown) { @@ -251,22 +242,13 @@ function ManageTag(): JSX.Element { } }; - if (userTagAssignedMembersLoading || orgUserTagsAncestorsLoading) { - return ; - } - - if (userTagAssignedMembersError || orgUserTagsAncestorsError) { + if (userTagAssignedMembersError) { return (
- Error occured while loading{' '} - {userTagAssignedMembersError ? 'assigned users' : 'tag ancestors'} -
- {userTagAssignedMembersError - ? userTagAssignedMembersError.message - : orgUserTagsAncestorsError?.message} + Error occured while loading assigned users
@@ -277,7 +259,16 @@ function ManageTag(): JSX.Element { userTagAssignedMembersData?.getAssignedUsers.usersAssignedTo.edges.map( (edge) => edge.node, ) ?? /* istanbul ignore next */ []; - const orgUserTagAncestors = orgUserTagAncestorsData?.getUserTagAncestors; + + // get the ancestorTags array and push the current tag in it + // used for the tag breadcrumbs + const orgUserTagAncestors = [ + ...(userTagAssignedMembersData?.getAssignedUsers.ancestorTags ?? []), + { + _id: currentTagId, + name: currentTagName, + }, + ]; const redirectToSubTags = (tagId: string): void => { navigate(`/orgtags/${orgId}/subTags/${tagId}`); @@ -285,7 +276,6 @@ function ManageTag(): JSX.Element { const redirectToManageTag = (tagId: string): void => { navigate(`/orgtags/${orgId}/manageTag/${tagId}`); }; - const toggleUnassignUserTagModal = (): void => { if (unassignUserTagModalIsOpen) { setUnassignUserId(null); @@ -366,21 +356,18 @@ function ManageTag(): JSX.Element {
+ + setAssignedMemberSearchInput(e.target.value.trim()) + } data-testid="searchByName" autoComplete="off" - required /> -
- {tCommon('sort')} + {assignedMemberSortOrder === 'DESCENDING' + ? tCommon('Latest') + : tCommon('Oldest')} - + setAssignedMemberSortOrder('DESCENDING')} + > {tCommon('Latest')} - + setAssignedMemberSortOrder('ASCENDING')} + > {tCommon('Oldest')} @@ -423,131 +418,137 @@ function ManageTag(): JSX.Element { {t('addPeopleToTag')}
- - -
-
- + + {userTagAssignedMembersLoading ? ( + + ) : ( + + +
+
+ +
+
navigate(`/orgtags/${orgId}`)} + className={`fs-6 ms-3 my-1 ${styles.tagsBreadCrumbs}`} + data-testid="allTagsBtn" + > + {'Tags'} + +
+ {orgUserTagAncestors?.map((tag, index) => ( +
redirectToManageTag(tag._id as string)} + data-testid="redirectToManageTag" + > + {tag.name} + {orgUserTagAncestors.length - 1 !== index && ( + /* istanbul ignore next */ + + )} +
+ ))}
navigate(`/orgtags/${orgId}`)} - className={`fs-6 ms-3 my-1 ${styles.tagsBreadCrumbs}`} - data-testid="allTagsBtn" + id="manageTagScrollableDiv" + data-testid="manageTagScrollableDiv" + className={styles.manageTagScrollableDiv} > - {'Tags'} - + } + scrollableTarget="manageTagScrollableDiv" + > + row.id} + slots={{ + noRowsOverlay: /* istanbul ignore next */ () => ( + + {t('noAssignedMembersFound')} + + ), + }} + sx={dataGridStyle} + getRowClassName={() => `${styles.rowBackground}`} + autoHeight + rowHeight={65} + rows={userTagAssignedMembers?.map( + (assignedMembers, index) => ({ + id: index + 1, + ...assignedMembers, + }), + )} + columns={columns} + isRowSelectable={() => false} + /> + +
+ + +
+
{'Actions'}
- {orgUserTagAncestors?.map((tag, index) => ( +
redirectToManageTag(tag._id as string)} - data-testid="redirectToManageTag" + onClick={() => { + setTagActionType('assignToTags'); + showTagActionsModal(); + }} + className="my-2 btn btn-primary btn-sm w-75" + data-testid="assignToTags" > - {tag.name} - {orgUserTagAncestors.length - 1 !== index && ( - /* istanbul ignore next */ - - )} + {t('assignToTags')}
- ))} -
-
- } - scrollableTarget="manageTagScrollableDiv" - > - row.id} - slots={{ - noRowsOverlay: /* istanbul ignore next */ () => ( - - {t('noAssignedMembersFound')} - - ), +
{ + setTagActionType('removeFromTags'); + showTagActionsModal(); + }} + className="mb-1 btn btn-danger btn-sm w-75" + data-testid="removeFromTags" + > + {t('removeFromTags')} +
+
`${styles.rowBackground}`} - autoHeight - rowHeight={65} - rows={userTagAssignedMembers?.map( - (assignedMembers, index) => ({ - id: index + 1, - ...assignedMembers, - }), - )} - columns={columns} - isRowSelectable={() => false} /> -
-
- - -
-
{'Actions'}
-
-
-
{ - setTagActionType('assignToTags'); - showTagActionsModal(); - }} - className="my-2 btn btn-primary btn-sm w-75" - data-testid="assignToTags" - > - {t('assignToTags')} -
-
{ - setTagActionType('removeFromTags'); - showTagActionsModal(); - }} - className="mb-1 btn btn-danger btn-sm w-75" - data-testid="removeFromTags" - > - {t('removeFromTags')} -
-
-
- {tCommon('edit')} -
-
- {tCommon('remove')} +
+ {tCommon('edit')} +
+
+ {tCommon('remove')} +
-
- -
+ + + )}
diff --git a/src/screens/ManageTag/ManageTagMocks.ts b/src/screens/ManageTag/ManageTagMocks.ts index 4e9c64badd..5ce1e62595 100644 --- a/src/screens/ManageTag/ManageTagMocks.ts +++ b/src/screens/ManageTag/ManageTagMocks.ts @@ -3,10 +3,7 @@ import { UNASSIGN_USER_TAG, UPDATE_USER_TAG, } from 'GraphQl/Mutations/TagMutations'; -import { - USER_TAG_ANCESTORS, - USER_TAGS_ASSIGNED_MEMBERS, -} from 'GraphQl/Queries/userTagQueries'; +import { USER_TAGS_ASSIGNED_MEMBERS } from 'GraphQl/Queries/userTagQueries'; import { TAGS_QUERY_DATA_CHUNK_SIZE } from 'utils/organizationTagsUtils'; export const MOCKS = [ @@ -16,6 +13,11 @@ export const MOCKS = [ variables: { id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: '' }, + lastName: { starts_with: '' }, + }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { @@ -113,6 +115,7 @@ export const MOCKS = [ }, totalCount: 12, }, + ancestorTags: [], }, }, }, @@ -124,6 +127,11 @@ export const MOCKS = [ id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, after: '10', + where: { + firstName: { starts_with: '' }, + lastName: { starts_with: '' }, + }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { @@ -157,25 +165,106 @@ export const MOCKS = [ }, totalCount: 12, }, + ancestorTags: [], }, }, }, }, { request: { - query: USER_TAG_ANCESTORS, + query: USER_TAGS_ASSIGNED_MEMBERS, variables: { id: '1', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: 'assigned' }, + lastName: { starts_with: 'user' }, + }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { data: { - getUserTagAncestors: [ - { - _id: '1', - name: 'tag1', + getAssignedUsers: { + name: 'tag1', + usersAssignedTo: { + edges: [ + { + node: { + _id: '1', + firstName: 'assigned', + lastName: 'user1', + }, + cursor: '1', + }, + { + node: { + _id: '2', + firstName: 'assigned', + lastName: 'user2', + }, + cursor: '2', + }, + ], + pageInfo: { + startCursor: '1', + endCursor: '2', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, }, - ], + ancestorTags: [], + }, + }, + }, + }, + { + request: { + query: USER_TAGS_ASSIGNED_MEMBERS, + variables: { + id: '1', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { + firstName: { starts_with: 'assigned' }, + lastName: { starts_with: 'user' }, + }, + sortedBy: { id: 'ASCENDING' }, + }, + }, + result: { + data: { + getAssignedUsers: { + name: 'tag1', + usersAssignedTo: { + edges: [ + { + node: { + _id: '2', + firstName: 'assigned', + lastName: 'user2', + }, + cursor: '2', + }, + { + node: { + _id: '1', + firstName: 'assigned', + lastName: 'user1', + }, + cursor: '1', + }, + ], + pageInfo: { + startCursor: '2', + endCursor: '1', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + ancestorTags: [], + }, }, }, }, @@ -235,57 +324,11 @@ export const MOCKS_ERROR_ASSIGNED_MEMBERS = [ variables: { id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, - }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: USER_TAG_ANCESTORS, - variables: { - id: '1', - }, - }, - result: { - data: { - getUserTagAncestors: [], - }, - }, - }, -]; - -export const MOCKS_ERROR_TAG_ANCESTORS = [ - { - request: { - query: USER_TAGS_ASSIGNED_MEMBERS, - variables: { - id: '1', - first: TAGS_QUERY_DATA_CHUNK_SIZE, - }, - }, - result: { - data: { - getAssignedUsers: { - name: 'tag1', - usersAssignedTo: { - edges: [], - pageInfo: { - startCursor: '1', - endCursor: '1', - hasNextPage: false, - hasPreviousPage: false, - }, - totalCount: 1, - }, + where: { + firstName: { starts_with: '' }, + lastName: { starts_with: '' }, }, - }, - }, - }, - { - request: { - query: USER_TAG_ANCESTORS, - variables: { - id: '1', + sortedBy: { id: 'DESCENDING' }, }, }, error: new Error('Mock Graphql Error'), diff --git a/src/screens/OrganizationTags/OrganizationTags.test.tsx b/src/screens/OrganizationTags/OrganizationTags.test.tsx index 1ab94ce35c..0d426d20ac 100644 --- a/src/screens/OrganizationTags/OrganizationTags.test.tsx +++ b/src/screens/OrganizationTags/OrganizationTags.test.tsx @@ -88,7 +88,7 @@ describe('Organisation Tags Page', () => { cleanup(); }); - test('Component loads correctly', async () => { + test('component loads correctly', async () => { const { getByText } = renderOrganizationTags(link); await wait(); @@ -160,7 +160,85 @@ describe('Organisation Tags Page', () => { }); }); - test('Fetches more tags with infinite scroll', async () => { + test('searchs for tags where the name matches the provided search input', async () => { + renderOrganizationTags(link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.searchByName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.searchByName); + fireEvent.change(input, { target: { value: 'searchUserTag' } }); + + // should render the two searched tags from the mock data + // where name starts with "searchUserTag" + await waitFor(() => { + const buttons = screen.getAllByTestId('manageTagBtn'); + expect(buttons.length).toEqual(2); + }); + }); + + test('fetches the tags by the sort order, i.e. latest or oldest first', async () => { + renderOrganizationTags(link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.searchByName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.searchByName); + fireEvent.change(input, { target: { value: 'searchUserTag' } }); + + // should render the two searched tags from the mock data + // where name starts with "searchUserTag" + await waitFor(() => { + expect(screen.getAllByTestId('tagName')[0]).toHaveTextContent( + 'searchUserTag 1', + ); + }); + + // now change the sorting order + await waitFor(() => { + expect(screen.getByTestId('sortTags')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('sortTags')); + + await waitFor(() => { + expect(screen.getByTestId('oldest')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('oldest')); + + // returns the tags in reverse order + await waitFor(() => { + expect(screen.getAllByTestId('tagName')[0]).toHaveTextContent( + 'searchUserTag 2', + ); + }); + + await waitFor(() => { + expect(screen.getByTestId('sortTags')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('sortTags')); + + await waitFor(() => { + expect(screen.getByTestId('latest')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('latest')); + + // reverse the order again + await waitFor(() => { + expect(screen.getAllByTestId('tagName')[0]).toHaveTextContent( + 'searchUserTag 1', + ); + }); + }); + + test('fetches more tags with infinite scroll', async () => { const { getByText } = renderOrganizationTags(link); await wait(); @@ -199,6 +277,12 @@ describe('Organisation Tags Page', () => { }); userEvent.click(screen.getByTestId('createTagBtn')); + userEvent.click(screen.getByTestId('createTagSubmitBtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith(translations.enterTagName); + }); + userEvent.type( screen.getByPlaceholderText(translations.tagNamePlaceholder), 'userTag 12', diff --git a/src/screens/OrganizationTags/OrganizationTags.tsx b/src/screens/OrganizationTags/OrganizationTags.tsx index ce37528cc1..b59da67fd0 100644 --- a/src/screens/OrganizationTags/OrganizationTags.tsx +++ b/src/screens/OrganizationTags/OrganizationTags.tsx @@ -1,11 +1,11 @@ import { useMutation, useQuery } from '@apollo/client'; -import { Search, WarningAmberRounded } from '@mui/icons-material'; +import { WarningAmberRounded } from '@mui/icons-material'; import SortIcon from '@mui/icons-material/Sort'; import Loader from 'components/Loader/Loader'; import IconComponent from 'components/IconComponent/IconComponent'; import { useNavigate, useParams, Link } from 'react-router-dom'; import type { ChangeEvent } from 'react'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Form } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import Dropdown from 'react-bootstrap/Dropdown'; @@ -13,10 +13,16 @@ import Modal from 'react-bootstrap/Modal'; import Row from 'react-bootstrap/Row'; import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; -import type { InterfaceQueryOrganizationUserTags } from 'utils/interfaces'; +import type { + InterfaceQueryOrganizationUserTags, + InterfaceTagData, +} from 'utils/interfaces'; import styles from './OrganizationTags.module.css'; import { DataGrid } from '@mui/x-data-grid'; -import type { InterfaceOrganizationTagsQuery } from 'utils/organizationTagsUtils'; +import type { + InterfaceOrganizationTagsQuery, + SortedByType, +} from 'utils/organizationTagsUtils'; import { dataGridStyle, TAGS_QUERY_DATA_CHUNK_SIZE, @@ -43,6 +49,9 @@ function OrganizationTags(): JSX.Element { const [createTagModalIsOpen, setCreateTagModalIsOpen] = useState(false); + const [tagSearchName, setTagSearchName] = useState(''); + const [tagSortOrder, setTagSortOrder] = useState('DESCENDING'); + const { orgId } = useParams(); const navigate = useNavigate(); @@ -67,6 +76,8 @@ function OrganizationTags(): JSX.Element { variables: { id: orgId, first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: tagSearchName } }, + sortedBy: { id: tagSortOrder }, }, }); @@ -76,6 +87,7 @@ function OrganizationTags(): JSX.Element { first: TAGS_QUERY_DATA_CHUNK_SIZE, after: orgUserTagsData?.organizations?.[0]?.userTags?.pageInfo?.endCursor ?? + /* istanbul ignore next */ null, }, updateQuery: ( @@ -88,7 +100,7 @@ function OrganizationTags(): JSX.Element { }; }, ) => { - if (!fetchMoreResult) return prevResult; + if (!fetchMoreResult) /* istanbul ignore next */ return prevResult; return { organizations: [ @@ -109,12 +121,21 @@ function OrganizationTags(): JSX.Element { }); }; + useEffect(() => { + orgUserTagsRefetch(); + }, []); + const [create, { loading: createUserTagLoading }] = useMutation(CREATE_USER_TAG); const createTag = async (e: ChangeEvent): Promise => { e.preventDefault(); + if (!tagName.trim()) { + toast.error(t('enterTagName')); + return; + } + try { const { data } = await create({ variables: { @@ -124,7 +145,7 @@ function OrganizationTags(): JSX.Element { }); if (data) { - toast.success(t('tagCreationSuccess') as string); + toast.success(t('tagCreationSuccess')); orgUserTagsRefetch(); setTagName(''); setCreateTagModalIsOpen(false); @@ -137,10 +158,6 @@ function OrganizationTags(): JSX.Element { } }; - if (createUserTagLoading || orgUserTagsLoading) { - return ; - } - if (orgUserTagsError) { return (
@@ -188,16 +205,29 @@ function OrganizationTags(): JSX.Element { minWidth: 100, sortable: false, headerClassName: `${styles.tableHeader}`, - renderCell: (params: GridCellParams) => { + renderCell: (params: GridCellParams) => { return ( -
redirectToSubTags(params.row._id)} - > - {params.row.name} +
+ {params.row.parentTag && + params.row.ancestorTags?.map((tag) => ( +
+ {tag.name} + +
+ ))} - +
redirectToSubTags(params.row._id)} + > + {params.row.name} + +
); }, @@ -272,21 +302,16 @@ function OrganizationTags(): JSX.Element {
+ setTagSearchName(e.target.value.trim())} autoComplete="off" - required /> -
- {tCommon('sort')} + {tagSortOrder === 'DESCENDING' + ? tCommon('Latest') + : tCommon('Oldest')} - + setTagSortOrder('DESCENDING')} + > {tCommon('Latest')} - + setTagSortOrder('ASCENDING')} + > {tCommon('Oldest')} @@ -322,61 +355,66 @@ function OrganizationTags(): JSX.Element {
-
-
-
- -
+ {orgUserTagsLoading || createUserTagLoading ? ( + + ) : ( +
+
+
+ +
-
- {'Tags'} +
+ {'Tags'} +
-
-
- } - scrollableTarget="orgUserTagsScrollableDiv" + +
- row.id} - slots={{ - noRowsOverlay: /* istanbul ignore next */ () => ( - - {t('noTagsFound')} - - ), - }} - sx={dataGridStyle} - getRowClassName={() => `${styles.rowBackground}`} - autoHeight - rowHeight={65} - rows={userTagsList?.map((userTag, index) => ({ - id: index + 1, - ...userTag, - }))} - columns={columns} - isRowSelectable={() => false} - /> - + } + scrollableTarget="orgUserTagsScrollableDiv" + > + row.id} + slots={{ + noRowsOverlay: /* istanbul ignore next */ () => ( + + {t('noTagsFound')} + + ), + }} + sx={dataGridStyle} + getRowClassName={() => `${styles.rowBackground}`} + autoHeight + rowHeight={65} + rows={userTagsList?.map((userTag, index) => ({ + id: index + 1, + ...userTag, + }))} + columns={columns} + isRowSelectable={() => false} + /> + +
-
+ )}
diff --git a/src/screens/OrganizationTags/OrganizationTagsMocks.ts b/src/screens/OrganizationTags/OrganizationTagsMocks.ts index a8e186b393..0fe48ca97f 100644 --- a/src/screens/OrganizationTags/OrganizationTagsMocks.ts +++ b/src/screens/OrganizationTags/OrganizationTagsMocks.ts @@ -9,6 +9,8 @@ export const MOCKS = [ variables: { id: '123', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { @@ -21,12 +23,14 @@ export const MOCKS = [ node: { _id: '1', name: 'userTag 1', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 11, }, + ancestorTags: [], }, cursor: '1', }, @@ -34,12 +38,14 @@ export const MOCKS = [ node: { _id: '2', name: 'userTag 2', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 0, }, + ancestorTags: [], }, cursor: '2', }, @@ -47,12 +53,14 @@ export const MOCKS = [ node: { _id: '3', name: 'userTag 3', + parentTag: null, usersAssignedTo: { totalCount: 0, }, childTags: { totalCount: 5, }, + ancestorTags: [], }, cursor: '3', }, @@ -60,12 +68,14 @@ export const MOCKS = [ node: { _id: '4', name: 'userTag 4', + parentTag: null, usersAssignedTo: { totalCount: 0, }, childTags: { totalCount: 0, }, + ancestorTags: [], }, cursor: '4', }, @@ -73,12 +83,14 @@ export const MOCKS = [ node: { _id: '5', name: 'userTag 5', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 5, }, + ancestorTags: [], }, cursor: '5', }, @@ -86,12 +98,14 @@ export const MOCKS = [ node: { _id: '6', name: 'userTag 6', + parentTag: null, usersAssignedTo: { totalCount: 6, }, childTags: { totalCount: 6, }, + ancestorTags: [], }, cursor: '6', }, @@ -99,12 +113,14 @@ export const MOCKS = [ node: { _id: '7', name: 'userTag 7', + parentTag: null, usersAssignedTo: { totalCount: 7, }, childTags: { totalCount: 7, }, + ancestorTags: [], }, cursor: '7', }, @@ -112,12 +128,14 @@ export const MOCKS = [ node: { _id: '8', name: 'userTag 8', + parentTag: null, usersAssignedTo: { totalCount: 8, }, childTags: { totalCount: 8, }, + ancestorTags: [], }, cursor: '8', }, @@ -125,12 +143,14 @@ export const MOCKS = [ node: { _id: '9', name: 'userTag 9', + parentTag: null, usersAssignedTo: { totalCount: 9, }, childTags: { totalCount: 9, }, + ancestorTags: [], }, cursor: '9', }, @@ -138,12 +158,14 @@ export const MOCKS = [ node: { _id: '10', name: 'userTag 10', + parentTag: null, usersAssignedTo: { totalCount: 10, }, childTags: { totalCount: 10, }, + ancestorTags: [], }, cursor: '10', }, @@ -168,6 +190,8 @@ export const MOCKS = [ id: '123', first: TAGS_QUERY_DATA_CHUNK_SIZE, after: '10', + where: { name: { starts_with: '' } }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { @@ -180,12 +204,14 @@ export const MOCKS = [ node: { _id: '11', name: 'userTag 11', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 5, }, + ancestorTags: [], }, cursor: '11', }, @@ -193,12 +219,14 @@ export const MOCKS = [ node: { _id: '12', name: 'userTag 12', + parentTag: null, usersAssignedTo: { totalCount: 5, }, childTags: { totalCount: 0, }, + ancestorTags: [], }, cursor: '12', }, @@ -216,6 +244,154 @@ export const MOCKS = [ }, }, }, + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: 'searchUserTag' } }, + sortedBy: { id: 'DESCENDING' }, + }, + }, + result: { + data: { + organizations: [ + { + userTags: { + edges: [ + { + node: { + _id: 'searchUserTag1', + name: 'searchUserTag 1', + parentTag: { + _id: '1', + }, + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchUserTag1', + }, + { + node: { + _id: 'searchUserTag2', + name: 'searchUserTag 2', + parentTag: { + _id: '1', + }, + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchUserTag2', + }, + ], + pageInfo: { + startCursor: 'searchUserTag1', + endCursor: 'searchUserTag2', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + }, + ], + }, + }, + }, + { + request: { + query: ORGANIZATION_USER_TAGS_LIST, + variables: { + id: '123', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: 'searchUserTag' } }, + sortedBy: { id: 'ASCENDING' }, + }, + }, + result: { + data: { + organizations: [ + { + userTags: { + edges: [ + { + node: { + _id: 'searchUserTag2', + name: 'searchUserTag 2', + parentTag: { + _id: '1', + }, + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 5, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchUserTag2', + }, + { + node: { + _id: 'searchUserTag1', + name: 'searchUserTag 1', + parentTag: { + _id: '1', + }, + usersAssignedTo: { + totalCount: 5, + }, + childTags: { + totalCount: 0, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchUserTag1', + }, + ], + pageInfo: { + startCursor: 'searchUserTag2', + endCursor: 'searchUserTag1', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + }, + ], + }, + }, + }, { request: { query: CREATE_USER_TAG, @@ -241,6 +417,8 @@ export const MOCKS_ERROR = [ variables: { id: '123', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, + sortedBy: { id: 'DESCENDING' }, }, }, error: new Error('Mock Graphql Error'), diff --git a/src/screens/SubTags/SubTags.test.tsx b/src/screens/SubTags/SubTags.test.tsx index 9cd778b858..145d31109d 100644 --- a/src/screens/SubTags/SubTags.test.tsx +++ b/src/screens/SubTags/SubTags.test.tsx @@ -20,12 +20,8 @@ import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18n from 'utils/i18nForTest'; import SubTags from './SubTags'; -import { - MOCKS, - MOCKS_ERROR_SUB_TAGS, - MOCKS_ERROR_TAG_ANCESTORS, -} from './SubTagsMocks'; -import { type ApolloLink } from '@apollo/client'; +import { MOCKS, MOCKS_ERROR_SUB_TAGS } from './SubTagsMocks'; +import { InMemoryCache, type ApolloLink } from '@apollo/client'; const translations = { ...JSON.parse( @@ -39,7 +35,6 @@ const translations = { const link = new StaticMockLink(MOCKS, true); const link2 = new StaticMockLink(MOCKS_ERROR_SUB_TAGS, true); -const link3 = new StaticMockLink(MOCKS_ERROR_TAG_ANCESTORS, true); async function wait(ms = 500): Promise { await act(() => { @@ -56,25 +51,37 @@ jest.mock('react-toastify', () => ({ }, })); -// const cache = new InMemoryCache({ -// typePolicies: { -// Query: { -// fields: { -// getUserTag: { -// keyArgs: false, -// merge(_, incoming) { -// return incoming; -// }, -// }, -// }, -// }, -// }, -// }); +const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + getUserTag: { + merge(existing = {}, incoming) { + const merged = { + ...existing, + ...incoming, + childTags: { + ...existing.childTags, + ...incoming.childTags, + edges: [ + ...(existing.childTags?.edges || []), + ...(incoming.childTags?.edges || []), + ], + }, + }; + + return merged; + }, + }, + }, + }, + }, +}); const renderSubTags = (link: ApolloLink): RenderResult => { return render( - - + + @@ -104,6 +111,7 @@ describe('Organisation Tags Page', () => { ...jest.requireActual('react-router-dom'), useParams: () => ({ orgId: 'orgId' }), })); + cache.reset(); }); afterEach(() => { @@ -131,16 +139,6 @@ describe('Organisation Tags Page', () => { }); }); - test('renders error component on unsuccessful userTag ancestors query', async () => { - const { queryByText } = renderSubTags(link3); - - await wait(); - - await waitFor(() => { - expect(queryByText(translations.addChildTag)).not.toBeInTheDocument(); - }); - }); - test('opens and closes the create tag modal', async () => { renderSubTags(link); @@ -238,6 +236,84 @@ describe('Organisation Tags Page', () => { }); }); + test('searchs for tags where the name matches the provided search input', async () => { + renderSubTags(link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.searchByName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.searchByName); + fireEvent.change(input, { target: { value: 'searchSubTag' } }); + + // should render the two searched tags from the mock data + // where name starts with "searchUserTag" + await waitFor(() => { + const buttons = screen.getAllByTestId('manageTagBtn'); + expect(buttons.length).toEqual(2); + }); + }); + + test('fetches the tags by the sort order, i.e. latest or oldest first', async () => { + renderSubTags(link); + + await wait(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(translations.searchByName), + ).toBeInTheDocument(); + }); + const input = screen.getByPlaceholderText(translations.searchByName); + fireEvent.change(input, { target: { value: 'searchSubTag' } }); + + // should render the two searched tags from the mock data + // where name starts with "searchUserTag" + await waitFor(() => { + expect(screen.getAllByTestId('tagName')[0]).toHaveTextContent( + 'searchSubTag 1', + ); + }); + + // now change the sorting order + await waitFor(() => { + expect(screen.getByTestId('sortTags')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('sortTags')); + + await waitFor(() => { + expect(screen.getByTestId('oldest')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('oldest')); + + // returns the tags in reverse order + await waitFor(() => { + expect(screen.getAllByTestId('tagName')[0]).toHaveTextContent( + 'searchSubTag 2', + ); + }); + + await waitFor(() => { + expect(screen.getByTestId('sortTags')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('sortTags')); + + await waitFor(() => { + expect(screen.getByTestId('latest')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('latest')); + + // reverse the order again + await waitFor(() => { + expect(screen.getAllByTestId('tagName')[0]).toHaveTextContent( + 'searchSubTag 1', + ); + }); + }); + test('Fetches more sub tags with infinite scroll', async () => { const { getByText } = renderSubTags(link); diff --git a/src/screens/SubTags/SubTags.tsx b/src/screens/SubTags/SubTags.tsx index 193670ac59..063ce9f028 100644 --- a/src/screens/SubTags/SubTags.tsx +++ b/src/screens/SubTags/SubTags.tsx @@ -1,5 +1,5 @@ -import { useMutation, useQuery, type ApolloError } from '@apollo/client'; -import { Search, WarningAmberRounded } from '@mui/icons-material'; +import { useMutation, useQuery } from '@apollo/client'; +import { WarningAmberRounded } from '@mui/icons-material'; import SortIcon from '@mui/icons-material/Sort'; import Loader from 'components/Loader/Loader'; import IconComponent from 'components/IconComponent/IconComponent'; @@ -16,7 +16,10 @@ import { toast } from 'react-toastify'; import type { InterfaceQueryUserTagChildTags } from 'utils/interfaces'; import styles from './SubTags.module.css'; import { DataGrid } from '@mui/x-data-grid'; -import type { InterfaceOrganizationSubTagsQuery } from 'utils/organizationTagsUtils'; +import type { + InterfaceOrganizationSubTagsQuery, + SortedByType, +} from 'utils/organizationTagsUtils'; import { dataGridStyle, TAGS_QUERY_DATA_CHUNK_SIZE, @@ -24,10 +27,7 @@ import { import type { GridCellParams, GridColDef } from '@mui/x-data-grid'; import { Stack } from '@mui/material'; import { CREATE_USER_TAG } from 'GraphQl/Mutations/TagMutations'; -import { - USER_TAG_ANCESTORS, - USER_TAG_SUB_TAGS, -} from 'GraphQl/Queries/userTagQueries'; +import { USER_TAG_SUB_TAGS } from 'GraphQl/Queries/userTagQueries'; import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteScrollLoader from 'components/InfiniteScrollLoader/InfiniteScrollLoader'; @@ -52,6 +52,9 @@ function SubTags(): JSX.Element { const [tagName, setTagName] = useState(''); + const [tagSearchName, setTagSearchName] = useState(''); + const [tagSortOrder, setTagSortOrder] = useState('DESCENDING'); + const showAddSubTagModal = (): void => { setAddSubTagModalIsOpen(true); }; @@ -71,6 +74,8 @@ function SubTags(): JSX.Element { variables: { id: parentTagId, first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: tagSearchName } }, + sortedBy: { id: tagSortOrder }, }, }); @@ -88,7 +93,7 @@ function SubTags(): JSX.Element { fetchMoreResult?: { getChildTags: InterfaceQueryUserTagChildTags }; }, ) => { - if (!fetchMoreResult) return prevResult; + if (!fetchMoreResult) /* istanbul ignore next */ return prevResult; return { getChildTags: { @@ -106,26 +111,6 @@ function SubTags(): JSX.Element { }); }; - const { - data: orgUserTagAncestorsData, - loading: orgUserTagsAncestorsLoading, - error: orgUserTagsAncestorsError, - }: { - data?: { - getUserTagAncestors: { - _id: string; - name: string; - }[]; - }; - loading: boolean; - error?: ApolloError; - refetch: () => void; - } = useQuery(USER_TAG_ANCESTORS, { - variables: { - id: parentTagId, - }, - }); - const [create, { loading: createUserTagLoading }] = useMutation(CREATE_USER_TAG); @@ -156,22 +141,13 @@ function SubTags(): JSX.Element { } }; - if (createUserTagLoading || subTagsLoading || orgUserTagsAncestorsLoading) { - return ; - } - - if (subTagsError || orgUserTagsAncestorsError) { + if (subTagsError) { return (
- Error occured while loading{' '} - {subTagsError ? 'sub tags' : 'tag ancestors'} -
- {subTagsError - ? subTagsError.message - : orgUserTagsAncestorsError?.message} + Error occured while loading sub tags
@@ -182,7 +158,17 @@ function SubTags(): JSX.Element { subTagsData?.getChildTags.childTags.edges.map((edge) => edge.node) ?? /* istanbul ignore next */ []; - const orgUserTagAncestors = orgUserTagAncestorsData?.getUserTagAncestors; + const parentTagName = subTagsData?.getChildTags.name; + + // get the ancestorTags array and push the current tag in it + // used for the tag breadcrumbs + const orgUserTagAncestors = [ + ...(subTagsData?.getChildTags.ancestorTags ?? []), + { + _id: parentTagId, + name: parentTagName, + }, + ]; const redirectToManageTag = (tagId: string): void => { navigate(`/orgtags/${orgId}/manageTag/${tagId}`); @@ -296,21 +282,16 @@ function SubTags(): JSX.Element {
+ setTagSearchName(e.target.value.trim())} data-testid="searchByName" autoComplete="off" - required /> -
- {tCommon('sort')} + {tagSortOrder === 'DESCENDING' + ? tCommon('Latest') + : tCommon('Oldest')} - + setTagSortOrder('DESCENDING')} + > {tCommon('Latest')} - + setTagSortOrder('ASCENDING')} + > {tCommon('Oldest')} @@ -355,82 +344,86 @@ function SubTags(): JSX.Element {
-
-
-
- -
- -
navigate(`/orgtags/${orgId}`)} - className={`fs-6 ms-3 my-1 ${styles.tagsBreadCrumbs}`} - data-testid="allTagsBtn" - > - {'Tags'} - -
+ {subTagsLoading || createUserTagLoading ? ( + + ) : ( +
+
+
+ +
- {orgUserTagAncestors?.map((tag, index) => (
redirectToSubTags(tag._id as string)} - data-testid="redirectToSubTags" + onClick={() => navigate(`/orgtags/${orgId}`)} + className={`fs-6 ms-3 my-1 ${styles.tagsBreadCrumbs}`} + data-testid="allTagsBtn" > - {tag.name} - - {orgUserTagAncestors.length - 1 !== index && ( - - )} + {'Tags'} +
- ))} -
-
- } - scrollableTarget="subTagsScrollableDiv" + + {orgUserTagAncestors?.map((tag, index) => ( +
redirectToSubTags(tag._id as string)} + data-testid="redirectToSubTags" + > + {tag.name} + + {orgUserTagAncestors.length - 1 !== index && ( + + )} +
+ ))} +
+
- row.id} - slots={{ - noRowsOverlay: /* istanbul ignore next */ () => ( - - {t('noTagsFound')} - - ), - }} - sx={dataGridStyle} - getRowClassName={() => `${styles.rowBackground}`} - autoHeight - rowHeight={65} - rows={subTagsList?.map((subTag, index) => ({ - id: index + 1, - ...subTag, - }))} - columns={columns} - isRowSelectable={() => false} - /> - + } + scrollableTarget="subTagsScrollableDiv" + > + row.id} + slots={{ + noRowsOverlay: /* istanbul ignore next */ () => ( + + {t('noTagsFound')} + + ), + }} + sx={dataGridStyle} + getRowClassName={() => `${styles.rowBackground}`} + autoHeight + rowHeight={65} + rows={subTagsList?.map((subTag, index) => ({ + id: index + 1, + ...subTag, + }))} + columns={columns} + isRowSelectable={() => false} + /> + +
-
+ )}
diff --git a/src/screens/SubTags/SubTagsMocks.ts b/src/screens/SubTags/SubTagsMocks.ts index b51541a5e7..5165ea3a53 100644 --- a/src/screens/SubTags/SubTagsMocks.ts +++ b/src/screens/SubTags/SubTagsMocks.ts @@ -1,8 +1,5 @@ import { CREATE_USER_TAG } from 'GraphQl/Mutations/TagMutations'; -import { - USER_TAG_ANCESTORS, - USER_TAG_SUB_TAGS, -} from 'GraphQl/Queries/userTagQueries'; +import { USER_TAG_SUB_TAGS } from 'GraphQl/Queries/userTagQueries'; import { TAGS_QUERY_DATA_CHUNK_SIZE } from 'utils/organizationTagsUtils'; export const MOCKS = [ @@ -12,6 +9,8 @@ export const MOCKS = [ variables: { id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { @@ -30,6 +29,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag1', }, @@ -43,6 +48,12 @@ export const MOCKS = [ childTags: { totalCount: 0, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag2', }, @@ -56,6 +67,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag3', }, @@ -69,6 +86,12 @@ export const MOCKS = [ childTags: { totalCount: 0, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag4', }, @@ -82,6 +105,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag5', }, @@ -95,6 +124,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag6', }, @@ -108,6 +143,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag7', }, @@ -121,6 +162,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag8', }, @@ -134,6 +181,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag9', }, @@ -147,6 +200,12 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag10', }, @@ -159,6 +218,7 @@ export const MOCKS = [ }, totalCount: 11, }, + ancestorTags: [], }, }, }, @@ -170,12 +230,14 @@ export const MOCKS = [ id: '1', after: '10', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { data: { getChildTags: { - name: 'tag1', + name: 'userTag 1', childTags: { edges: [ { @@ -188,6 +250,12 @@ export const MOCKS = [ childTags: { totalCount: 0, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, cursor: 'subTag11', }, @@ -200,34 +268,19 @@ export const MOCKS = [ }, totalCount: 11, }, + ancestorTags: [], }, }, }, }, - { - request: { - query: USER_TAG_ANCESTORS, - variables: { - id: '1', - }, - }, - result: { - data: { - getUserTagAncestors: [ - { - _id: '1', - name: 'tag1', - }, - ], - }, - }, - }, { request: { query: USER_TAG_SUB_TAGS, variables: { id: 'subTag1', first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { @@ -246,6 +299,16 @@ export const MOCKS = [ childTags: { totalCount: 5, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + { + _id: 'subTag1', + name: 'subTag 1', + }, + ], }, cursor: 'subTag1.1', }, @@ -258,109 +321,180 @@ export const MOCKS = [ }, totalCount: 1, }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], }, }, }, }, { request: { - query: USER_TAG_ANCESTORS, + query: USER_TAG_SUB_TAGS, variables: { - id: 'subTag1', + id: '1', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: 'searchSubTag' } }, + sortedBy: { id: 'DESCENDING' }, }, }, result: { data: { - getUserTagAncestors: [ - { - _id: '1', - name: 'tag 1', - }, - { - _id: 'subTag1', - name: 'subTag 1', + getChildTags: { + name: 'userTag 1', + childTags: { + edges: [ + { + node: { + _id: 'searchSubTag1', + name: 'searchSubTag 1', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchSubTag1', + }, + { + node: { + _id: 'searchSubTag2', + name: 'searchSubTag 2', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchSubTag2', + }, + ], + pageInfo: { + startCursor: 'searchSubTag1', + endCursor: 'searchSubTag2', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, }, - ], - }, - }, - }, - { - request: { - query: CREATE_USER_TAG, - variables: { - name: 'subTag 12', - organizationId: '123', - parentTagId: '1', - }, - }, - result: { - data: { - createUserTag: { - _id: 'subTag12', + ancestorTags: [], }, }, }, }, -]; - -export const MOCKS_ERROR_SUB_TAGS = [ { request: { query: USER_TAG_SUB_TAGS, variables: { id: '1', first: TAGS_QUERY_DATA_CHUNK_SIZE, - }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: USER_TAG_ANCESTORS, - variables: { - id: '1', + where: { name: { starts_with: 'searchSubTag' } }, + sortedBy: { id: 'ASCENDING' }, }, }, result: { data: { - getUserTagAncestors: [], + getChildTags: { + name: 'userTag 1', + childTags: { + edges: [ + { + node: { + _id: 'searchSubTag2', + name: 'searchSubTag 2', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchSubTag2', + }, + { + node: { + _id: 'searchSubTag1', + name: 'searchSubTag 1', + usersAssignedTo: { + totalCount: 0, + }, + childTags: { + totalCount: 0, + }, + ancestorTags: [ + { + _id: '1', + name: 'userTag 1', + }, + ], + }, + cursor: 'searchSubTag1', + }, + ], + pageInfo: { + startCursor: 'searchSubTag2', + endCursor: 'searchSubTag1', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 2, + }, + ancestorTags: [], + }, }, }, }, -]; - -export const MOCKS_ERROR_TAG_ANCESTORS = [ { request: { - query: USER_TAG_SUB_TAGS, + query: CREATE_USER_TAG, variables: { - id: '1', - first: TAGS_QUERY_DATA_CHUNK_SIZE, + name: 'subTag 12', + organizationId: '123', + parentTagId: '1', }, }, result: { data: { - getChildTags: { - name: 'tag1', - childTags: { - edges: [], - pageInfo: { - startCursor: '0', - endCursor: '0', - hasNextPage: false, - hasPreviousPage: false, - }, - totalCount: 0, - }, + createUserTag: { + _id: 'subTag12', }, }, }, }, +]; + +export const MOCKS_ERROR_SUB_TAGS = [ { request: { - query: USER_TAG_ANCESTORS, + query: USER_TAG_SUB_TAGS, variables: { id: '1', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + where: { name: { starts_with: '' } }, + sortedBy: { id: 'DESCENDING' }, }, }, error: new Error('Mock Graphql Error'), diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 3d3af2ac64..234b36262e 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -212,12 +212,17 @@ export interface InterfaceQueryOrganizationPostListItem { export interface InterfaceTagData { _id: string; name: string; + parentTag: { _id: string }; usersAssignedTo: { totalCount: number; }; childTags: { totalCount: number; }; + ancestorTags: { + _id: string; + name: string; + }[]; } interface InterfaceTagNodeData { @@ -258,16 +263,19 @@ export interface InterfaceQueryOrganizationUserTags { export interface InterfaceQueryUserTagChildTags { name: string; childTags: InterfaceTagNodeData; + ancestorTags: { + _id: string; + name: string; + }[]; } export interface InterfaceQueryUserTagsAssignedMembers { name: string; usersAssignedTo: InterfaceTagMembersData; -} - -export interface InterfaceQueryUserTagsMembersToAssignTo { - name: string; - usersToAssignTo: InterfaceTagMembersData; + ancestorTags: { + _id: string; + name: string; + }[]; } export interface InterfaceQueryUserTagsMembersToAssignTo { diff --git a/src/utils/organizationTagsUtils.ts b/src/utils/organizationTagsUtils.ts index 75c001816e..99faca3bf2 100644 --- a/src/utils/organizationTagsUtils.ts +++ b/src/utils/organizationTagsUtils.ts @@ -44,6 +44,9 @@ export const TAGS_QUERY_DATA_CHUNK_SIZE = 10; // the tag action type export type TagActionType = 'assignToTags' | 'removeFromTags'; +// the sortedByType +export type SortedByType = 'ASCENDING' | 'DESCENDING'; + // Interfaces for tag queries: // 1. Base interface for Apollo query results interface InterfaceBaseQueryResult {