## 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:
@@ -0,0 +1,5 @@
|
||||
export enum AppBasePath {
|
||||
Auth = '/auth',
|
||||
Settings = '/settings',
|
||||
Root = '/',
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export enum AppPath {
|
||||
// Not logged-in
|
||||
Verify = '/verify',
|
||||
VerifyEmail = '/verify-email',
|
||||
SignInUp = '/welcome',
|
||||
Invite = '/invite/:workspaceInviteHash',
|
||||
ResetPassword = '/reset-password/:passwordResetToken',
|
||||
|
||||
// Onboarding
|
||||
CreateWorkspace = '/create/workspace',
|
||||
CreateProfile = '/create/profile',
|
||||
SyncEmails = '/sync/emails',
|
||||
InviteTeam = '/invite-team',
|
||||
PlanRequired = '/plan-required',
|
||||
PlanRequiredSuccess = '/plan-required/payment-success',
|
||||
BookCallDecision = '/book-call-decision',
|
||||
BookCall = '/book-call',
|
||||
|
||||
// Onboarded
|
||||
Index = '/',
|
||||
TasksPage = '/objects/tasks',
|
||||
OpportunitiesPage = '/objects/opportunities',
|
||||
|
||||
RecordIndexPage = '/objects/:objectNamePlural',
|
||||
RecordShowPage = '/object/:objectNameSingular/:objectRecordId',
|
||||
|
||||
Settings = `settings`,
|
||||
SettingsCatchAll = `/${Settings}/*`,
|
||||
Developers = `developers`,
|
||||
DevelopersCatchAll = `/${Developers}/*`,
|
||||
|
||||
Authorize = '/authorize',
|
||||
|
||||
// 404 page not found
|
||||
NotFoundWildcard = '*',
|
||||
NotFound = '/not-found',
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
export enum SettingsPath {
|
||||
ProfilePage = 'profile',
|
||||
TwoFactorAuthenticationStrategyConfig = 'profile/two-factor-authentication/:twoFactorAuthenticationStrategy',
|
||||
Experience = 'experience',
|
||||
Accounts = 'accounts',
|
||||
NewAccount = 'accounts/new',
|
||||
AccountsCalendars = 'accounts/calendars',
|
||||
AccountsEmails = 'accounts/emails',
|
||||
NewImapSmtpCaldavConnection = 'accounts/new-imap-smtp-caldav-connection',
|
||||
EditImapSmtpCaldavConnection = 'accounts/edit-imap-smtp-caldav-connection/:connectedAccountId',
|
||||
Billing = 'billing',
|
||||
Objects = 'objects',
|
||||
ObjectOverview = 'objects/overview',
|
||||
ObjectDetail = 'objects/:objectNamePlural',
|
||||
ObjectNewFieldSelect = 'objects/:objectNamePlural/new-field/select',
|
||||
ObjectNewFieldConfigure = 'objects/:objectNamePlural/new-field/configure',
|
||||
ObjectFieldEdit = 'objects/:objectNamePlural/:fieldName',
|
||||
NewObject = 'objects/new',
|
||||
ServerlessFunctions = 'functions',
|
||||
NewServerlessFunction = 'functions/new',
|
||||
ServerlessFunctionDetail = 'functions/:serverlessFunctionId',
|
||||
WorkspaceMembersPage = 'members',
|
||||
Workspace = 'general',
|
||||
Domains = 'domains',
|
||||
Domain = 'domains/domain',
|
||||
NewApprovedAccessDomain = 'domains/approved-access-domain/new',
|
||||
Releases = 'releases',
|
||||
AI = 'ai',
|
||||
AINewAgent = 'ai/new-agent',
|
||||
AIAgentDetail = 'ai/agents/:agentId',
|
||||
ApiWebhooks = 'api-webhooks',
|
||||
RestPlayground = 'playground/rest/:schema',
|
||||
GraphQLPlayground = 'playground/graphql/:schema',
|
||||
NewApiKey = 'api-webhooks/apis/new',
|
||||
ApiKeyDetail = 'api-webhooks/apis/:apiKeyId',
|
||||
NewWebhook = 'api-webhooks/webhooks/new',
|
||||
WebhookDetail = 'api-webhooks/webhooks/:webhookId',
|
||||
Integrations = 'integrations',
|
||||
IntegrationMCP = 'integrations/mcp',
|
||||
IntegrationDatabase = 'integrations/:databaseKey',
|
||||
IntegrationDatabaseConnection = 'integrations/:databaseKey/:connectionId',
|
||||
IntegrationEditDatabaseConnection = 'integrations/:databaseKey/:connectionId/edit',
|
||||
IntegrationNewDatabaseConnection = 'integrations/:databaseKey/new',
|
||||
Security = 'security',
|
||||
NewSSOIdentityProvider = 'security/sso/new',
|
||||
|
||||
AdminPanel = 'admin-panel',
|
||||
AdminPanelHealthStatus = 'admin-panel#health-status',
|
||||
AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
|
||||
AdminPanelConfigVariableDetails = 'admin-panel/config-variables/:variableName',
|
||||
|
||||
Roles = 'roles',
|
||||
RoleCreate = 'roles/create',
|
||||
RoleDetail = 'roles/:roleId',
|
||||
RoleObjectLevel = 'roles/:roleId/object/:objectMetadataId',
|
||||
RoleAddObjectLevel = 'roles/:roleId/add-object-permission',
|
||||
PageLayout = 'page-layout',
|
||||
PageLayoutNew = 'page-layout/new',
|
||||
PageLayoutEdit = 'page-layout/:id',
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
export type { AllowedAddressSubField } from './AddressFieldsType';
|
||||
export { ALLOWED_ADDRESS_SUBFIELDS } from './AddressFieldsType';
|
||||
export { AppBasePath } from './AppBasePath';
|
||||
export { AppPath } from './AppPath';
|
||||
export type { ConfigVariableValue } from './ConfigVariableValue';
|
||||
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
|
||||
export type { EnumFieldMetadataType } from './EnumFieldMetadataType';
|
||||
@@ -34,6 +36,7 @@ export type {
|
||||
export { DEFAULT_RELATIVE_DATE_VALUE } from './RelativeDateValue';
|
||||
export type { RestrictedFieldPermissions } from './RestrictedFieldPermissions';
|
||||
export type { RestrictedFieldsPermissions } from './RestrictedFieldsPermissions';
|
||||
export { SettingsPath } from './SettingsPath';
|
||||
export type { StepFilterGroup, StepFilter } from './StepFilters';
|
||||
export { StepLogicalOperator } from './StepFilters';
|
||||
export { TwoFactorAuthenticationStrategy } from './TwoFactorAuthenticationStrategy';
|
||||
|
||||
@@ -24,6 +24,8 @@ export {
|
||||
getLogoUrlFromDomainName,
|
||||
} from './image/getLogoUrlFromDomainName';
|
||||
export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields';
|
||||
export { getAppPath } from './navigation/getAppPath';
|
||||
export { getSettingsPath } from './navigation/getSettingsPath';
|
||||
export { parseJson } from './parseJson';
|
||||
export { removePropertiesFromRecord } from './removePropertiesFromRecord';
|
||||
export { removeUndefinedFields } from './removeUndefinedFields';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user