Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 9b1e6d5a9d fix(views): handle missing object metadata in useFiltersFromQueryParams
https://sonarly.com/issue/33231?type=bug

When a user navigates to a deleted custom object URL (e.g., `/objects/accommodations`), the `QueryParamsFiltersEffect` component crashes because `useFiltersFromQueryParams` reads the object name from URL params and calls `useObjectNameSingularFromPlural` which throws for non-existent objects, before the page-level redirect to `/not-found` takes effect.
2026-05-01 04:51:24 +00:00
10 changed files with 68 additions and 72 deletions
@@ -4,6 +4,6 @@ set -e
echo "==> START Registering cron jobs"
cd /app/packages/twenty-server
yarn command:prod cron:register:all
yarn command:prod cron:register:all --dev-mode
echo "==> DONE"
@@ -1,8 +1,6 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -18,10 +16,10 @@ export const QueryParamsFiltersEffect = () => {
useFiltersFromQueryParams();
const { hasFiltersQueryParams } = useHasFiltersInQueryParams();
const { objectNamePlural = '' } = useParams();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
// Use objectNameSingular from validated RecordIndex context instead of raw
// URL params to avoid crashes when navigating to non-existent objects
const { objectNameSingular, recordIndexId } =
useRecordIndexContextOrThrow();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
@@ -30,7 +28,6 @@ export const QueryParamsFiltersEffect = () => {
const { mapViewFiltersToRecordFilters } = useMapViewFiltersToFilters();
const { recordIndexId } = useRecordIndexContextOrThrow();
const setCurrentRecordFilters = useSetAtomComponentState(
currentRecordFiltersComponentState,
recordIndexId,
@@ -2,7 +2,6 @@ import { isNonEmptyString, isObject } from '@sniptt/guards';
import qs from 'qs';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
@@ -10,17 +9,18 @@ import { filterUrlQueryParamsSchema } from '@/views/schemas/filterUrlQueryParams
import { type ViewFilter } from '@/views/types/ViewFilter';
import { deserializeUrlRecursiveFilterGroup } from '@/views/utils/deserializeUrlRecursiveFilterGroup';
import { splitFieldNameIntoBaseAndSubField } from '@/views/utils/splitFieldNameIntoBaseAndSubField';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useCallback } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import { type ViewFilterOperand } from 'twenty-shared/types';
import { isDefined, isExpectedSubFieldName } from 'twenty-shared/utils';
export const useFiltersFromQueryParams = () => {
const [searchParams] = useSearchParams();
const { objectNamePlural = '' } = useParams();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
// Use objectNameSingular from validated RecordIndex context instead of raw
// URL params to avoid crashes when navigating to non-existent objects
const { objectNameSingular } = useRecordIndexContextOrThrow();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
@@ -1,6 +1,6 @@
import { Logger } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { Command, CommandRunner, Option } from 'nest-commander';
import { MarketplaceCatalogSyncCronCommand } from 'src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.cron.command';
import { StaleRegistrationCleanupCronCommand } from 'src/engine/core-modules/application/application-oauth/stale-registration-cleanup/commands/stale-registration-cleanup.cron.command';
@@ -68,8 +68,33 @@ export class CronRegisterAllCommand extends CommandRunner {
super();
}
private devMode = false;
@Option({
flags: '--dev-mode',
description:
'Only register cron jobs relevant to app development (cron triggers, marketplace sync, version check, stale cleanup)',
required: false,
})
parseDevMode(): boolean {
this.devMode = true;
return true;
}
private static readonly DEV_MODE_COMMANDS = new Set([
'CronTrigger',
'MarketplaceCatalogSync',
'ApplicationVersionCheck',
'StaleRegistrationCleanup',
]);
async run(): Promise<void> {
this.logger.log('Registering all background sync cron jobs...');
this.logger.log(
this.devMode
? 'Registering app-dev cron jobs...'
: 'Registering all background sync cron jobs...',
);
const allCommands = [
{
@@ -170,12 +195,18 @@ export class CronRegisterAllCommand extends CommandRunner {
},
];
const commands = this.devMode
? allCommands.filter(({ name }) =>
CronRegisterAllCommand.DEV_MODE_COMMANDS.has(name),
)
: allCommands;
let successCount = 0;
let failureCount = 0;
const failures: string[] = [];
const successes: string[] = [];
for (const { name, command } of allCommands) {
for (const { name, command } of commands) {
try {
this.logger.log(`Registering ${name} cron job...`);
await command.run();
@@ -7,7 +7,6 @@ export const WORKFLOW_SYSTEM_PROMPTS = {
Tool usage strategy:
- Chain multiple tools to solve complex tasks
- Prefer batch tools (\`create_many_*\`, \`update_many_*\`, etc.) over looping single-item calls
- If a tool fails, try alternative approaches
- Use results from one tool to inform the next
- Don't give up after first failure - be persistent
@@ -41,7 +41,6 @@ For simple CRUD operations (find/create/update/delete a record), you do NOT need
- Always apply filters to narrow results — don't fetch all records of a type.
- Fetch one type of data at a time and check if you have what you need before fetching more.
- Every record returned consumes context. Fetching too many records at once will cause failures.
- For multiple items of the same type, use batch tools (\`create_many_*\`, \`update_many_*\`, etc.) instead of looping single-item calls.
## Tool Strategy
@@ -1304,17 +1304,6 @@
"contextWindowTokens": 64000,
"maxOutputTokens": 64000
},
{
"name": "mistral-medium-2604",
"label": "Mistral Medium 3.5",
"modelFamily": "MISTRAL",
"inputCostPerMillionTokens": 1.5,
"outputCostPerMillionTokens": 7.5,
"contextWindowTokens": 262144,
"maxOutputTokens": 262144,
"modalities": ["image"],
"supportsReasoning": true
},
{
"name": "devstral-small-2505",
"label": "Devstral Small 2505",
@@ -1333,6 +1322,16 @@
"contextWindowTokens": 128000,
"maxOutputTokens": 128000
},
{
"name": "mistral-medium-latest",
"label": "Mistral Medium (latest)",
"modelFamily": "MISTRAL",
"inputCostPerMillionTokens": 0.4,
"outputCostPerMillionTokens": 2,
"contextWindowTokens": 128000,
"maxOutputTokens": 16384,
"modalities": ["image"]
},
{
"name": "open-mistral-7b",
"label": "Mistral 7B",
@@ -1379,17 +1378,6 @@
"contextWindowTokens": 262144,
"maxOutputTokens": 262144,
"modalities": ["image"]
},
{
"name": "mistral-medium-latest",
"label": "Mistral Medium (latest)",
"modelFamily": "MISTRAL",
"inputCostPerMillionTokens": 1.5,
"outputCostPerMillionTokens": 7.5,
"contextWindowTokens": 262144,
"maxOutputTokens": 262144,
"modalities": ["image"],
"supportsReasoning": true
}
]
},
@@ -1419,24 +1407,6 @@
"maxOutputTokens": 4096,
"modalities": ["image"]
},
{
"name": "grok-4.3",
"label": "Grok 4.3",
"modelFamily": "GROK",
"inputCostPerMillionTokens": 1.25,
"outputCostPerMillionTokens": 2.5,
"cachedInputCostPerMillionTokens": 0.2,
"longContextCost": {
"inputCostPerMillionTokens": 2.5,
"outputCostPerMillionTokens": 5,
"thresholdTokens": 200000,
"cachedInputCostPerMillionTokens": 0.4
},
"contextWindowTokens": 1000000,
"maxOutputTokens": 30000,
"modalities": ["image"],
"supportsReasoning": true
},
{
"name": "grok-3-mini-fast",
"label": "Grok 3 Mini Fast",
@@ -42,7 +42,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
handle: 'tim@apple.dev',
visibility: CalendarChannelVisibility.METADATA,
isContactAutoCreationEnabled: true,
isSyncEnabled: false,
isSyncEnabled: true,
},
{
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JONY,
@@ -50,7 +50,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
handle: 'jony@apple.dev',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
isContactAutoCreationEnabled: true,
isSyncEnabled: false,
isSyncEnabled: true,
},
{
id: CALENDAR_CHANNEL_DATA_SEED_IDS.PHIL,
@@ -58,7 +58,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
handle: 'phil@apple.dev',
visibility: CalendarChannelVisibility.METADATA,
isContactAutoCreationEnabled: true,
isSyncEnabled: false,
isSyncEnabled: true,
},
{
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE,
@@ -66,7 +66,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
handle: 'jane.austen@apple.dev',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
isContactAutoCreationEnabled: true,
isSyncEnabled: false,
isSyncEnabled: true,
},
{
id: CALENDAR_CHANNEL_DATA_SEED_IDS.COMPANY_MAIN,
@@ -74,7 +74,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
handle: 'company-main@apple.dev',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
isContactAutoCreationEnabled: true,
isSyncEnabled: false,
isSyncEnabled: true,
},
{
id: CALENDAR_CHANNEL_DATA_SEED_IDS.TEAM_CALENDAR,
@@ -82,6 +82,6 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
handle: 'team-calendar@apple.dev',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
isContactAutoCreationEnabled: true,
isSyncEnabled: false,
isSyncEnabled: true,
},
];
@@ -59,7 +59,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
type: MessageChannelType.EMAIL,
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
handle: 'tim@apple.dev',
isSyncEnabled: false,
isSyncEnabled: true,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
},
@@ -72,7 +72,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
type: MessageChannelType.EMAIL,
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
handle: 'jony.ive@apple.dev',
isSyncEnabled: false,
isSyncEnabled: true,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
},
@@ -85,7 +85,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
type: MessageChannelType.EMAIL,
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.PHIL,
handle: 'phil.schiler@apple.dev',
isSyncEnabled: false,
isSyncEnabled: true,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
},
@@ -98,7 +98,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
type: MessageChannelType.EMAIL,
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
handle: 'jane.austen@apple.dev',
isSyncEnabled: false,
isSyncEnabled: true,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
},
@@ -111,7 +111,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
type: MessageChannelType.EMAIL,
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, // Use TIM's connected account for shared inbox
handle: 'support@apple.dev',
isSyncEnabled: false,
isSyncEnabled: true,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
},
@@ -124,7 +124,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
type: MessageChannelType.EMAIL,
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, // Use TIM's connected account for shared inbox
handle: 'sales@apple.dev',
isSyncEnabled: false,
isSyncEnabled: true,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
},
@@ -41,7 +41,7 @@ export class MessagingBlocklistListener {
);
}
@OnDatabaseBatchEvent('blocklist', DatabaseEventAction.DELETED)
@OnDatabaseBatchEvent('blocklist', DatabaseEventAction.CREATED)
async handleDeletedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<BlocklistWorkspaceEntity>