🔧 Restore PRs #14348 and #14352 that were reverted by PR #14347 (#14359)

## Problem

**CRITICAL:** Two PRs were accidentally reverted when PR #14347 "Prevent
csv export injections" was merged:

1. **PR #14348** "[Page Layout] - Review Refactor" -  **RESTORED**
2. **PR #14352** "Fix wrong path used by backend" -  **RESTORED**

## Root Cause Analysis

During the merge of PR #14347, there was a complex merge conflict with
PR #14352 "Fix wrong path used by backend". The merge commit
`324d7204bb` in the PR #14347 branch brought in changes from PR #14352,
but during the conflict resolution, **BOTH PR #14348 and PR #14352's
changes were accidentally overwritten**.

## What This PR Restores

This PR restores **BOTH** PRs by cherry-picking their commits:

###  PR #14348 Changes Restored:
- `GraphWidgetRenderer.tsx` - was deleted, now restored
- `WidgetRenderer.tsx` - was missing, now restored  
- `SettingsPageLayoutTabsInstanceId.ts` - was deleted, now restored
- `useUpdatePageLayoutWidget.ts` - was renamed back, now restored with
correct name
- Multiple test files that were deleted
- Several hook files that were renamed/reverted
- File renames: `usePageLayoutWidgetUpdate.ts` →
`useUpdatePageLayoutWidget.ts`
- Hook refactoring and test file organization
- Page layout component improvements

###  PR #14352 Changes Restored:
- **Types moved to twenty-shared:**
  - `packages/twenty-shared/src/types/AppBasePath.ts`  RESTORED
  - `packages/twenty-shared/src/types/AppPath.ts`  RESTORED
  - `packages/twenty-shared/src/types/SettingsPath.ts`  RESTORED
- **Navigation utilities moved to twenty-shared:**
- `packages/twenty-shared/src/utils/navigation/getAppPath.ts`  RESTORED
- `packages/twenty-shared/src/utils/navigation/getSettingsPath.ts` 
RESTORED
- **200+ import statements updated** across the codebase to use
twenty-shared
- **Old type files deleted** from twenty-front/src/modules/types/

## Evidence of Complete Restoration

**Before (reverted state):**
-  Types were in `packages/twenty-front/src/modules/types/`
-  Page layout files missing
-  Hook files incorrectly named

**After (this PR):**
-  Types correctly in `packages/twenty-shared/src/types/`
-  All page layout files restored
-  Hook files correctly named
-  All import statements updated

## Verification

**Total changes:**
- PR #14348: 36 files changed, 863 insertions(+), 442 deletions(-)
- PR #14352: 243 files changed, 492 insertions(+), 461 deletions(-)
- **Combined: 279 files changed, 1355 insertions(+), 903 deletions(-)**

## Impact

This completely restores both PRs that were accidentally lost, ensuring:
1. Page layout refactoring work is back
2. Type organization and path utilities are correctly in twenty-shared
3. Backend email paths work correctly again
4. No functionality is lost

Fixes the reversion caused by the merge conflict in PR #14347.

