Files
5e79dd699e fix: prevent country defaulting to Zimbabwe in search result contacts (#14098)
## Context

Same root-cause as #7822 / #14078, but in a second file that PR #14078
doesn't touch:

\`app/javascript/dashboard/modules/search/components/SearchResultContactItem.vue\`.

\`countries.json\` entries only have \`id\` (e.g. \`"AF"\`) — there is
no \`code\` field. So:

\`\`\`js
const countriesMap = countries.reduce((acc, country) => {
acc[country.code] = country; // country.code is undefined →
acc[undefined] = country
  acc[country.id] = country;
  return acc;
}, {});
\`\`\`

…overwrites \`acc[undefined]\` on every iteration, leaving whichever
country comes last in the list (Zimbabwe, \`id: "ZW"\`). Later, the
\`countryDetails\` lookup falls back to that value whenever the
contact's \`country\` / \`countryCode\` is missing or unknown, and
Zimbabwe is displayed incorrectly.

## Fix

One-line delete — drop the dead \`acc[country.code] = country\` write.
Lookups by ISO code continue to work via \`country.id\`.

## Scope

Only the search-results card. The Contacts card is already being fixed
in #14078 with the same one-line delete. It's worth patching this
surface too so the same symptom doesn't reappear when the same contact
is accessed via \`Ctrl+K\` search.

## Test plan

- [ ] Reproduce: search for a contact with \`country: null\` /
\`countryCode: null\` — before this patch the flag renders as Zimbabwe;
after, it renders as no country (current expected fallback).
- [ ] Search for a contact with a valid ISO \`countryCode\` (e.g.
\`"IN"\`) — country still resolves correctly.
- [ ] Contacts list page (\`ContactsCard.vue\`, fixed in #14078) — no
regression.

## Follow-up (not in this PR)

Both components keep rebuilding the same \`countriesMap\` per mount. A
small \`shared/constants/countries.js\` export (\`export const byId =
countries.reduce(...)\`, computed once at module load) would save the
per-mount cost and centralise the shape so this bug can't return.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-28 12:08:46 +05:30

154 lines
3.8 KiB
Vue

<script setup>
import { computed } from 'vue';
import { frontendURL } from 'dashboard/helper/URLHelper';
import countries from 'shared/constants/countries';
import { dynamicTime } from 'shared/helpers/timeHelper';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Flag from 'dashboard/components-next/flag/Flag.vue';
const props = defineProps({
id: {
type: [String, Number],
default: 0,
},
email: {
type: String,
default: '',
},
phone: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
thumbnail: {
type: String,
default: '',
},
accountId: {
type: [String, Number],
default: 0,
},
additionalAttributes: {
type: Object,
default: () => ({}),
},
updatedAt: {
type: Number,
default: 0,
},
});
const navigateTo = computed(() => {
return frontendURL(`accounts/${props.accountId}/contacts/${props.id}`);
});
const countriesMap = computed(() => {
return countries.reduce((acc, country) => {
acc[country.id] = country;
return acc;
}, {});
});
const updatedAtTime = computed(() => {
if (!props.updatedAt) return '';
return dynamicTime(props.updatedAt);
});
const countryDetails = computed(() => {
const { country, countryCode, city } = props.additionalAttributes;
if (!country && !countryCode) return null;
const activeCountry =
countriesMap.value[country] || countriesMap.value[countryCode];
if (!activeCountry) return null;
return {
countryCode: activeCountry.id,
city: city ? `${city},` : null,
name: activeCountry.name,
};
});
const formattedLocation = computed(() => {
if (!countryDetails.value) return '';
return [countryDetails.value.city, countryDetails.value.name]
.filter(Boolean)
.join(' ');
});
</script>
<template>
<router-link :to="navigateTo">
<CardLayout
layout="row"
class="[&>div]:justify-start [&>div]:px-4 [&>div]:py-3 [&>div]:items-start hover:bg-n-slate-2 dark:hover:bg-n-solid-3"
>
<Avatar
:name="name"
:src="thumbnail"
:size="24"
rounded-full
class="mt-1 flex-shrink-0"
/>
<div class="min-w-0 flex flex-col items-start gap-1.5 w-full">
<div class="flex items-center min-w-0 justify-between gap-2 w-full">
<h5 class="text-sm font-medium truncate min-w-0 text-n-slate-12 py-1">
{{ name }}
</h5>
<span
v-if="updatedAtTime"
class="text-sm font-normal min-w-0 truncate text-n-slate-11"
>
{{ $t('SEARCH.UPDATED_AT', { time: updatedAtTime }) }}
</span>
</div>
<div
class="grid items-center gap-3 m-0 text-sm overflow-hidden min-w-0 grid-cols-[minmax(0,max-content)_auto_minmax(0,max-content)_auto_minmax(0,max-content)]"
>
<span
v-if="email"
class="truncate text-n-slate-11 min-w-0"
:title="email"
>
{{ email }}
</span>
<div v-if="email && phone" class="w-px h-3 bg-n-slate-6 rounded" />
<span
v-if="phone"
:title="phone"
class="truncate text-n-slate-11 min-w-0"
>
{{ phone }}
</span>
<div
v-if="(email || phone) && countryDetails"
class="w-px h-3 bg-n-slate-6 rounded"
/>
<span
v-if="countryDetails"
class="truncate text-n-slate-11 flex items-center gap-1 min-w-0"
>
<Flag
:country="countryDetails.countryCode"
class="size-3 shrink-0"
/>
<span class="truncate min-w-0">{{ formattedLocation }}</span>
</span>
</div>
</div>
</CardLayout>
</router-link>
</template>