diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f6989965bc..7baf8be9cd 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 src/screens/ManageTag/ManageTag.tsx src/utils/interfaces.ts + ./.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/utils/interfaces.ts - name: Get changed TypeScript files id: changed-files @@ -97,6 +97,7 @@ jobs: .node-version .husky/** scripts/** + schema.graphql package.json tsconfig.json .gitignore diff --git a/schema.graphql b/schema.graphql index 9bfd49671c..80ca281f73 100644 --- a/schema.graphql +++ b/schema.graphql @@ -214,25 +214,6 @@ type DeletePayload { success: Boolean! } -type DirectChat { - _id: ID! - createdAt: DateTime! - creator: User - messages: [DirectChatMessage] - updatedAt: DateTime! - users: [User!]! -} - -type DirectChatMessage { - _id: ID! - createdAt: DateTime! - directChatMessageBelongsTo: DirectChat! - messageContent: String! - receiver: User! - sender: User! - updatedAt: DateTime! -} - type Donation { _id: ID! amount: Float! @@ -470,26 +451,6 @@ type Group { updatedAt: DateTime! } -type GroupChat { - _id: ID! - title: String! - createdAt: DateTime! - creator: User - messages: [GroupChatMessage] - organization: Organization! - updatedAt: DateTime! - users: [User!]! -} - -type GroupChatMessage { - _id: ID! - createdAt: DateTime! - groupChatMessageBelongsTo: GroupChat! - messageContent: String! - sender: User! - updatedAt: DateTime! -} - type InvalidCursor implements FieldError { message: String! path: [String!]! @@ -579,31 +540,6 @@ type MembershipRequest { user: User! } -type Message { - _id: ID! - createdAt: DateTime! - creator: User - imageUrl: URL - text: String! - updatedAt: DateTime! - videoUrl: URL -} - -type MessageChat { - _id: ID! - createdAt: DateTime! - languageBarrier: Boolean - message: String! - receiver: User! - sender: User! - updatedAt: DateTime! -} - -input MessageChatInput { - message: String! - receiver: ID! -} - type MinimumLengthError implements FieldError { limit: Int! message: String! @@ -659,10 +595,8 @@ type Mutation { organizationId: ID! ): UserCustomData! addUserImage(file: String!): User! - addUserToGroupChat(chatId: ID!, userId: ID!): GroupChat! addUserToUserFamily(familyId: ID!, userId: ID!): UserFamily! adminRemoveEvent(eventId: ID!): Event! - adminRemoveGroup(groupId: ID!): GroupChat! assignUserTag(input: ToggleUserTagAssignInput!): User blockPluginCreationBySuperadmin( blockUser: Boolean! @@ -692,7 +626,6 @@ type Mutation { createAgendaCategory(input: CreateAgendaCategoryInput!): AgendaCategory! createComment(data: CommentInput!, postId: ID!): Comment createChat(data: chatInput!): Chat! - createDirectChat(data: createChatInput!): DirectChat! createDonation( amount: Float! nameOfOrg: String! @@ -706,9 +639,7 @@ type Mutation { recurrenceRuleData: RecurrenceRuleInput ): Event! createEventVolunteer(data: EventVolunteerInput!): EventVolunteer! - createGroupChat(data: createGroupChatInput!): GroupChat! createMember(input: UserAndOrganizationInput!): Organization! - createMessageChat(data: MessageChatInput!): MessageChat! createOrganization(data: OrganizationInput, file: String): Organization! createPlugin( pluginCreatedBy: String! @@ -743,11 +674,9 @@ type Mutation { removeAdmin(data: UserAndOrganizationInput!): AppUserProfile! removeAdvertisement(id: ID!): Advertisement removeComment(id: ID!): Comment - removeDirectChat(chatId: ID!, organizationId: ID!): DirectChat! removeEvent(id: ID!): Event! removeEventAttendee(data: EventAttendeeInput!): User! removeEventVolunteer(id: ID!): EventVolunteer! - removeGroupChat(chatId: ID!): GroupChat! removeMember(data: UserAndOrganizationInput!): Organization! removeOrganization(id: ID!): UserData! removeOrganizationCustomField( @@ -759,7 +688,6 @@ type Mutation { removeSampleOrganization: Boolean! removeUserCustomData(organizationId: ID!): UserCustomData! removeUserFamily(familyId: ID!): UserFamily! - removeUserFromGroupChat(chatId: ID!, userId: ID!): GroupChat! removeUserFromUserFamily(familyId: ID!, userId: ID!): UserFamily! removeUserImage: User! removeUserTag(id: ID!): UserTag @@ -767,14 +695,6 @@ type Mutation { saveFcmToken(token: String): Boolean! sendMembershipRequest(organizationId: ID!): MembershipRequest! sendMessageToChat(chatId: ID!, messageContent: String!, type: String!, replyTo: ID): ChatMessage! - sendMessageToDirectChat( - chatId: ID! - messageContent: String! - ): DirectChatMessage! - sendMessageToGroupChat( - chatId: ID! - messageContent: String! - ): GroupChatMessage! signUp(data: UserInput!, file: String): AuthData! togglePostPin(id: ID!, title: String): Post! unassignUserTag(input: ToggleUserTagAssignInput!): User @@ -1096,11 +1016,6 @@ type Query { customFieldsByOrganization(id: ID!): [OrganizationCustomField] chatById(id: ID!): Chat! chatsByUserId(id: ID!): [Chat] - directChatsByUserID(id: ID!): [DirectChat] - directChatsMessagesByChatID(id: ID!): [DirectChatMessage] - directChatById(id: ID!): DirectChat - groupChatById(id: ID!): DirectChat - groupChatsByUserId(id: ID!): [GroupChat] event(id: ID!): Event eventVolunteersByEvent(id: ID!): [EventVolunteer] eventsByOrganization(id: ID, orderBy: EventOrderByInput): [Event] @@ -1195,10 +1110,7 @@ enum Status { } type Subscription { - directMessageChat: MessageChat messageSentToChat(userId: ID!): ChatMessage - messageSentToDirectChat(userId: ID!): DirectChatMessage - messageSentToGroupChat(userId: ID!): GroupChatMessage onPluginUpdate: Plugin } @@ -1592,17 +1504,6 @@ enum WeekDays { WE } -input createChatInput { - organizationId: ID - userIds: [ID!]! -} - -input createGroupChatInput { - organizationId: ID! - title: String! - userIds: [ID!]! -} - type Venue { _id: ID! capacity: Int! diff --git a/src/App.tsx b/src/App.tsx index dfeb566ca1..37f3bc301e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -150,10 +150,10 @@ function app(): JSX.Element { } /> } /> } /> - } /> + } /> } /> } /> {}, refetchAssignedMembersData: () => {}, - t: (key: string) => translations[key], - tCommon: (key: string) => translations[key], + t: ((key: string) => translations[key]) as TFunction< + 'translation', + 'manageTag' + >, + tCommon: ((key: string) => translations[key]) as TFunction< + 'common', + undefined + >, }; const renderAddPeopleToTagModal = ( @@ -67,7 +74,7 @@ const renderAddPeopleToTagModal = ( } /> diff --git a/src/components/AddPeopleToTag/AddPeopleToTag.tsx b/src/components/AddPeopleToTag/AddPeopleToTag.tsx index a43a47b006..f5c90be096 100644 --- a/src/components/AddPeopleToTag/AddPeopleToTag.tsx +++ b/src/components/AddPeopleToTag/AddPeopleToTag.tsx @@ -1,4 +1,3 @@ -import type { ApolloError } from '@apollo/client'; import { useMutation, useQuery } from '@apollo/client'; import type { GridCellParams, GridColDef } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; @@ -10,8 +9,9 @@ import { Modal, Form, Button } from 'react-bootstrap'; import { useParams } from 'react-router-dom'; import type { InterfaceQueryUserTagsMembersToAssignTo } from 'utils/interfaces'; import styles from './AddPeopleToTag.module.css'; +import type { InterfaceTagUsersToAssignToQuery } from 'utils/organizationTagsUtils'; import { - ADD_PEOPLE_TO_TAGS_QUERY_LIMIT, + TAGS_QUERY_DATA_CHUNK_SIZE, dataGridStyle, } from 'utils/organizationTagsUtils'; import { Stack } from '@mui/material'; @@ -20,6 +20,8 @@ import { ADD_PEOPLE_TO_TAG } from 'GraphQl/Mutations/TagMutations'; import InfiniteScroll from 'react-infinite-scroll-component'; import { WarningAmberRounded } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; +import InfiniteScrollLoader from 'components/InfiniteScrollLoader/InfiniteScrollLoader'; +import type { TFunction } from 'i18next'; /** * Props for the `AddPeopleToTag` component. @@ -28,8 +30,8 @@ export interface InterfaceAddPeopleToTagProps { addPeopleToTagModalIsOpen: boolean; hideAddPeopleToTagModal: () => void; refetchAssignedMembersData: () => void; - t: (key: string) => string; - tCommon: (key: string) => string; + t: TFunction<'translation', 'manageTag'>; + tCommon: TFunction<'common', undefined>; } interface InterfaceMemberData { @@ -58,64 +60,48 @@ const AddPeopleToTag: React.FC = ({ loading: userTagsMembersToAssignToLoading, error: userTagsMembersToAssignToError, fetchMore: fetchMoreMembersToAssignTo, - }: { - data?: { - getUserTag: InterfaceQueryUserTagsMembersToAssignTo; - }; - loading: boolean; - error?: ApolloError; - fetchMore: (options: { + }: InterfaceTagUsersToAssignToQuery = useQuery( + USER_TAGS_MEMBERS_TO_ASSIGN_TO, + { variables: { - after?: string | null; - first?: number | null; - }; - updateQuery?: ( - previousQueryResult: { - getUserTag: InterfaceQueryUserTagsMembersToAssignTo; - }, - options: { - fetchMoreResult: { - getUserTag: InterfaceQueryUserTagsMembersToAssignTo; - }; - }, - ) => { getUserTag: InterfaceQueryUserTagsMembersToAssignTo }; - }) => Promise; - } = useQuery(USER_TAGS_MEMBERS_TO_ASSIGN_TO, { - variables: { - id: currentTagId, - first: ADD_PEOPLE_TO_TAGS_QUERY_LIMIT, + id: currentTagId, + first: TAGS_QUERY_DATA_CHUNK_SIZE, + }, + skip: !addPeopleToTagModalIsOpen, + fetchPolicy: 'no-cache', }, - skip: !addPeopleToTagModalIsOpen, - }); + ); const loadMoreMembersToAssignTo = (): void => { fetchMoreMembersToAssignTo({ variables: { - first: ADD_PEOPLE_TO_TAGS_QUERY_LIMIT, + first: TAGS_QUERY_DATA_CHUNK_SIZE, after: - userTagsMembersToAssignToData?.getUserTag.usersToAssignTo.pageInfo - .endCursor, // Fetch after the last loaded cursor + userTagsMembersToAssignToData?.getUsersToAssignTo.usersToAssignTo + .pageInfo.endCursor, // Fetch after the last loaded cursor }, updateQuery: ( - prevResult: { getUserTag: InterfaceQueryUserTagsMembersToAssignTo }, + prevResult: { + getUsersToAssignTo: InterfaceQueryUserTagsMembersToAssignTo; + }, { fetchMoreResult, }: { fetchMoreResult: { - getUserTag: InterfaceQueryUserTagsMembersToAssignTo; + getUsersToAssignTo: InterfaceQueryUserTagsMembersToAssignTo; }; }, ) => { if (!fetchMoreResult) return prevResult; return { - getUserTag: { - ...fetchMoreResult.getUserTag, + getUsersToAssignTo: { + ...fetchMoreResult.getUsersToAssignTo, usersToAssignTo: { - ...fetchMoreResult.getUserTag.usersToAssignTo, + ...fetchMoreResult.getUsersToAssignTo.usersToAssignTo, edges: [ - ...prevResult.getUserTag.usersToAssignTo.edges, - ...fetchMoreResult.getUserTag.usersToAssignTo.edges, + ...prevResult.getUsersToAssignTo.usersToAssignTo.edges, + ...fetchMoreResult.getUsersToAssignTo.usersToAssignTo.edges, ], }, }, @@ -125,9 +111,9 @@ const AddPeopleToTag: React.FC = ({ }; const userTagMembersToAssignTo = - userTagsMembersToAssignToData?.getUserTag.usersToAssignTo.edges.map( + userTagsMembersToAssignToData?.getUsersToAssignTo.usersToAssignTo.edges.map( (edge) => edge.node, - ); + ) ?? /* istanbul ignore next */ []; const handleAddOrRemoveMember = (member: InterfaceMemberData): void => { setAssignToMembers((prevMembers) => { @@ -168,8 +154,7 @@ const AddPeopleToTag: React.FC = ({ hideAddPeopleToTagModal(); setAssignToMembers([]); } - } catch (error: unknown) { - /* istanbul ignore next */ + } catch (error: unknown) /* istanbul ignore next */ { const errorMessage = error instanceof Error ? error.message : tErrors('unknownError'); toast.error(errorMessage); @@ -299,7 +284,7 @@ const AddPeopleToTag: React.FC = ({ id="scrollableDiv" data-testid="scrollableDiv" style={{ - height: 300, + maxHeight: 300, overflow: 'auto', }} > @@ -307,21 +292,18 @@ const AddPeopleToTag: React.FC = ({ dataLength={userTagMembersToAssignTo?.length ?? 0} // This is important field to render the next data next={loadMoreMembersToAssignTo} hasMore={ - userTagsMembersToAssignToData?.getUserTag.usersToAssignTo - .pageInfo.hasNextPage ?? false - } - loader={ -
-
-
+ userTagsMembersToAssignToData?.getUsersToAssignTo + .usersToAssignTo.pageInfo.hasNextPage ?? + /* istanbul ignore next */ false } + loader={} scrollableTarget="scrollableDiv" > row._id} + getRowId={(row) => row.id} slots={{ noRowsOverlay: /* istanbul ignore next */ () => ( = ({ ), }} - sx={dataGridStyle} + sx={{ + ...dataGridStyle, + '& .MuiDataGrid-topContainer': { + position: 'static', + }, + '& .MuiDataGrid-virtualScrollerContent': { + marginTop: '0', + }, + }} getRowClassName={() => `${styles.rowBackground}`} autoHeight rowHeight={65} diff --git a/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts b/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts index ab185bb858..223fcd3064 100644 --- a/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts +++ b/src/components/AddPeopleToTag/AddPeopleToTagsMocks.ts @@ -1,5 +1,6 @@ import { ADD_PEOPLE_TO_TAG } from 'GraphQl/Mutations/TagMutations'; import { USER_TAGS_MEMBERS_TO_ASSIGN_TO } from 'GraphQl/Queries/userTagQueries'; +import { TAGS_QUERY_DATA_CHUNK_SIZE } from 'utils/organizationTagsUtils'; export const MOCKS = [ { @@ -7,12 +8,12 @@ export const MOCKS = [ query: USER_TAGS_MEMBERS_TO_ASSIGN_TO, variables: { id: '1', - first: 7, + first: TAGS_QUERY_DATA_CHUNK_SIZE, }, }, result: { data: { - getUserTag: { + getUsersToAssignTo: { name: 'tag1', usersToAssignTo: { edges: [ @@ -72,14 +73,38 @@ export const MOCKS = [ }, cursor: '7', }, + { + node: { + _id: '8', + firstName: 'member', + lastName: '8', + }, + cursor: '8', + }, + { + node: { + _id: '9', + firstName: 'member', + lastName: '9', + }, + cursor: '9', + }, + { + node: { + _id: '10', + firstName: 'member', + lastName: '10', + }, + cursor: '10', + }, ], pageInfo: { startCursor: '1', - endCursor: '7', + endCursor: '10', hasNextPage: true, hasPreviousPage: false, }, - totalCount: 10, + totalCount: 12, }, }, }, @@ -90,48 +115,40 @@ export const MOCKS = [ query: USER_TAGS_MEMBERS_TO_ASSIGN_TO, variables: { id: '1', - first: 7, - after: '7', + first: TAGS_QUERY_DATA_CHUNK_SIZE, + after: '10', }, }, result: { data: { - getUserTag: { + getUsersToAssignTo: { name: 'tag1', usersToAssignTo: { edges: [ { node: { - _id: '8', - firstName: 'member', - lastName: '8', - }, - cursor: '8', - }, - { - node: { - _id: '9', + _id: '11', firstName: 'member', - lastName: '9', + lastName: '11', }, - cursor: '9', + cursor: '11', }, { node: { - _id: '10', + _id: '12', firstName: 'member', - lastName: '10', + lastName: '12', }, - cursor: '10', + cursor: '12', }, ], pageInfo: { - startCursor: '8', - endCursor: '10', + startCursor: '11', + endCursor: '12', hasNextPage: false, hasPreviousPage: true, }, - totalCount: 10, + totalCount: 12, }, }, }, @@ -161,7 +178,7 @@ export const MOCKS_ERROR = [ query: USER_TAGS_MEMBERS_TO_ASSIGN_TO, variables: { id: '1', - first: 7, + first: TAGS_QUERY_DATA_CHUNK_SIZE, }, }, error: new Error('Mock Graphql Error'), diff --git a/src/components/InfiniteScrollLoader/InfiniteScrollLoader.module.css b/src/components/InfiniteScrollLoader/InfiniteScrollLoader.module.css new file mode 100644 index 0000000000..a5b609ae75 --- /dev/null +++ b/src/components/InfiniteScrollLoader/InfiniteScrollLoader.module.css @@ -0,0 +1,23 @@ +.simpleLoader { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} + +.spinner { + width: 2rem; + height: 2rem; + margin: 1rem 0; + border: 4px solid transparent; + border-top-color: var(--bs-gray-400); + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/components/InfiniteScrollLoader/InfiniteScrollLoader.test.tsx b/src/components/InfiniteScrollLoader/InfiniteScrollLoader.test.tsx new file mode 100644 index 0000000000..1e179e0de0 --- /dev/null +++ b/src/components/InfiniteScrollLoader/InfiniteScrollLoader.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import InfiniteScrollLoader from './InfiniteScrollLoader'; + +describe('Testing InfiniteScrollLoader component', () => { + test('Component should be rendered properly', () => { + render(); + + expect(screen.getByTestId('infiniteScrollLoader')).toBeInTheDocument(); + expect( + screen.getByTestId('infiniteScrollLoaderSpinner'), + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/InfiniteScrollLoader/InfiniteScrollLoader.tsx b/src/components/InfiniteScrollLoader/InfiniteScrollLoader.tsx new file mode 100644 index 0000000000..7846889cdb --- /dev/null +++ b/src/components/InfiniteScrollLoader/InfiniteScrollLoader.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import styles from './InfiniteScrollLoader.module.css'; + +/** + * A Loader for infinite scroll. + */ + +const InfiniteScrollLoader = (): JSX.Element => { + return ( +
+
+
+ ); +}; + +export default InfiniteScrollLoader; diff --git a/src/components/TagActions/TagActions.module.css b/src/components/TagActions/TagActions.module.css index e667adb96e..62c5855981 100644 --- a/src/components/TagActions/TagActions.module.css +++ b/src/components/TagActions/TagActions.module.css @@ -1,83 +1,3 @@ -.btnsContainer { - display: flex; - margin: 2rem 0; -} - -.btnsContainer .btnsBlock { - display: flex; - width: max-content; -} - -.btnsContainer .btnsBlock button { - margin-left: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.btnsContainer .input { - flex: 1; - position: relative; - max-width: 60%; - justify-content: space-between; -} - -.btnsContainer input { - outline: 1px solid var(--bs-gray-400); -} - -.btnsContainer .input button { - width: 52px; -} - -@media (max-width: 1020px) { - .btnsContainer { - flex-direction: column; - margin: 1.5rem 0; - } - - .btnsContainer .btnsBlock { - margin: 1.5rem 0 0 0; - justify-content: space-between; - } - - .btnsContainer .btnsBlock button { - margin: 0; - } - - .btnsContainer .btnsBlock div button { - margin-right: 1.5rem; - } -} - -/* For mobile devices */ - -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } - - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } - - .btnsContainer .btnsBlock div { - flex: 1; - } - - .btnsContainer .btnsBlock div[title='Sort organizations'] { - margin-right: 0.5rem; - } - - .btnsContainer .btnsBlock button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } -} - .errorContainer { min-height: 100vh; } @@ -96,30 +16,8 @@ margin-bottom: 1rem; } -.tableHeader { - background-color: var(--bs-primary); - color: var(--bs-white); - font-size: 1rem; -} - -.rowBackground { - background-color: var(--bs-white); - max-height: 120px; -} - -.tagsBreadCrumbs { - color: var(--bs-gray); - cursor: pointer; -} - -.tagsBreadCrumbs:hover { - color: var(--bs-blue); - font-weight: 600; - text-decoration: underline; -} - .scrollContainer { - max-height: 100px; /* Adjust as needed */ + max-height: 100px; overflow-y: auto; margin-bottom: 1rem; } @@ -129,46 +27,10 @@ align-items: center; padding: 5px 10px; border-radius: 12px; - background-color: #f8f9fa; /* Light background */ - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - max-width: calc(100% - 30px); /* Ensure it fits within the container */ + box-shadow: 0 1px 3px var(--bs-gray-400); + max-width: calc(100% - 2rem); } .removeFilterIcon { cursor: pointer; } - -.scrContainer { - max-height: 300px; - overflow: scroll; - /* padding-right: 8px; */ -} - -.allTagsHeading { - color: rgb(77, 76, 76); - font-weight: 600; -} - -/* SimpleLoader.css */ -.simpleLoader { - display: flex; - justify-content: start; - align-items: center; - width: 100%; - height: 100%; -} - -.spinner { - width: 40px; - height: 40px; - border: 4px solid var(--bs-gray); - border-top-color: var(--bs-gray); - border-radius: 50%; - animation: spin 0.6s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} diff --git a/src/components/TagActions/TagActions.test.tsx b/src/components/TagActions/TagActions.test.tsx index 39e287395b..140504d044 100644 --- a/src/components/TagActions/TagActions.test.tsx +++ b/src/components/TagActions/TagActions.test.tsx @@ -27,6 +27,7 @@ import { MOCKS_ERROR_ORGANIZATION_TAGS_QUERY, MOCKS_ERROR_SUBTAGS_QUERY, } from './TagActionsMocks'; +import type { TFunction } from 'i18next'; const link = new StaticMockLink(MOCKS, true); const link2 = new StaticMockLink(MOCKS_ERROR_ORGANIZATION_TAGS_QUERY, true); @@ -57,18 +58,30 @@ const translations = { const props: InterfaceTagActionsProps[] = [ { - assignToTagsModalIsOpen: true, - hideAssignToTagsModal: () => {}, + tagActionsModalIsOpen: true, + hideTagActionsModal: () => {}, tagActionType: 'assignToTags', - t: (key: string) => translations[key], - tCommon: (key: string) => translations[key], + t: ((key: string) => translations[key]) as TFunction< + 'translation', + 'manageTag' + >, + tCommon: ((key: string) => translations[key]) as TFunction< + 'common', + undefined + >, }, { - assignToTagsModalIsOpen: true, - hideAssignToTagsModal: () => {}, + tagActionsModalIsOpen: true, + hideTagActionsModal: () => {}, tagActionType: 'removeFromTags', - t: (key: string) => translations[key], - tCommon: (key: string) => translations[key], + t: ((key: string) => translations[key]) as TFunction< + 'translation', + 'manageTag' + >, + tCommon: ((key: string) => translations[key]) as TFunction< + 'common', + undefined + >, }, ]; @@ -83,7 +96,7 @@ const renderTagActionsModal = ( } /> @@ -127,6 +140,37 @@ describe('Organisation Tags Page', () => { }); }); + test('Component calls hideTagActionsModal when modal is closed', async () => { + const hideTagActionsModalMock = jest.fn(); + + const props2: InterfaceTagActionsProps = { + tagActionsModalIsOpen: true, + hideTagActionsModal: hideTagActionsModalMock, + tagActionType: 'assignToTags', + t: ((key: string) => translations[key]) as TFunction< + 'translation', + 'manageTag' + >, + tCommon: ((key: string) => translations[key]) as TFunction< + 'common', + undefined + >, + }; + + renderTagActionsModal(props2, link); + + await wait(); + + await waitFor(() => { + expect(screen.getByTestId('closeTagActionsModalBtn')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('closeTagActionsModalBtn')); + + await waitFor(() => { + expect(hideTagActionsModalMock).toHaveBeenCalled(); + }); + }); + test('Renders error component when when query is unsuccessful', async () => { const { queryByText } = renderTagActionsModal(props[0], link2); diff --git a/src/components/TagActions/TagActions.tsx b/src/components/TagActions/TagActions.tsx index 0c3246f16c..60099e855d 100644 --- a/src/components/TagActions/TagActions.tsx +++ b/src/components/TagActions/TagActions.tsx @@ -1,4 +1,3 @@ -import type { ApolloError } from '@apollo/client'; import { useMutation, useQuery } from '@apollo/client'; import Loader from 'components/Loader/Loader'; import { USER_TAG_ANCESTORS } from 'GraphQl/Queries/userTagQueries'; @@ -17,11 +16,16 @@ import { REMOVE_FROM_TAGS, } from 'GraphQl/Mutations/TagMutations'; import { toast } from 'react-toastify'; -import type { TagActionType } from 'utils/organizationTagsUtils'; -import { TAGS_QUERY_LIMIT } from 'utils/organizationTagsUtils'; +import type { + InterfaceOrganizationTagsQuery, + TagActionType, +} from 'utils/organizationTagsUtils'; +import { TAGS_QUERY_DATA_CHUNK_SIZE } from 'utils/organizationTagsUtils'; import InfiniteScroll from 'react-infinite-scroll-component'; import { WarningAmberRounded } from '@mui/icons-material'; import TagNode from './TagNode'; +import InfiniteScrollLoader from 'components/InfiniteScrollLoader/InfiniteScrollLoader'; +import type { TFunction } from 'i18next'; interface InterfaceUserTagsAncestorData { _id: string; @@ -32,16 +36,16 @@ interface InterfaceUserTagsAncestorData { * Props for the `AssignToTags` component. */ export interface InterfaceTagActionsProps { - assignToTagsModalIsOpen: boolean; - hideAssignToTagsModal: () => void; + tagActionsModalIsOpen: boolean; + hideTagActionsModal: () => void; tagActionType: TagActionType; - t: (key: string) => string; - tCommon: (key: string) => string; + t: TFunction<'translation', 'manageTag'>; + tCommon: TFunction<'common', undefined>; } const TagActions: React.FC = ({ - assignToTagsModalIsOpen, - hideAssignToTagsModal, + tagActionsModalIsOpen, + hideTagActionsModal, tagActionType, t, tCommon, @@ -53,38 +57,55 @@ const TagActions: React.FC = ({ loading: orgUserTagsLoading, error: orgUserTagsError, fetchMore: orgUserTagsFetchMore, - }: { - data?: { - organizations: InterfaceQueryOrganizationUserTags[]; - }; - loading: boolean; - error?: ApolloError; - refetch: () => void; - fetchMore: (options: { + }: InterfaceOrganizationTagsQuery = useQuery(ORGANIZATION_USER_TAGS_LIST, { + variables: { + id: orgId, + first: TAGS_QUERY_DATA_CHUNK_SIZE, + }, + skip: !tagActionsModalIsOpen, + }); + + const loadMoreUserTags = (): void => { + orgUserTagsFetchMore({ variables: { - first: number; - after?: string; - }; + first: TAGS_QUERY_DATA_CHUNK_SIZE, + after: orgUserTagsData?.organizations[0].userTags.pageInfo.endCursor, + }, updateQuery: ( - previousResult: { organizations: InterfaceQueryOrganizationUserTags[] }, - options: { + prevResult: { organizations: InterfaceQueryOrganizationUserTags[] }, + { + fetchMoreResult, + }: { fetchMoreResult?: { organizations: InterfaceQueryOrganizationUserTags[]; }; }, - ) => { organizations: InterfaceQueryOrganizationUserTags[] }; - }) => void; - } = useQuery(ORGANIZATION_USER_TAGS_LIST, { - variables: { - id: orgId, - first: TAGS_QUERY_LIMIT, - }, - skip: !assignToTagsModalIsOpen, - }); + ) => { + if (!fetchMoreResult) return prevResult; - const userTagsList = orgUserTagsData?.organizations[0]?.userTags.edges.map( - (edge) => edge.node, - ); + return { + organizations: [ + { + ...prevResult.organizations[0], + userTags: { + ...prevResult.organizations[0].userTags, + edges: [ + ...prevResult.organizations[0].userTags.edges, + ...fetchMoreResult.organizations[0].userTags.edges, + ], + pageInfo: fetchMoreResult.organizations[0].userTags.pageInfo, + }, + }, + ], + }; + }, + }); + }; + + const userTagsList = + orgUserTagsData?.organizations[0]?.userTags.edges.map( + (edge) => edge.node, + ) ?? /* istanbul ignore next */ []; const [checkedTagId, setCheckedTagId] = useState(null); const [uncheckedTagId, setUncheckedTagId] = useState(null); @@ -208,43 +229,6 @@ const TagActions: React.FC = ({ }, }); - const loadMoreUserTags = (): void => { - orgUserTagsFetchMore({ - variables: { - first: TAGS_QUERY_LIMIT, - after: orgUserTagsData?.organizations[0].userTags.pageInfo.endCursor, - }, - updateQuery: ( - prevResult: { organizations: InterfaceQueryOrganizationUserTags[] }, - { - fetchMoreResult, - }: { - fetchMoreResult?: { - organizations: InterfaceQueryOrganizationUserTags[]; - }; - }, - ) => { - if (!fetchMoreResult) return prevResult; - - return { - organizations: [ - { - ...prevResult.organizations[0], - userTags: { - ...prevResult.organizations[0].userTags, - edges: [ - ...prevResult.organizations[0].userTags.edges, - ...fetchMoreResult.organizations[0].userTags.edges, - ], - pageInfo: fetchMoreResult.organizations[0].userTags.pageInfo, - }, - }, - ], - }; - }, - }); - }; - const [assignToTags] = useMutation(ASSIGN_TO_TAGS); const [removeFromTags] = useMutation(REMOVE_FROM_TAGS); @@ -272,7 +256,7 @@ const TagActions: React.FC = ({ } else { toast.success(t('successfullyRemovedFromTags')); } - hideAssignToTagsModal(); + hideTagActionsModal(); } } catch (error: unknown) { /* istanbul ignore next */ @@ -298,8 +282,8 @@ const TagActions: React.FC = ({ return ( <> = ({ )}
-
+
{t('allTags')}
@@ -354,10 +338,9 @@ const TagActions: React.FC = ({ id="scrollableDiv" data-testid="scrollableDiv" style={{ - height: 300, + maxHeight: 300, overflow: 'auto', }} - className={`${styles.scrContainer}`} > = ({ orgUserTagsData?.organizations[0].userTags.pageInfo .hasNextPage ?? false } - loader={ -
-
-
- } + loader={} scrollableTarget="scrollableDiv" > {userTagsList?.map((tag) => ( @@ -396,7 +375,7 @@ const TagActions: React.FC = ({