---------

Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2025-09-08 21:48:13 +02:00
committed by GitHub
parent ebc48e3bb2
commit f7cde28dd6
277 changed files with 1354 additions and 906 deletions
@@ -0,0 +1,110 @@
import { AppPath } from '../../../types';
import { getAppPath } from '../getAppPath';
describe('getAppPath', () => {
it('should return path as-is when no params or query params provided', () => {
expect(getAppPath(AppPath.Index)).toBe('/');
expect(getAppPath(AppPath.TasksPage)).toBe('/objects/tasks');
expect(getAppPath(AppPath.SignInUp)).toBe('/welcome');
});
it('should generate path with params using react-router generatePath', () => {
expect(
getAppPath(AppPath.RecordIndexPage, {
objectNamePlural: 'companies',
}),
).toBe('/objects/companies');
expect(
getAppPath(AppPath.RecordShowPage, {
objectNameSingular: 'company',
objectRecordId: '123',
}),
).toBe('/object/company/123');
expect(
getAppPath(AppPath.Invite, {
workspaceInviteHash: 'abc123',
}),
).toBe('/invite/abc123');
});
it('should append query params when provided', () => {
expect(
getAppPath(AppPath.Index, undefined, {
filter: 'active',
sort: 'name',
}),
).toBe('/?filter=active&sort=name');
expect(
getAppPath(AppPath.TasksPage, undefined, {
view: 'kanban',
}),
).toBe('/objects/tasks?view=kanban');
});
it('should handle both params and query params together', () => {
expect(
getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: 'companies' },
{ filter: 'active', view: 'table' },
),
).toBe('/objects/companies?filter=active&view=table');
expect(
getAppPath(
AppPath.RecordShowPage,
{ objectNameSingular: 'company', objectRecordId: '123' },
{ tab: 'details' },
),
).toBe('/object/company/123?tab=details');
});
it('should filter out null and undefined values from query params', () => {
expect(
getAppPath(AppPath.Index, undefined, {
filter: 'active',
sort: null,
view: undefined,
limit: '10',
}),
).toBe('/?filter=active&limit=10');
});
it('should handle empty query params object', () => {
expect(getAppPath(AppPath.Index, undefined, {})).toBe('/');
});
it('should handle query params with only null/undefined values', () => {
expect(
getAppPath(AppPath.Index, undefined, {
filter: null,
sort: undefined,
}),
).toBe('/');
});
it('should handle complex query param values', () => {
expect(
getAppPath(AppPath.Index, undefined, {
filters: JSON.stringify({ status: 'active' }),
array: ['value1', 'value2'],
number: 42,
boolean: true,
}),
).toBe(
'/?filters=%7B%22status%22%3A%22active%22%7D&array%5B0%5D=value1&array%5B1%5D=value2&number=42&boolean=true',
);
});
it('should handle special characters in query params', () => {
expect(
getAppPath(AppPath.Index, undefined, {
search: 'test & query',
email: 'user@example.com',
}),
).toBe('/?search=test%20%26%20query&email=user%40example.com');
});
});
@@ -0,0 +1,148 @@
import { SettingsPath } from '../../../types';
import { getSettingsPath } from '../getSettingsPath';
describe('getSettingsPath', () => {
it('should return settings path with correct prefix when no params or query params provided', () => {
expect(getSettingsPath(SettingsPath.ProfilePage)).toBe('/settings/profile');
expect(getSettingsPath(SettingsPath.Workspace)).toBe('/settings/general');
expect(getSettingsPath(SettingsPath.Accounts)).toBe('/settings/accounts');
});
it('should generate path with params using react-router generatePath', () => {
expect(
getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: 'companies',
}),
).toBe('/settings/objects/companies');
expect(
getSettingsPath(SettingsPath.ObjectFieldEdit, {
objectNamePlural: 'companies',
fieldName: 'name',
}),
).toBe('/settings/objects/companies/name');
expect(
getSettingsPath(SettingsPath.ServerlessFunctionDetail, {
serverlessFunctionId: 'func123',
}),
).toBe('/settings/functions/func123');
expect(
getSettingsPath(SettingsPath.EditImapSmtpCaldavConnection, {
connectedAccountId: 'account123',
}),
).toBe('/settings/accounts/edit-imap-smtp-caldav-connection/account123');
});
it('should append query params when provided', () => {
expect(
getSettingsPath(SettingsPath.ProfilePage, undefined, {
tab: 'personal',
edit: 'true',
}),
).toBe('/settings/profile?tab=personal&edit=true');
expect(
getSettingsPath(SettingsPath.Objects, undefined, {
view: 'list',
}),
).toBe('/settings/objects?view=list');
});
it('should append hash when provided', () => {
expect(
getSettingsPath(
SettingsPath.ProfilePage,
undefined,
undefined,
'section1',
),
).toBe('/settings/profile#section1');
expect(
getSettingsPath(SettingsPath.Workspace, undefined, undefined, 'general'),
).toBe('/settings/general#general');
});
it('should handle hash with leading # character', () => {
expect(
getSettingsPath(
SettingsPath.ProfilePage,
undefined,
undefined,
'#section1',
),
).toBe('/settings/profile#section1');
});
it('should handle params, query params, and hash together', () => {
expect(
getSettingsPath(
SettingsPath.ObjectDetail,
{ objectNamePlural: 'companies' },
{ tab: 'fields', edit: 'true' },
'advanced',
),
).toBe('/settings/objects/companies?tab=fields&edit=true#advanced');
expect(
getSettingsPath(
SettingsPath.ServerlessFunctionDetail,
{ serverlessFunctionId: 'func123' },
{ mode: 'edit' },
'code',
),
).toBe('/settings/functions/func123?mode=edit#code');
});
it('should filter out null and undefined values from query params', () => {
expect(
getSettingsPath(SettingsPath.ProfilePage, undefined, {
tab: 'personal',
edit: null,
view: undefined,
active: 'true',
}),
).toBe('/settings/profile?tab=personal&active=true');
});
it('should handle empty query params object', () => {
expect(getSettingsPath(SettingsPath.ProfilePage, undefined, {})).toBe(
'/settings/profile',
);
});
it('should handle query params with only null/undefined values', () => {
expect(
getSettingsPath(SettingsPath.ProfilePage, undefined, {
tab: null,
edit: undefined,
}),
).toBe('/settings/profile');
});
it('should handle complex query param values', () => {
expect(
getSettingsPath(SettingsPath.Objects, undefined, {
filters: JSON.stringify({ type: 'custom' }),
sort: ['name', 'created'],
page: 1,
enabled: true,
}),
).toBe(
'/settings/objects?filters=%7B%22type%22%3A%22custom%22%7D&sort%5B0%5D=name&sort%5B1%5D=created&page=1&enabled=true',
);
});
it('should handle special characters in query params', () => {
expect(
getSettingsPath(SettingsPath.ProfilePage, undefined, {
search: 'test & query',
email: 'user@example.com',
}),
).toBe(
'/settings/profile?search=test%20%26%20query&email=user%40example.com',
);
});
});
@@ -0,0 +1,30 @@
import qs from 'qs';
import { generatePath, type PathParam } from 'react-router-dom';
import { type AppPath } from '../../types';
import { isDefined } from '../validation';
export const getAppPath = <T extends AppPath>(
to: T,
params?: { [key in PathParam<T>]: string | null },
queryParams?: Record<string, any>,
) => {
let path: string = to;
if (isDefined(params)) {
path = generatePath<T>(to, params);
}
if (isDefined(queryParams)) {
const filteredParams = Object.fromEntries(
Object.entries(queryParams).filter(([_, value]) => isDefined(value)),
);
const queryString = qs.stringify(filteredParams);
if (queryString !== '') {
path += `?${queryString}`;
}
}
return path;
};
@@ -0,0 +1,40 @@
import qs from 'qs';
import { generatePath, type PathParam } from 'react-router-dom';
import { AppPath, type SettingsPath } from '../../types';
import { isDefined } from '../validation';
export const getSettingsPath = <T extends SettingsPath>(
to: T,
params?: {
[key in PathParam<`/${AppPath.Settings}/${T}`>]: string | null;
},
queryParams?: Record<string, any>,
hash?: string,
) => {
let path = `/${AppPath.Settings}/${to}`;
if (isDefined(params)) {
path = generatePath<`/${AppPath.Settings}/${T}`>(
`/${AppPath.Settings}/${to}`,
params,
);
}
if (isDefined(queryParams)) {
const filteredParams = Object.fromEntries(
Object.entries(queryParams).filter(([_, value]) => isDefined(value)),
);
const queryString = qs.stringify(filteredParams);
if (queryString !== '') {
path += `?${queryString}`;
}
}
if (isDefined(hash)) {
path += `#${hash.replace(/^#/, '')}`;
}
return path;
};