[CP-3303] duplicated contacts menu (#2654)

This commit is contained in:
slawomir-werner
2025-09-23 11:31:22 +02:00
parent fe6fc0056c
commit 3f4c826821
19 changed files with 261 additions and 34 deletions

View File

@@ -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)")
})
})

View File

@@ -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)")
})
})

View File

@@ -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 () => {

View File

@@ -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)
})

View File

@@ -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()

View File

@@ -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")

View File

@@ -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<HeaderProps> = ({
}
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 (
<HeaderWrapper>
{previousViewName ? (
@@ -137,4 +128,42 @@ const Header: FunctionComponent<HeaderProps> = ({
)
}
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

View File

@@ -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,
}
},
}

View File

@@ -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(),
})

View File

@@ -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,

View File

@@ -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",
},
}

View File

@@ -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",
},
}

View File

@@ -13,6 +13,6 @@ export const contactsView: View = {
entityTypes: ["contacts"],
},
// @ts-ignore
screenTitle: "Contacts",
screenTitle: "All Contacts",
},
}

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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<typeof configValidator>
export const mcContactsDuplicatesView = {
key: "mc-contacts-duplicates-view",
dataValidator,
configValidator,
} as const

View File

@@ -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,
}

View File

@@ -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.",
},
},
}
}

View File

@@ -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)),