diff --git a/.changeset/olive-carrots-change.md b/.changeset/olive-carrots-change.md new file mode 100644 index 000000000..072850667 --- /dev/null +++ b/.changeset/olive-carrots-change.md @@ -0,0 +1,5 @@ +--- +'@orchestrator-ui/orchestrator-ui-components': minor +--- + +Fix notes not updating when changing pages, fix cancel button resetting note to older value diff --git a/apps/wfo-ui b/apps/wfo-ui index 859b99c0e..fafc3eea7 160000 --- a/apps/wfo-ui +++ b/apps/wfo-ui @@ -1 +1 @@ -Subproject commit 859b99c0e4622dce0881744e79ce6f0ea2abff94 +Subproject commit fafc3eea7f9eef89e809b7f5d3dd70a906f22d94 diff --git a/package-lock.json b/package-lock.json index 0f696f6a3..860578546 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@elastic/eui": "^97.3.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.1", - "@orchestrator-ui/orchestrator-ui-components": "^2.8.2", + "@orchestrator-ui/orchestrator-ui-components": "*", "@reduxjs/toolkit": "^2.0.1", "eslint": "^8.57.0", "moment": "^2.29.4", @@ -63,7 +63,7 @@ "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.1", "@open-policy-agent/opa-wasm": "^1.2.0", - "@orchestrator-ui/orchestrator-ui-components": "^2.8.2", + "@orchestrator-ui/orchestrator-ui-components": "*", "@reduxjs/toolkit": "^2.0.1", "@sentry/nextjs": "^8.37.1", "eslint": "^8.57.0", @@ -25760,7 +25760,7 @@ }, "packages/orchestrator-ui-components": { "name": "@orchestrator-ui/orchestrator-ui-components", - "version": "2.15.0", + "version": "3.0.3", "license": "Apache-2.0", "dependencies": { "@elastic/eui": "^97.0.0", diff --git a/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoInlineNoteEdit.tsx b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoInlineNoteEdit.tsx index 1b96e9c2b..ba19660f2 100644 --- a/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoInlineNoteEdit.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoInlineNoteEdit.tsx @@ -5,50 +5,37 @@ import { EuiInlineEditText } from '@elastic/eui'; import { WfoToolTip } from '@/components'; import { useOrchestratorTheme } from '@/hooks'; -import { useStartProcessMutation } from '@/rtk/endpoints/forms'; -import type { Subscription } from '@/types'; +import { INVISIBLE_CHARACTER } from '@/utils'; interface WfoInlineNoteEditProps { - value: Subscription['note']; - subscriptionId?: Subscription['subscriptionId']; + value: string; onlyShowOnHover?: boolean; + triggerNoteModifyWorkflow?: (note: string) => void; } export const WfoInlineNoteEdit: FC = ({ value, - subscriptionId, onlyShowOnHover = false, + triggerNoteModifyWorkflow = () => {}, }) => { const { theme } = useOrchestratorTheme(); - const [note, setNote] = useState(value ?? ''); + const [note, setNote] = useState(value); const [isTooltipVisible, setIsTooltipVisible] = useState(true); - const [startProcess] = useStartProcessMutation(); - const triggerNoteModifyWorkflow = () => { - const noteModifyPayload = [ - { subscription_id: subscriptionId }, - { note }, - ]; - startProcess({ - workflowName: 'modify_note', - userInputs: noteModifyPayload, - }); - }; - const handleSave = () => { - triggerNoteModifyWorkflow(); + triggerNoteModifyWorkflow(note); setIsTooltipVisible(true); }; const handleCancel = () => { - setNote(value ?? ''); + setNote(value); setIsTooltipVisible(true); }; // This useEffect makes sure the note is updated when a new value property is passed in // for example by a parent component that is update through a websocket event useEffect(() => { - setNote(value ?? ''); + setNote(value); }, [value]); return ( @@ -63,72 +50,79 @@ export const WfoInlineNoteEdit: FC = ({ }} > - ) => { - setNote(e.target.value); - }} - onCancel={handleCancel} - onSave={handleSave} - size={'s'} - css={{ - width: theme.base * 16, - '.euiFlexItem:nth-of-type(2)': { - justifyContent: 'center', - }, - '.euiButtonEmpty__content': { - justifyContent: 'left', - }, - }} - readModeProps={{ - onClick: () => setIsTooltipVisible(false), - title: '', - css: { - minWidth: '100%', - '.euiIcon': { - visibility: onlyShowOnHover - ? 'hidden' - : 'visible', + + ) => { + setNote(e.target.value); + }} + onCancel={handleCancel} + onSave={handleSave} + size={'s'} + css={{ + width: theme.base * 16, + '.euiFlexItem:nth-of-type(2)': { + justifyContent: 'center', }, - }, - }} - editModeProps={{ - saveButtonProps: { - color: 'primary', - size: 'xs', - }, - cancelButtonProps: { - color: 'danger', - size: 'xs', - }, - inputProps: { - css: { + '.euiButtonEmpty__content': { justifyContent: 'left', - height: '32px', - paddingLeft: '4px', - margin: '0', - width: '98%', }, - }, - formRowProps: { + }} + readModeProps={{ + onClick: () => setIsTooltipVisible(false), + title: '', css: { - padding: 0, - margin: 0, - height: '32px', - '.euiFormRow__fieldWrapper': { - minHeight: '32px', + minWidth: '100%', + '.euiIcon': { + visibility: onlyShowOnHover + ? 'hidden' + : 'visible', + }, + }, + }} + editModeProps={{ + saveButtonProps: { + color: 'primary', + size: 'xs', + }, + cancelButtonProps: { + color: 'danger', + size: 'xs', + }, + inputProps: { + css: { + justifyContent: 'left', height: '32px', + paddingLeft: '4px', + margin: '0', + width: '98%', + }, + }, + formRowProps: { + css: { padding: 0, margin: 0, + height: '32px', + '.euiFormRow__fieldWrapper': { + minHeight: '32px', + height: '32px', + padding: 0, + margin: 0, + }, }, }, - }, - }} - /> + }} + /> + ); diff --git a/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoSubscriptionDetailNoteEdit.tsx b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoSubscriptionDetailNoteEdit.tsx new file mode 100644 index 000000000..2a8b19680 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoSubscriptionDetailNoteEdit.tsx @@ -0,0 +1,55 @@ +import type { FC } from 'react'; +import React from 'react'; + +import { WfoInlineNoteEdit } from '@/components'; +import { useGetSubscriptionDetailQuery } from '@/rtk'; +import { useStartProcessMutation } from '@/rtk/endpoints/forms'; +import { useUpdateSubscriptionDetailNoteOptimisticMutation } from '@/rtk/endpoints/subscriptionListMutation'; +import { SubscriptionDetail } from '@/types'; +import { INVISIBLE_CHARACTER } from '@/utils'; + +interface WfoSubscriptionDetailNoteEditProps { + subscriptionId: SubscriptionDetail['subscriptionId']; + onlyShowOnHover?: boolean; +} + +export const WfoSubscriptionDetailNoteEdit: FC< + WfoSubscriptionDetailNoteEditProps +> = ({ subscriptionId, onlyShowOnHover = false }) => { + const { data, endpointName } = useGetSubscriptionDetailQuery({ + subscriptionId, + }); + + const selectedItem = data?.subscription ?? { note: '' }; + const [startProcess] = useStartProcessMutation(); + const [updateSub] = useUpdateSubscriptionDetailNoteOptimisticMutation(); + + const triggerNoteModifyWorkflow = (note: string) => { + const noteModifyPayload = [ + { subscription_id: subscriptionId }, + { note: note === INVISIBLE_CHARACTER ? '' : note }, + ]; + startProcess({ + workflowName: 'modify_note', + userInputs: noteModifyPayload, + }); + + updateSub({ + queryName: endpointName ?? '', + subscriptionId: subscriptionId, + note: note, + }); + }; + + return ( + + ); +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoSubscriptionNoteEdit.tsx b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoSubscriptionNoteEdit.tsx new file mode 100644 index 000000000..a750583e3 --- /dev/null +++ b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/WfoSubscriptionNoteEdit.tsx @@ -0,0 +1,64 @@ +import type { FC } from 'react'; +import React from 'react'; + +import { WfoInlineNoteEdit } from '@/components'; +import { ApiResult, SubscriptionListResponse, UseQuery } from '@/rtk'; +import { useStartProcessMutation } from '@/rtk/endpoints/forms'; +import { useUpdateSubscriptionNoteOptimisticMutation } from '@/rtk/endpoints/subscriptionListMutation'; +import { Subscription } from '@/types'; +import { INVISIBLE_CHARACTER } from '@/utils'; + +interface WfoSubscriptionNoteEditProps { + subscriptionId: Subscription['subscriptionId']; + onlyShowOnHover?: boolean; + queryVariables: Record; + useQuery: UseQuery; +} + +export const WfoSubscriptionNoteEdit: FC = ({ + subscriptionId, + onlyShowOnHover = false, + queryVariables, + useQuery, +}) => { + const { selectedItem } = useQuery(queryVariables, { + selectFromResult: (result: ApiResult) => ({ + selectedItem: result?.data?.subscriptions?.find( + (sub) => sub.subscriptionId === subscriptionId, + ), + }), + }); + const endpointName = useQuery().endpointName; + const [startProcess] = useStartProcessMutation(); + const [updateSub] = useUpdateSubscriptionNoteOptimisticMutation(); + + const triggerNoteModifyWorkflow = (note: string) => { + const noteModifyPayload = [ + { subscription_id: subscriptionId }, + { note: note === INVISIBLE_CHARACTER ? '' : note }, + ]; + startProcess({ + workflowName: 'modify_note', + userInputs: noteModifyPayload, + }); + + updateSub({ + queryName: endpointName ?? '', + subscriptionId: subscriptionId, + graphQlQueryVariables: queryVariables, + note: note, + }); + }; + + return ( + + ); +}; diff --git a/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/index.ts b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/index.ts index a1484d8bd..130c6cb2d 100644 --- a/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/index.ts +++ b/packages/orchestrator-ui-components/src/components/WfoInlineNoteEdit/index.ts @@ -1 +1,3 @@ export * from './WfoInlineNoteEdit'; +export * from './WfoSubscriptionNoteEdit'; +export * from './WfoSubscriptionDetailNoteEdit'; diff --git a/packages/orchestrator-ui-components/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx b/packages/orchestrator-ui-components/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx index df637b229..87b789c46 100644 --- a/packages/orchestrator-ui-components/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx @@ -6,7 +6,7 @@ import { SubscriptionKeyValueBlock, WfoCustomerDescriptionsField, WfoInSyncField, - WfoInlineNoteEdit, + WfoSubscriptionDetailNoteEdit, WfoSubscriptionStatusBadge, } from '@/components'; import { SubscriptionDetail } from '@/types'; @@ -35,7 +35,6 @@ export const WfoSubscriptionDetailSection = ({ status, customer, customerDescriptions, - note, } = subscriptionDetail; const subscriptionDetailBlockData = [ @@ -102,9 +101,9 @@ export const WfoSubscriptionDetailSection = ({ { key: t('note'), value: ( - ), }, diff --git a/packages/orchestrator-ui-components/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx b/packages/orchestrator-ui-components/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx index f0604d261..a18f0d810 100644 --- a/packages/orchestrator-ui-components/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx +++ b/packages/orchestrator-ui-components/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx @@ -10,24 +10,26 @@ import { Pagination, WfoDateTime, WfoInlineJson, - WfoInlineNoteEdit, WfoInsyncIcon, WfoJsonCodeBlock, WfoSubscriptionStatusBadge, getPageIndexChangeHandler, getPageSizeChangeHandler, } from '@/components'; +import { WfoSubscriptionNoteEdit } from '@/components/WfoInlineNoteEdit/WfoSubscriptionNoteEdit'; import { WfoAdvancedTable } from '@/components/WfoTable/WfoAdvancedTable'; import { WfoAdvancedTableColumnConfig } from '@/components/WfoTable/WfoAdvancedTable/types'; import { ColumnType } from '@/components/WfoTable/WfoTable'; import { mapSortableAndFilterableValuesToTableColumnConfig } from '@/components/WfoTable/WfoTable/utils'; import { DataDisplayParams, useShowToastMessage } from '@/hooks'; +import { UseQuery } from '@/rtk'; import { + SubscriptionListResponse, useGetSubscriptionListQuery, useLazyGetSubscriptionListQuery, } from '@/rtk/endpoints/subscriptionList'; import { mapRtkErrorToWfoError } from '@/rtk/utils'; -import { GraphqlQueryVariables, SortOrder } from '@/types'; +import { GraphqlQueryVariables, SortOrder, Subscription } from '@/types'; import { getQueryVariablesForExport, getTypedFieldFromObject, @@ -150,13 +152,21 @@ export const WfoSubscriptionsList: FC = ({ note: { columnType: ColumnType.DATA, label: t('note'), - renderData: (cellValue, row) => ( - - ), + renderData: (cellValue, row) => { + return ( + + } + /> + ); + }, }, metadata: { columnType: ColumnType.DATA, diff --git a/packages/orchestrator-ui-components/src/rtk/api.ts b/packages/orchestrator-ui-components/src/rtk/api.ts index e69e12b6f..1feda651a 100644 --- a/packages/orchestrator-ui-components/src/rtk/api.ts +++ b/packages/orchestrator-ui-components/src/rtk/api.ts @@ -3,10 +3,12 @@ import { GraphQLErrorExtensions } from 'graphql/error/GraphQLError'; import { getSession, signOut } from 'next-auth/react'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { ErrorResponse } from '@rtk-query/graphql-request-base-query/dist/GraphqlBaseQueryTypes'; +import { SubscriptionListItem } from '@/components'; import type { WfoSession } from '@/hooks'; import { wfoGraphqlRequestBaseQuery } from '@/rtk/wfoGraphqlRequestBaseQuery'; -import { CacheTagType } from '@/types'; +import { CacheTagType, GraphqlQueryVariables } from '@/types'; import type { RootState } from './store'; @@ -28,6 +30,34 @@ export enum HttpStatus { MultipleChoices = 300, } +export interface ApiResult { + data?: T; + error?: ErrorResponse; + isLoading: boolean; + isFetching: boolean; + isError: boolean; + refetch?: () => void; + selectFromResult?: (result: T) => T; + endpointName?: string; +} + +interface UseQueryOptions { + selectFromResult?: ( + result: ApiResult, + ) => Partial> & { selectedItem?: U }; + subscriptionId?: string; +} + +interface UseQueryReturn extends ApiResult { + selectedItem?: U; + endpointName?: string; +} + +export type UseQuery = ( + queryVariables?: GraphqlQueryVariables, + options?: UseQueryOptions, +) => UseQueryReturn; + type ExtraOptions = { baseQueryType?: BaseQueryTypes; apiName?: string; diff --git a/packages/orchestrator-ui-components/src/rtk/endpoints/subscriptionListMutation.ts b/packages/orchestrator-ui-components/src/rtk/endpoints/subscriptionListMutation.ts new file mode 100644 index 000000000..476b07b33 --- /dev/null +++ b/packages/orchestrator-ui-components/src/rtk/endpoints/subscriptionListMutation.ts @@ -0,0 +1,90 @@ +import { SubscriptionListItem } from '@/components/WfoSubscriptionsList'; +import { + SubscriptionDetailResponse, + SubscriptionListResponse, + orchestratorApi, +} from '@/rtk'; +import { GraphqlQueryVariables } from '@/types'; + +const subscriptionListMutationApi = orchestratorApi.injectEndpoints({ + endpoints: (builder) => ({ + emptyQuery: builder.query< + SubscriptionListResponse, + GraphqlQueryVariables + >({ query: () => ({}) }), + emptyDetailQuery: builder.query< + SubscriptionDetailResponse, + { subscriptionId: string } + >({ query: () => ({}) }), + updateSubscriptionNoteOptimistic: builder.mutation< + { mockResponse: boolean }, + { + queryName: string; + subscriptionId: string; + graphQlQueryVariables: GraphqlQueryVariables; + note: string; + } + >({ + queryFn: async () => ({ data: { mockResponse: true } }), + async onQueryStarted( + { queryName, subscriptionId, graphQlQueryVariables, ...patch }, + { dispatch, queryFulfilled }, + ) { + const patchResult = dispatch( + subscriptionListMutationApi.util.updateQueryData( + // @ts-expect-error - Suggest ts ignore because of the type mismatch between emptyQuery and queryName + queryName, + graphQlQueryVariables, + (draft: SubscriptionListResponse) => { + const subscription = draft.subscriptions.find( + (item) => + item.subscriptionId === subscriptionId, + ); + if (subscription) { + subscription.note = patch.note; + } + }, + ), + ); + try { + await queryFulfilled; + } catch { + patchResult.undo(); + } + }, + }), + updateSubscriptionDetailNoteOptimistic: builder.mutation< + { mockResponse: boolean }, + { queryName: string; subscriptionId: string; note: string } + >({ + queryFn: async () => ({ data: { mockResponse: true } }), + async onQueryStarted( + { queryName, subscriptionId, ...patch }, + { dispatch, queryFulfilled }, + ) { + const patchResult = dispatch( + subscriptionListMutationApi.util.updateQueryData( + // @ts-expect-error - Suggest ts ignore because of the type mismatch between emptyDetailQuery and queryName + queryName, + { subscriptionId: subscriptionId }, + (draft: SubscriptionDetailResponse) => { + if (draft) { + draft.subscription.note = patch.note; + } + }, + ), + ); + try { + await queryFulfilled; + } catch { + patchResult.undo(); + } + }, + }), + }), +}); + +export const { + useUpdateSubscriptionNoteOptimisticMutation, + useUpdateSubscriptionDetailNoteOptimisticMutation, +} = subscriptionListMutationApi; diff --git a/packages/orchestrator-ui-components/src/utils/string.spec.ts b/packages/orchestrator-ui-components/src/utils/string.spec.ts index 806593d83..7c0e6867f 100644 --- a/packages/orchestrator-ui-components/src/utils/string.spec.ts +++ b/packages/orchestrator-ui-components/src/utils/string.spec.ts @@ -1,6 +1,7 @@ import { camelToHuman, isAllUpperCase, + isNullOrEmpty, removeSuffix, snakeToHuman, snakeToKebab, @@ -132,3 +133,29 @@ describe('isAllUpperCase()', () => { expect(result).toBe(false); }); }); + +describe('isNullOrEmpty', () => { + it('should return true for null', () => { + expect(isNullOrEmpty(null)).toBe(true); + }); + + it('should return true for undefined', () => { + expect(isNullOrEmpty(undefined)).toBe(true); + }); + + it('should return true for an empty string', () => { + expect(isNullOrEmpty('')).toBe(true); + }); + + it('should return true for a string with only spaces', () => { + expect(isNullOrEmpty(' ')).toBe(true); + }); + + it('should return false for a non-empty string', () => { + expect(isNullOrEmpty('Hello')).toBe(false); + }); + + it('should return false for a string with spaces and text', () => { + expect(isNullOrEmpty(' Hello ')).toBe(false); + }); +}); diff --git a/packages/orchestrator-ui-components/src/utils/strings.ts b/packages/orchestrator-ui-components/src/utils/strings.ts index 6a17fdfbe..516d5c9c9 100644 --- a/packages/orchestrator-ui-components/src/utils/strings.ts +++ b/packages/orchestrator-ui-components/src/utils/strings.ts @@ -24,3 +24,9 @@ export const snakeToKebab = (value: string): string => { }; export const isAllUpperCase = (str: string) => str === str.toUpperCase(); + +export const isNullOrEmpty = (str: string | null | undefined): boolean => { + return str === null || str === undefined || str.trim() === ''; +}; + +export const INVISIBLE_CHARACTER = '‎';