From 3f4c82682120e47c7c8e68d923e866206273bab8 Mon Sep 17 00:00:00 2001 From: slawomir-werner Date: Tue, 23 Sep 2025 11:31:22 +0200 Subject: [PATCH] [CP-3303] duplicated contacts menu (#2654) --- .../kompakt-contacts-delete-details.ts | 4 +- .../specs/overview/kompakt-contacts-delete.ts | 4 +- .../kompakt-contacts-viewing-details.ts | 2 +- .../overview/kompakt-contacts-viewing.ts | 2 +- .../src/lib/helpers/api-configuration-data.ts | 1 + .../src/lib/menu-configuration.spec.tsx | 14 +++ .../rest/header/header.component.tsx | 79 ++++++++++----- .../renderer/constants/views.ts | 3 +- .../device/models/src/lib/menu/menu-config.ts | 2 + .../responses/src/lib/default-responses.ts | 23 +++++ .../lib/feature-configuration-responses.ts | 13 ++- .../views/contacts-duplicates-view.ts | 18 ++++ .../lib/use-dev-views/views/contacts-view.ts | 2 +- .../src/lib/use-dev-views/views/index.ts | 2 + libs/generic-view/models/src/index.ts | 3 + .../src/lib/mc-contacts-duplicates-view.ts | 20 ++++ .../ui/src/lib/generated/index.ts | 5 + .../mc-contacts-duplicates-view.ts | 97 +++++++++++++++++++ .../src/lib/generate-menu/generate-menu.ts | 1 + 19 files changed, 261 insertions(+), 34 deletions(-) create mode 100644 libs/generic-view/feature/src/lib/use-dev-views/views/contacts-duplicates-view.ts create mode 100644 libs/generic-view/models/src/lib/mc-contacts-duplicates-view.ts create mode 100644 libs/generic-view/ui/src/lib/generated/mc-contacts-view/mc-contacts-duplicates-view.ts diff --git a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete-details.ts b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete-details.ts index 6fc5c66fd..a4eb1aedb 100644 --- a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete-details.ts +++ b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete-details.ts @@ -45,7 +45,7 @@ describe("E2E mock sample - overview view", () => { const contactsCounter = ContactsKompaktPage.contactsCounter await expect(contactsCounter).toBeDisplayed() - await expect(contactsCounter).toHaveText("Contacts (17)") + await expect(contactsCounter).toHaveText("All Contacts (17)") await browser.pause(500) }) @@ -108,6 +108,6 @@ describe("E2E mock sample - overview view", () => { //verify if counter is updated after deleting (number should be deducted by 1) const contactsCounter = ContactsKompaktPage.contactsCounter await expect(contactsCounter).toBeDisplayed() - await expect(contactsCounter).toHaveText("Contacts (16)") + await expect(contactsCounter).toHaveText("All Contacts (16)") }) }) diff --git a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete.ts b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete.ts index bc9fbd73c..aa595a2cb 100644 --- a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete.ts +++ b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-delete.ts @@ -45,7 +45,7 @@ describe("E2E mock sample - overview view", () => { const contactsCounter = ContactsKompaktPage.contactsCounter await expect(contactsCounter).toBeDisplayed() - await expect(contactsCounter).toHaveText("Contacts (17)") + await expect(contactsCounter).toHaveText("All Contacts (17)") }) it("Select first contact's checkbox", async () => { @@ -114,6 +114,6 @@ describe("E2E mock sample - overview view", () => { //verify if counter is updated after deleting (number should be deducted by 1) const contactsCounter = ContactsKompaktPage.contactsCounter await expect(contactsCounter).toBeDisplayed() - await expect(contactsCounter).toHaveText("Contacts (16)") + await expect(contactsCounter).toHaveText("All Contacts (16)") }) }) diff --git a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing-details.ts b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing-details.ts index 5cff9b037..6ca5266ea 100644 --- a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing-details.ts +++ b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing-details.ts @@ -45,7 +45,7 @@ describe("E2E mock sample - overview view", () => { const contactsCounter = ContactsKompaktPage.contactsCounter await expect(contactsCounter).toBeDisplayed() - await expect(contactsCounter).toHaveText("Contacts (17)") + await expect(contactsCounter).toHaveText("All Contacts (17)") }) it("Select sixth contact to open contact details", async () => { diff --git a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing.ts b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing.ts index 94d4cade9..cf7746a2c 100644 --- a/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing.ts +++ b/apps/mudita-center-e2e/src/specs/overview/kompakt-contacts-viewing.ts @@ -44,7 +44,7 @@ describe("E2E mock sample - overview view", () => { const contactsCounter = ContactsKompaktPage.contactsCounter await expect(contactsCounter).toBeDisplayed() - await expect(contactsCounter).toHaveText("Contacts (17)") + await expect(contactsCounter).toHaveText("All Contacts (17)") await browser.pause(500) }) diff --git a/libs/api-devices-testing/src/lib/helpers/api-configuration-data.ts b/libs/api-devices-testing/src/lib/helpers/api-configuration-data.ts index fcd92fa30..d8b2bfd08 100644 --- a/libs/api-devices-testing/src/lib/helpers/api-configuration-data.ts +++ b/libs/api-devices-testing/src/lib/helpers/api-configuration-data.ts @@ -16,6 +16,7 @@ export async function getApiFeaturesAndEntityTypes( const genericFeatures = [ "mc-overview", "mc-contacts", + "mc-contacts-duplicates", "mc-data-migration", "mc-file-manager-internal", ].sort() diff --git a/libs/api-devices-testing/src/lib/menu-configuration.spec.tsx b/libs/api-devices-testing/src/lib/menu-configuration.spec.tsx index 368a1623a..d41171029 100644 --- a/libs/api-devices-testing/src/lib/menu-configuration.spec.tsx +++ b/libs/api-devices-testing/src/lib/menu-configuration.spec.tsx @@ -80,6 +80,20 @@ describe("API configuration", () => { expect(mcContactsMenuItem?.feature).toBe("mc-contacts") expect(mcContactsMenuItem?.icon).toBe("contacts-book") + const mcContactsDuplicatesMenuItem = mcContactsMenuItem?.submenu?.find( + (item) => item.feature === "mc-contacts-duplicates" + ) + if (mcContactsDuplicatesMenuItem !== undefined) { + expect(mcContactsMenuItem?.inheritHeaderName).toBeTruthy() + expect(mcContactsDuplicatesMenuItem).toBeDefined() + expect(mcContactsDuplicatesMenuItem?.displayName).toBe( + "Manage Duplicates" + ) + expect(mcContactsDuplicatesMenuItem?.feature).toBe( + "mc-contacts-duplicates" + ) + } + expect(mcDataMigrationMenuItem?.displayName).toBe("Data Migration") expect(mcDataMigrationMenuItem?.feature).toBe("mc-data-migration") expect(mcDataMigrationMenuItem?.icon).toBe("data-migration") diff --git a/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx b/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx index b765349bc..6fc3fc8aa 100644 --- a/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx +++ b/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx @@ -25,6 +25,10 @@ import Icon, { import { IconType } from "Core/__deprecated__/renderer/components/core/icon/icon-type" import { intl } from "Core/__deprecated__/renderer/utils/intl" import { selectActiveDeviceMenuElements } from "generic-view/store" +import { + MenuElement, + MenuElementItem, +} from "Core/__deprecated__/renderer/constants/menu-elements" const messages = defineMessages({ backButtonLabel: { id: "module.generic.viewBackButton" }, @@ -74,39 +78,26 @@ const Header: FunctionComponent = ({ } const previousViewName = location?.state?.previousViewName - const genericMenu = useSelector( - // (state: ReduxRootState) => state.genericViews.menu - selectActiveDeviceMenuElements - ) + const genericMenu = useSelector(selectActiveDeviceMenuElements) const [currentLocation, setCurrentLocation] = useState< { id: string } | string >() const [renderHeaderButton, setRenderHeaderButton] = useState(false) useEffect(() => { const pathname = location.pathname - const currentMenuElementName = Object.keys(views).find( - (key) => views[key as keyof typeof views].url === pathname - ) - const menuElementNameWithHeaderButton = Object.keys(views).find( - (key) => views[key as keyof typeof views].renderHeaderButton - ) - if (currentMenuElementName) { - const currentMenuElement = - views[currentMenuElementName as keyof typeof views] - setCurrentLocation(currentMenuElement.label) - setRenderHeaderButton( - menuElementNameWithHeaderButton === currentMenuElementName - ) - } else if (!previousViewName) { - const currentGenericMenuElement = genericMenu - ?.flatMap((element) => element.items) - .find((item) => item?.button.url === pathname) - if (currentGenericMenuElement) { - setCurrentLocation(currentGenericMenuElement.button.label) - setRenderHeaderButton(false) - } + const label = resolveHeaderLabel(pathname, genericMenu ?? []) + if (label !== undefined) { + setCurrentLocation(label) } + const renderButton = + Object.keys(views).find( + (key) => + views[key as keyof typeof views].url === pathname && + views[key as keyof typeof views].renderHeaderButton + ) !== undefined + setRenderHeaderButton(renderButton) }, [genericMenu, location, previousViewName]) + return ( {previousViewName ? ( @@ -137,4 +128,42 @@ const Header: FunctionComponent = ({ ) } +const findLabelInMenuItems = ( + items: MenuElementItem[], + pathname: string +): string | undefined => { + for (const item of items) { + if (item?.items && item.button.inheritHeaderName) { + const found = findLabelInMenuItems(item.items, pathname) + if (found) return found + } + if (item?.button?.url === pathname) { + const label = item.button.label + return typeof label === "string" ? label : label?.id + } + } + return undefined +} + +const resolveHeaderLabel = ( + pathname: string, + genericMenu: MenuElement[] +): string | undefined => { + const viewKey = Object.keys(views).find( + (key) => views[key as keyof typeof views].url === pathname + ) + + if (viewKey) { + const label = views[viewKey as keyof typeof views].label + if (typeof label === "string") return label + if (label?.id) return intl.formatMessage({ id: label.id }) + return undefined + } + + const allItems = genericMenu.flatMap((element) => + Array.isArray(element.items) ? element.items : [] + ) + return allItems.length ? findLabelInMenuItems(allItems, pathname) : undefined +} + export default Header diff --git a/libs/core/__deprecated__/renderer/constants/views.ts b/libs/core/__deprecated__/renderer/constants/views.ts index 7497e80a0..adf7ee6da 100644 --- a/libs/core/__deprecated__/renderer/constants/views.ts +++ b/libs/core/__deprecated__/renderer/constants/views.ts @@ -52,6 +52,7 @@ export type Views = { } | string url: string + inheritHeaderName?: boolean renderHeaderButton?: boolean } } @@ -113,5 +114,5 @@ export const views: Views = { [View.Quotations]: { label: messages.quotations, url: URL_MAIN.quotations, - } + }, } diff --git a/libs/device/models/src/lib/menu/menu-config.ts b/libs/device/models/src/lib/menu/menu-config.ts index 4c1f3cd91..d11a7189a 100644 --- a/libs/device/models/src/lib/menu/menu-config.ts +++ b/libs/device/models/src/lib/menu/menu-config.ts @@ -9,12 +9,14 @@ import { z } from "zod" const SubmenuItemConfigValidator = z.object({ feature: z.string(), displayName: z.string().optional(), + inheritHeaderName: z.boolean().optional(), }) const MenuItemConfigValidator = z.object({ feature: z.string(), displayName: z.string().optional(), icon: z.nativeEnum(IconType).optional(), + inheritHeaderName: z.boolean().optional(), submenu: z.array(SubmenuItemConfigValidator).optional(), }) diff --git a/libs/e2e-mock/responses/src/lib/default-responses.ts b/libs/e2e-mock/responses/src/lib/default-responses.ts index b3f8cf376..14598e8f6 100644 --- a/libs/e2e-mock/responses/src/lib/default-responses.ts +++ b/libs/e2e-mock/responses/src/lib/default-responses.ts @@ -15,6 +15,7 @@ import { featureConfigurationFileManagerInternal, featureConfigurationFileManagerExternal, featureConfigurationOverview, + featureConfigurationContactsDuplicates, } from "./feature-configuration-responses" //import from "Core/device" breaks usage in e2e @@ -69,6 +70,7 @@ export const DEFAULT_RESPONSES: MocksArrayResponsesMap = { features: [ "mc-overview", "mc-contacts", + "mc-contacts-duplicates", "mc-file-manager-internal", "mc-file-manager-external", ], @@ -107,6 +109,17 @@ export const DEFAULT_RESPONSES: MocksArrayResponsesMap = { feature: "mc-contacts", displayName: "Contacts", icon: "contacts-book", + inheritHeaderName: true, + submenu: [ + { + feature: "mc-contacts", + displayName: "All Contacts", + }, + { + feature: "mc-contacts-duplicates", + displayName: "Manage Duplicates", + }, + ], }, { feature: "mc-file-manager-internal", @@ -148,6 +161,16 @@ export const DEFAULT_RESPONSES: MocksArrayResponsesMap = { }, }, }, + { + status: ResponseStatus.Ok, + body: featureConfigurationContactsDuplicates, + match: { + expected: { + feature: "mc-contacts-duplicates", + lang: "en-US", + }, + }, + }, { status: ResponseStatus.Ok, body: featureConfigurationOverview, diff --git a/libs/e2e-mock/responses/src/lib/feature-configuration-responses.ts b/libs/e2e-mock/responses/src/lib/feature-configuration-responses.ts index 6c8b9b0dd..ff3d4638d 100644 --- a/libs/e2e-mock/responses/src/lib/feature-configuration-responses.ts +++ b/libs/e2e-mock/responses/src/lib/feature-configuration-responses.ts @@ -124,7 +124,18 @@ export const featureConfigurationContacts = { entityTypes: ["contacts"], }, // @ts-ignore - screenTitle: "Contacts", + screenTitle: "All Contacts", + }, +} + +export const featureConfigurationContactsDuplicates = { + main: { + component: "mc-contacts-duplicates-view", + config: { + entityTypes: ["contacts"], + }, + // @ts-ignore + screenTitle: "Manage Duplicates", }, } diff --git a/libs/generic-view/feature/src/lib/use-dev-views/views/contacts-duplicates-view.ts b/libs/generic-view/feature/src/lib/use-dev-views/views/contacts-duplicates-view.ts new file mode 100644 index 000000000..145d998d5 --- /dev/null +++ b/libs/generic-view/feature/src/lib/use-dev-views/views/contacts-duplicates-view.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { View } from "generic-view/utils" + +// @ts-ignore +export const contactsDuplicatesView: View = { + main: { + component: "mc-contacts-duplicates-view", + config: { + entityTypes: ["contacts"], + }, + // @ts-ignore + screenTitle: "Manage Duplicates", + }, +} diff --git a/libs/generic-view/feature/src/lib/use-dev-views/views/contacts-view.ts b/libs/generic-view/feature/src/lib/use-dev-views/views/contacts-view.ts index 5bd7c1a3d..b6b471e20 100644 --- a/libs/generic-view/feature/src/lib/use-dev-views/views/contacts-view.ts +++ b/libs/generic-view/feature/src/lib/use-dev-views/views/contacts-view.ts @@ -13,6 +13,6 @@ export const contactsView: View = { entityTypes: ["contacts"], }, // @ts-ignore - screenTitle: "Contacts", + screenTitle: "All Contacts", }, } diff --git a/libs/generic-view/feature/src/lib/use-dev-views/views/index.ts b/libs/generic-view/feature/src/lib/use-dev-views/views/index.ts index 22f6094c7..56feb061d 100644 --- a/libs/generic-view/feature/src/lib/use-dev-views/views/index.ts +++ b/libs/generic-view/feature/src/lib/use-dev-views/views/index.ts @@ -3,12 +3,14 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +import { contactsDuplicatesView } from "./contacts-duplicates-view" import { contactsView } from "./contacts-view" import { fileManagerView } from "./file-manager-view" import { mcDataMigrationView } from "./mc-data-migration-view" export default { contacts: contactsView, + contactsDuplicates: contactsDuplicatesView, fileManager: fileManagerView, ["mc-data-migration"]: mcDataMigrationView, } diff --git a/libs/generic-view/models/src/index.ts b/libs/generic-view/models/src/index.ts index 8b773cf8e..fcf554ce4 100644 --- a/libs/generic-view/models/src/index.ts +++ b/libs/generic-view/models/src/index.ts @@ -52,6 +52,7 @@ import { backupRestore } from "./lib/backup-restore" import { backupRestoreAvailable } from "./lib/backup-restore-available" import { mcImportContactsButton } from "./lib/mc-import-contacts-button" import { mcContactsView } from "./lib/mc-contacts-view" +import { mcContactsDuplicatesView } from "./lib/mc-contacts-duplicates-view" import { mcDataMigration } from "./lib/mc-data-migration" import { mcFileManagerView } from "./lib/mc-file-manager" import { incomingFeatureInfo } from "./lib/incoming-feature-info" @@ -128,6 +129,7 @@ export * from "./lib/backup-restore" export * from "./lib/import-contacts" export * from "./lib/mc-import-contacts-button" export * from "./lib/mc-contacts-view" +export * from "./lib/mc-contacts-duplicates-view" export * from "./lib/modal-visibility-controller" export * from "./lib/mc-data-migration" export * from "./lib/mc-file-manager" @@ -203,6 +205,7 @@ export default { [importContacts.key]: importContacts, [mcImportContactsButton.key]: mcImportContactsButton, [mcContactsView.key]: mcContactsView, + [mcContactsDuplicatesView.key]: mcContactsDuplicatesView, [mcDataMigration.key]: mcDataMigration, [mcFileManagerView.key]: mcFileManagerView, [incomingFeatureInfo.key]: incomingFeatureInfo, diff --git a/libs/generic-view/models/src/lib/mc-contacts-duplicates-view.ts b/libs/generic-view/models/src/lib/mc-contacts-duplicates-view.ts new file mode 100644 index 000000000..b331291e9 --- /dev/null +++ b/libs/generic-view/models/src/lib/mc-contacts-duplicates-view.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { z } from "zod" + +const dataValidator = z.undefined() + +const configValidator = z.object({ + entityTypes: z.array(z.string()).min(1), +}) + +export type McContactsDuplicatesView = z.infer + +export const mcContactsDuplicatesView = { + key: "mc-contacts-duplicates-view", + dataValidator, + configValidator, +} as const diff --git a/libs/generic-view/ui/src/lib/generated/index.ts b/libs/generic-view/ui/src/lib/generated/index.ts index 2a67f9a98..c9ff535bf 100644 --- a/libs/generic-view/ui/src/lib/generated/index.ts +++ b/libs/generic-view/ui/src/lib/generated/index.ts @@ -6,6 +6,7 @@ import { Feature, mcContactsView, + mcContactsDuplicatesView, mcFileManagerView, mcImportContactsButton, } from "generic-view/models" @@ -14,12 +15,16 @@ import { generateMcFileManagerView } from "./mc-file-manager/mc-file-manager-vie import { generateFileManagerData } from "./mc-file-manager/mc-file-manager-data" import { generateMcImportContactsButton } from "./mc-import-contacts-button" import { View } from "generic-view/utils" +import { generateMcContactsDuplicatesView } from "./mc-contacts-view/mc-contacts-duplicates-view" export * from "./mc-import-contacts-button" export * from "./mc-contacts-view/mc-contacts-view" +export * from "./mc-contacts-view/mc-contacts-duplicates-view" +export * from "./mc-file-manager/mc-file-manager-view" export const generatedViews = { [mcContactsView.key]: generateMcContactsView, + [mcContactsDuplicatesView.key]: generateMcContactsDuplicatesView, [mcFileManagerView.key]: generateMcFileManagerView, [mcImportContactsButton.key]: generateMcImportContactsButton, } diff --git a/libs/generic-view/ui/src/lib/generated/mc-contacts-view/mc-contacts-duplicates-view.ts b/libs/generic-view/ui/src/lib/generated/mc-contacts-view/mc-contacts-duplicates-view.ts new file mode 100644 index 000000000..d8e72de22 --- /dev/null +++ b/libs/generic-view/ui/src/lib/generated/mc-contacts-view/mc-contacts-duplicates-view.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ComponentGenerator, IconType } from "generic-view/utils" +import { McContactsDuplicatesView } from "generic-view/models" + +export const generateMcContactsDuplicatesView: ComponentGenerator< + McContactsDuplicatesView +> = (key, config) => { + return { + [key]: { + component: "block-plain", + config: { + backgroundColor: "white", + }, + layout: { + width: "100%", + height: "100%", + gridLayout: { + rows: ["auto", "1fr"], + columns: ["1fr"], + }, + }, + childrenKeys: ["emptyListWrapper"], + }, + emptyListWrapper: { + component: "conditional-renderer", + dataProvider: { + source: "entities-metadata", + entitiesType: "contacts", + fields: [ + { + modifier: "length", + providerField: "totalDuplicates", + componentField: "data.render", + condition: "eq", + value: 0, + }, + ], + }, + childrenKeys: ["fullScreenWrapper"], + }, + fullScreenWrapper: { + component: "block-plain", + childrenKeys: [ + "emptyStateIcon", + "emptyStateText", + "importContactsButton", + ], + layout: { + flexLayout: { + direction: "column", + justifyContent: "center", + alignItems: "center", + rowGap: "24px", + }, + gridPlacement: { + row: 1, + column: 1, + width: 1, + height: 2, + }, + }, + }, + emptyStateIcon: { + component: "modal.titleIcon", + config: { + type: IconType.ContactsBook, + }, + }, + emptyStateText: { + component: "block-plain", + childrenKeys: ["emptyStateTitle", "emptyStateDetailText"], + layout: { + flexLayout: { + direction: "column", + alignItems: "center", + rowGap: "8px", + }, + }, + }, + emptyStateTitle: { + component: "typography.h3", + config: { + text: "We couldn't find any duplicates", + }, + }, + emptyStateDetailText: { + component: "typography.p1", + config: { + text: "If we detect any new duplicates, we'll list them here.", + }, + }, + } +} diff --git a/libs/generic-view/utils/src/lib/generate-menu/generate-menu.ts b/libs/generic-view/utils/src/lib/generate-menu/generate-menu.ts index dbf28d3b7..fa41e6b2f 100644 --- a/libs/generic-view/utils/src/lib/generate-menu/generate-menu.ts +++ b/libs/generic-view/utils/src/lib/generate-menu/generate-menu.ts @@ -53,6 +53,7 @@ const processMenuItem = (item: MenuItemConfig): MenuElementItem => ({ icon: getIcon(item.feature, item.icon), button: { label: item.displayName as string, + inheritHeaderName: item.inheritHeaderName as boolean, url: `/generic/${item.feature}`, }, items: item.submenu?.map((subitem) => processMenuItem(subitem)),