From 2b9fc4f7aed7ed79e5382ac6334e6211fe8d9be8 Mon Sep 17 00:00:00 2001 From: patinen Date: Thu, 29 Jan 2026 09:09:54 +0200 Subject: [PATCH 1/2] refactor(members): migrate to V2 roles-based schema --- .../src/entities/About/api/aboutApi.ts | 2 +- .../src/entities/Member/api/mappers.ts | 167 ++++++++++++++---- .../src/entities/Member/api/memberTeamsApi.ts | 4 +- .../src/entities/Member/api/membersApi.ts | 14 +- .../src/entities/Member/api/translations.ts | 53 +++++- .../src/entities/Member/model/types/types.ts | 27 ++- .../src/entities/Member/ui/MemberItem.tsx | 8 +- 7 files changed, 213 insertions(+), 62 deletions(-) diff --git a/frontend-next-migration/src/entities/About/api/aboutApi.ts b/frontend-next-migration/src/entities/About/api/aboutApi.ts index b195e6c62..69997d41f 100644 --- a/frontend-next-migration/src/entities/About/api/aboutApi.ts +++ b/frontend-next-migration/src/entities/About/api/aboutApi.ts @@ -26,7 +26,7 @@ export const aboutApi = directusApi.injectEndpoints({ queryFn: async () => { try { const members = await client.request( - readItems('members', { limit: 500 }), + readItems('members_v2', { limit: 500 }), ); const normalize = (input: unknown) => diff --git a/frontend-next-migration/src/entities/Member/api/mappers.ts b/frontend-next-migration/src/entities/Member/api/mappers.ts index dcf5522d7..2db192a8c 100644 --- a/frontend-next-migration/src/entities/Member/api/mappers.ts +++ b/frontend-next-migration/src/entities/Member/api/mappers.ts @@ -1,6 +1,6 @@ import { faGithub, faLinkedin, faInstagram, faFacebook } from '@fortawesome/free-brands-svg-icons'; import { faGlobe, faEnvelope } from '@fortawesome/free-solid-svg-icons'; -import { Member, Team } from '@/entities/Member/model/types/types'; +import { Member, Team, MemberRole, Department } from '@/entities/Member/model/types/types'; import { getDepartmentTranslation, getTeamTranslation, getLanguageCode } from './translations'; /** @@ -81,11 +81,103 @@ const fiDepartmentOrder = [ */ /** - * Organizes members into teams and departments based on their properties, + * Creates or retrieves a team from the teams map. + */ +const getOrCreateTeam = ( + teamsMap: Map, + memberTeam: Team, + fullLanguageCode: string, +): Team => { + let team = teamsMap.get(memberTeam.id); + if (!team) { + const teamName = getTeamTranslation(memberTeam.translations || [], fullLanguageCode); + team = { + id: memberTeam.id, + name: teamName || '', + translations: memberTeam.translations || [], + members: [], + departments: [], + }; + teamsMap.set(memberTeam.id, team); + } + return team; +}; + +/** + * Creates or retrieves a department within a team. + */ +const getOrCreateDepartment = ( + team: Team, + memberDepartment: Department, + fullLanguageCode: string, +): Department => { + let department = team.departments.find( + (departmentItem) => departmentItem.id === memberDepartment.id, + ); + if (!department) { + const departmentName = getDepartmentTranslation( + memberDepartment.translations || [], + fullLanguageCode, + ); + department = { + id: memberDepartment.id, + name: departmentName || '', + translations: memberDepartment.translations || [], + members: [], + }; + team.departments.push(department); + } + return department; +}; + +/** + * Adds a member to a team or department if not already present. + */ +const addMemberToTeamOrDepartment = ( + member: Member, + team: Team, + department: Department | null, +): void => { + if (department) { + if (!department.members.find((memberItem: Member) => memberItem.id === member.id)) { + department.members.push(member); + } + } else { + if (!team.members.find((memberItem: Member) => memberItem.id === member.id)) { + team.members.push(member); + } + } +}; + +/** + * Processes a single role and adds the member to the appropriate team/department. + */ +const processRole = ( + role: MemberRole, + member: Member, + teamsMap: Map, + fullLanguageCode: string, +): void => { + const memberTeam = role.team; + if (!memberTeam) { + return; + } + + const team = getOrCreateTeam(teamsMap, memberTeam, fullLanguageCode); + const memberDepartment = role.department; + const department = memberDepartment + ? getOrCreateDepartment(team, memberDepartment, fullLanguageCode) + : null; + + addMemberToTeamOrDepartment(member, team, department); +}; + +/** + * Organizes members into teams and departments based on their roles, * and sorts both the members alphabetically within their teams and departments, * as well as the teams based on a predefined order dictated by language. * - * @param {Member[]} members - An array of member objects, each containing associated team and department data. + * @param {Member[]} members - An array of member objects, each containing roles with team and department data. * @param {string} lng - The language code used to determine which language to use for translations and sorting. * @returns {OrganizedData} The organized data containing teams mapped by their IDs. */ @@ -98,40 +190,13 @@ export const organizeMembers = (members: Member[], lng: string) => { const departmentOrder = lng === 'fi' ? fiDepartmentOrder : enDepartmentOrder; members.forEach((member: Member) => { - const memberTeam = member.team; - const memberDepartment = member.department; - - if (memberTeam) { - let team = teamsMap.get(memberTeam.id); - if (!team) { - const teamName = getTeamTranslation( - memberTeam.translations || [], - fullLanguageCode, - ); - - team = { ...memberTeam, name: teamName, members: [], departments: [] }; - teamsMap.set(memberTeam.id, team); - } - - if (memberDepartment) { - let department = team.departments.find( - (departmentItem) => departmentItem.id === memberDepartment.id, - ); - if (!department) { - const departmentName = getDepartmentTranslation( - memberDepartment.translations || [], - fullLanguageCode, - ); - - department = { ...memberDepartment, name: departmentName, members: [] }; - team.departments.push(department); - } - - department.members.push(member); - } else { - team.members.push(member); - } + if (!member.roles || member.roles.length === 0) { + return; } + + member.roles.forEach((role: MemberRole) => { + processRole(role, member, teamsMap, fullLanguageCode); + }); }); teamsMap.forEach((team) => { team.members.sort((a, b) => a.name.localeCompare(b.name)); @@ -155,10 +220,36 @@ export const organizeMembers = (members: Member[], lng: string) => { department.members.sort((a, b) => a.name.localeCompare(b.name)); }); }); - const sortedTeams = Array.from(teamsMap.values()).sort((a, b) => { + // Sort teams according to predefined order + // Teams not in the order array go to the end, sorted by name + // Filter out teams with empty names + const validTeams = Array.from(teamsMap.values()).filter( + (team) => team.name && team.name.trim() !== '', + ); + + // Sort teams according to predefined order + // Teams not in the order array go to the end, sorted by name + const sortedTeams = validTeams.sort((a, b) => { const indexA = order.indexOf(a.name); const indexB = order.indexOf(b.name); - return indexA - indexB; + + // Both teams are in the order array - sort by position + if (indexA !== -1 && indexB !== -1) { + return indexA - indexB; + } + + // Team A is in order, Team B is not - A comes first + if (indexA !== -1 && indexB === -1) { + return -1; + } + + // Team B is in order, Team A is not - B comes first + if (indexA === -1 && indexB !== -1) { + return 1; + } + + // Neither team is in order array - sort alphabetically + return a.name.localeCompare(b.name); }); return { teamsMap: new Map(sortedTeams.map((team) => [team.id, team])) }; diff --git a/frontend-next-migration/src/entities/Member/api/memberTeamsApi.ts b/frontend-next-migration/src/entities/Member/api/memberTeamsApi.ts index df7b84520..7781b0cb5 100644 --- a/frontend-next-migration/src/entities/Member/api/memberTeamsApi.ts +++ b/frontend-next-migration/src/entities/Member/api/memberTeamsApi.ts @@ -14,7 +14,7 @@ const client = createDirectus(directusBaseUrl).with(rest()); * @module memberTeamsApi * * @endpoint getMemberTeams - * Endpoint to fetch member teams from the Directus `teams` collection. + * Endpoint to fetch member teams from the Directus `teams_v2` collection. * Retrieves information about teams, including their translations. * * @returns {Record[]} Response containing an array of teams. @@ -25,7 +25,7 @@ const memberTeamsApi = directusApi.injectEndpoints({ getMemberTeams: builder.query({ queryFn: async (_arg: void) => { const teams = await client.request( - readItems('teams', { + readItems('teams_v2', { fields: ['id', 'translations.*'], deep: { translations: true }, }), diff --git a/frontend-next-migration/src/entities/Member/api/membersApi.ts b/frontend-next-migration/src/entities/Member/api/membersApi.ts index 60b16d7a2..d9f0a26b4 100644 --- a/frontend-next-migration/src/entities/Member/api/membersApi.ts +++ b/frontend-next-migration/src/entities/Member/api/membersApi.ts @@ -13,14 +13,15 @@ const membersApi = directusApi.injectEndpoints({ queryFn: async (): Promise<{ data: Member[] } | { error: FetchBaseQueryError }> => { try { const members = await client.request[]>( - readItems('members', { + readItems('members_v2', { fields: [ '*', - 'department.*', - 'department.translations.*', - 'team.*', - 'team.translations.*', - 'translations.*', + 'roles.id', + 'roles.team.*', + 'roles.team.translations.*', + 'roles.department.*', + 'roles.department.translations.*', + 'roles.translations.*', 'logo.*', 'portrait.id', 'portrait.title', @@ -28,6 +29,7 @@ const membersApi = directusApi.injectEndpoints({ limit: 500, }), ); + return { data: members as Member[] }; } catch (error: any) { return { diff --git a/frontend-next-migration/src/entities/Member/api/translations.ts b/frontend-next-migration/src/entities/Member/api/translations.ts index b5f3714f1..fbac703ac 100644 --- a/frontend-next-migration/src/entities/Member/api/translations.ts +++ b/frontend-next-migration/src/entities/Member/api/translations.ts @@ -1,4 +1,9 @@ -import { Translation, DepartmentTranslation, TeamTranslation } from '../model/types/types'; +import { + Translation, + DepartmentTranslation, + TeamTranslation, + RoleTranslation, +} from '../model/types/types'; /** * Returns the language code in a standardized format. @@ -27,24 +32,62 @@ const getTranslation = ( key: keyof T, defaultValue: string = '', ): string => { - const translation = translations.find((t) => t.languages_code === languageCode); - return translation && key in translation ? (translation[key] as string) : defaultValue; + if (!translations || translations.length === 0) { + return defaultValue; + } + + // Try exact match first + let translation = translations.find((t) => t.languages_code === languageCode); + + // If no exact match, try fallback to 'en-US' or first available + if (!translation) { + translation = + translations.find((t) => t.languages_code === 'en-US') || + translations.find((t) => t.languages_code === 'fi-FI') || + translations[0]; + } + + if (translation) { + // Debug: log what keys are available + if (!(key in translation)) { + console.warn('[getTranslation] Key not found in translation:', { + key, + availableKeys: Object.keys(translation), + translation, + languageCode, + }); + } + + if (key in translation) { + const value = translation[key] as string; + return value || defaultValue; + } + } + + return defaultValue; }; export const getTaskTranslation = (translations: Translation[], languageCode: string): string => { return getTranslation(translations, languageCode, 'task', ''); }; +export const getRoleTaskTranslation = ( + translations: RoleTranslation[], + languageCode: string, +): string => { + return getTranslation(translations, languageCode, 'task', ''); +}; + export const getDepartmentTranslation = ( translations: DepartmentTranslation[], languageCode: string, ): string => { - return getTranslation(translations, languageCode, 'department', ''); + return getTranslation(translations, languageCode, 'name', ''); }; export const getTeamTranslation = ( translations: TeamTranslation[], languageCode: string, ): string => { - return getTranslation(translations, languageCode, 'team', ''); + return getTranslation(translations, languageCode, 'name', ''); }; diff --git a/frontend-next-migration/src/entities/Member/model/types/types.ts b/frontend-next-migration/src/entities/Member/model/types/types.ts index 42e643bf5..11bb8c68b 100644 --- a/frontend-next-migration/src/entities/Member/model/types/types.ts +++ b/frontend-next-migration/src/entities/Member/model/types/types.ts @@ -10,7 +10,6 @@ export interface Asset { export interface Member { id: number; name: string; - task?: string; email?: string; logo?: Logo | null; website?: string; @@ -19,12 +18,17 @@ export interface Member { facebook?: string | null; instagram?: string | null; language?: string; - department?: Department | null; - team?: Team | null; - translations?: Translation[]; + roles?: MemberRole[]; portrait?: Asset | null; } +export interface MemberRole { + id: number; + team?: Team | null; + department?: Department | null; + translations?: RoleTranslation[]; +} + export interface Department { id: number; name: string; @@ -42,16 +46,16 @@ export interface Team { export interface DepartmentTranslation { id: number; - departments_id: number; + departments_v2_id: number; languages_code: string; - department: string; + name: string; } export interface TeamTranslation { id: number; - teams_id: number; + teams_v2_id: number; languages_code: string; - team: string; + name: string; } export interface Translation { @@ -60,3 +64,10 @@ export interface Translation { languages_code: string; task?: string; } + +export interface RoleTranslation { + id: number; + members_roles_id: number; + languages_code: string; + task?: string; +} diff --git a/frontend-next-migration/src/entities/Member/ui/MemberItem.tsx b/frontend-next-migration/src/entities/Member/ui/MemberItem.tsx index 93e553a06..47089e162 100644 --- a/frontend-next-migration/src/entities/Member/ui/MemberItem.tsx +++ b/frontend-next-migration/src/entities/Member/ui/MemberItem.tsx @@ -6,7 +6,7 @@ import { getLinks } from '../api/mappers'; import { Member } from '../model/types/types'; import cls from './MemberItem.module.scss'; import { envHelper } from '@/shared/const/envHelper'; -import { getTaskTranslation, getLanguageCode } from '../api/translations'; +import { getRoleTaskTranslation, getLanguageCode } from '../api/translations'; interface MemberItemProps { member: Member; @@ -36,7 +36,11 @@ const MemberItem: FC = ({ member, language }) => { : null; const fullLanguageCode = getLanguageCode(language); - const task = getTaskTranslation(member.translations || [], fullLanguageCode); + // Get task from first role's translations, or fall back to empty string + const task = + member.roles && member.roles.length > 0 && member.roles[0].translations + ? getRoleTaskTranslation(member.roles[0].translations, fullLanguageCode) + : ''; return (
  • From 1e196abb87b13732c888ffe0aa205e5232ceb13d Mon Sep 17 00:00:00 2001 From: patinen Date: Thu, 29 Jan 2026 09:28:32 +0200 Subject: [PATCH 2/2] updated mappers.test to match the refactor --- .../src/entities/Member/api/mappers.test.ts | 104 ++++++++++-------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/frontend-next-migration/src/entities/Member/api/mappers.test.ts b/frontend-next-migration/src/entities/Member/api/mappers.test.ts index 974d9871e..b4487064a 100644 --- a/frontend-next-migration/src/entities/Member/api/mappers.test.ts +++ b/frontend-next-migration/src/entities/Member/api/mappers.test.ts @@ -1,13 +1,13 @@ import { organizeMembers } from './mappers'; -import { Member, Team, Department } from '@/entities/Member/model/types/types'; +import { Member, Team, Department, MemberRole } from '@/entities/Member/model/types/types'; const mockTeams: Team[] = [ { id: 1, name: 'Development', translations: [ - { id: 101, teams_id: 1, languages_code: 'en-US', team: 'Development' }, - { id: 102, teams_id: 1, languages_code: 'fi-FI', team: 'Ohjelmistokehitys' }, + { id: 101, teams_v2_id: 1, languages_code: 'en-US', name: 'Development' }, + { id: 102, teams_v2_id: 1, languages_code: 'fi-FI', name: 'Ohjelmistokehitys' }, ], members: [], departments: [], @@ -16,8 +16,8 @@ const mockTeams: Team[] = [ id: 2, name: 'Game Design', translations: [ - { id: 103, teams_id: 2, languages_code: 'en-US', team: 'Game Design' }, - { id: 104, teams_id: 2, languages_code: 'fi-FI', team: 'Pelisuunnittelu' }, + { id: 103, teams_v2_id: 2, languages_code: 'en-US', name: 'Game Design' }, + { id: 104, teams_v2_id: 2, languages_code: 'fi-FI', name: 'Pelisuunnittelu' }, ], members: [], departments: [], @@ -33,27 +33,32 @@ describe('organizeMembers', () => { github: 'https://github.com/Jonroi', linkedin: 'https://www.linkedin.com/in/joni-roine/', website: 'https://jonroi.netlify.app/', - team: mockTeams[0], - department: { - id: 10, - name: 'Website Developer', - translations: [ - { - id: 110, - departments_id: 10, - languages_code: 'en-US', - department: 'Website Developer', - }, - { - id: 111, - departments_id: 10, - languages_code: 'fi-FI', - department: 'Verkkosivukehittäjä', - }, - ], - members: [], - } as Department, - translations: [], + roles: [ + { + id: 1, + team: mockTeams[0], + department: { + id: 10, + name: 'Website Developer', + translations: [ + { + id: 110, + departments_v2_id: 10, + languages_code: 'en-US', + name: 'Website Developer', + }, + { + id: 111, + departments_v2_id: 10, + languages_code: 'fi-FI', + name: 'Verkkosivukehittäjä', + }, + ], + members: [], + } as Department, + translations: [], + } as MemberRole, + ], }, { id: 2, @@ -62,27 +67,32 @@ describe('organizeMembers', () => { github: 'https://github.com/AliceSmith', linkedin: 'https://www.linkedin.com/in/alice-smith/', website: 'https://alicesmith.com/', - team: mockTeams[1], - department: { - id: 20, - name: 'Game Developer', - translations: [ - { - id: 120, - departments_id: 20, - languages_code: 'en-US', - department: 'Game Developer', - }, - { - id: 121, - departments_id: 20, - languages_code: 'fi-FI', - department: 'Pelikehittäjä', - }, - ], - members: [], - } as Department, - translations: [], + roles: [ + { + id: 2, + team: mockTeams[1], + department: { + id: 20, + name: 'Game Developer', + translations: [ + { + id: 120, + departments_v2_id: 20, + languages_code: 'en-US', + name: 'Game Developer', + }, + { + id: 121, + departments_v2_id: 20, + languages_code: 'fi-FI', + name: 'Pelikehittäjä', + }, + ], + members: [], + } as Department, + translations: [], + } as MemberRole, + ], }, ];