Skip to content

Commit 41b2ee1

Browse files
authored
Merge pull request #2399 from teableio/sync/ee-20260107-053427
[sync] fix: coerce conditional lookup number values to proper type T1582
2 parents 32d7661 + c85eb56 commit 41b2ee1

File tree

28 files changed

+2220
-704
lines changed

28 files changed

+2220
-704
lines changed

apps/nestjs-backend/src/features/ai/ai.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ export class AiService {
355355
lg: await this.getModelInstance(chatModel?.lg, llmProviders),
356356
ability: chatModel?.ability,
357357
isInstance: lgProvider.isInstance,
358+
lgModelKey: chatModel.lg,
358359
};
359360
}
360361
}

apps/nestjs-backend/src/features/field/model/field-dto/number-field.dto.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NumberFieldCore } from '@teable/core';
1+
import { NumberFieldCore, parseStringToNumber } from '@teable/core';
22
import type { FieldBase } from '../field-base';
33

44
export class NumberFieldDto extends NumberFieldCore implements FieldBase {
@@ -15,8 +15,21 @@ export class NumberFieldDto extends NumberFieldCore implements FieldBase {
1515

1616
convertDBValue2CellValue(value: unknown): unknown {
1717
if (this.isMultipleCellValue) {
18-
return value == null || typeof value === 'object' ? value : JSON.parse(value as string);
18+
const parsed =
19+
value == null || typeof value === 'object' ? value : JSON.parse(value as string);
20+
if (Array.isArray(parsed)) {
21+
return parsed.map((item) => this.coerceNumber(item));
22+
}
23+
return parsed;
1924
}
20-
return value;
25+
return this.coerceNumber(value);
26+
}
27+
28+
private coerceNumber(value: unknown): unknown {
29+
if (typeof value !== 'string') {
30+
return value;
31+
}
32+
const parsed = parseStringToNumber(value, this.options.formatting);
33+
return parsed ?? value;
2134
}
2235
}

apps/nestjs-backend/src/features/record/query-builder/field-cte-visitor.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
type FieldCore,
4141
type IRollupFieldOptions,
4242
DbFieldType,
43+
CellValueType,
4344
extractFieldIdsFromFilter,
4445
SortFunc,
4546
isFieldReferenceValue,
@@ -1487,6 +1488,32 @@ export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
14871488
return this.unwrapSelectName(targetSelect);
14881489
}
14891490

1491+
private coerceConditionalLookupTargetExpression(
1492+
expression: string,
1493+
targetField: FieldCore
1494+
): string {
1495+
if (targetField.isConditionalLookup || targetField.isMultipleCellValue) {
1496+
return expression;
1497+
}
1498+
if (targetField.cellValueType === CellValueType.Number) {
1499+
if (this.dbProvider.driver === DriverClient.Pg) {
1500+
return `(${expression})::double precision`;
1501+
}
1502+
if (this.dbProvider.driver === DriverClient.Sqlite) {
1503+
return `CAST(${expression} AS NUMERIC)`;
1504+
}
1505+
}
1506+
if (targetField.cellValueType === CellValueType.Boolean) {
1507+
if (this.dbProvider.driver === DriverClient.Pg) {
1508+
return `(${expression})::boolean`;
1509+
}
1510+
if (this.dbProvider.driver === DriverClient.Sqlite) {
1511+
return `CAST(${expression} AS NUMERIC)`;
1512+
}
1513+
}
1514+
return expression;
1515+
}
1516+
14901517
private generateConditionalRollupFieldCte(field: ConditionalRollupFieldCore): void {
14911518
this.generateConditionalRollupFieldCteForScope(this.table, field);
14921519
}
@@ -1543,6 +1570,10 @@ export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
15431570
foreignAliasUsed,
15441571
selectVisitor
15451572
);
1573+
const normalizedExpression = this.coerceConditionalLookupTargetExpression(
1574+
rawExpression,
1575+
targetField
1576+
);
15461577
const formattingVisitor = new FieldFormattingVisitor(rawExpression, this.dialect);
15471578
const formattedExpression = targetField.accept(formattingVisitor);
15481579

@@ -1852,8 +1883,12 @@ export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
18521883

18531884
joinLinkDependencies(aggregateBase);
18541885

1886+
const normalizedExpression = this.coerceConditionalLookupTargetExpression(
1887+
rawExpression,
1888+
targetField
1889+
);
18551890
const targetValueAlias = `__cl_target_${field.id}`;
1856-
aggregateBase.select(this.qb.client.raw(`${rawExpression} as "${targetValueAlias}"`));
1891+
aggregateBase.select(this.qb.client.raw(`${normalizedExpression} as "${targetValueAlias}"`));
18571892
const projectedTargetExpr = `"${foreignAliasUsed}"."${targetValueAlias}"`;
18581893

18591894
let orderByClause: string | undefined;

apps/nestjs-backend/src/types/i18n.generated.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,13 +839,38 @@ export type I18nTranslations = {
839839
"model": string;
840840
"inputRate": string;
841841
"outputRate": string;
842+
"inputRateTip": string;
843+
"outputRateTip": string;
844+
"rateExplanationTitle": string;
845+
"rateExplanationFormula": string;
846+
"rateExplanationExample": string;
842847
"ratesDescription": string;
848+
"advancedRates": string;
849+
"advancedRatesDescription": string;
850+
"cacheRead": string;
851+
"cacheWrite": string;
852+
"reasoning": string;
853+
"perImage": string;
854+
"cacheReadRateTip": string;
855+
"cacheWriteRateTip": string;
856+
"reasoningRateTip": string;
857+
"imageRateTip": string;
843858
"imageModel": string;
844859
"imageGeneration": string;
845860
"imageToImage": string;
846861
"clickToToggleImageModel": string;
847862
"markedAsImageModel": string;
848863
"markedAsTextModel": string;
864+
"fetchPricing": string;
865+
"fetchPricingTip": string;
866+
"fetchPricingError": string;
867+
"pricingPreview": string;
868+
"pricingPreviewDesc": string;
869+
"openRouterId": string;
870+
"notFound": string;
871+
"applyPricing": string;
872+
"pricingApplied": string;
873+
"pricingAppliedCount": string;
849874
"hint": {
850875
"title": string;
851876
"missingV1Suffix": string;
@@ -1120,6 +1145,14 @@ export type I18nTranslations = {
11201145
"title": string;
11211146
"message": string;
11221147
};
1148+
"cancelled": {
1149+
"title": string;
1150+
"rateLimit": string;
1151+
"creditExhausted": string;
1152+
"authFailed": string;
1153+
"serviceUnavailable": string;
1154+
"unknown": string;
1155+
};
11231156
};
11241157
};
11251158
};
@@ -2993,8 +3026,15 @@ export type I18nTranslations = {
29933026
"tokenCreatedSuccess": string;
29943027
"copied": string;
29953028
"copy": string;
3029+
"copyAIDoc": string;
3030+
"aiDocPreview": string;
3031+
"manageToken": string;
3032+
"openInNewTab": string;
29963033
"advancedDesc": string;
29973034
"openAdvanced": string;
3035+
"queryBuilderTitle": string;
3036+
"queryBuilderDesc": string;
3037+
"viewApiDocs": string;
29983038
};
29993039
"personalView": {
30003040
"personal": string;
@@ -3353,6 +3393,18 @@ export type I18nTranslations = {
33533393
"saveConfigOnly": string;
33543394
"generate": string;
33553395
"generateFailed": string;
3396+
"generateMode": string;
3397+
"emptyOnlyMode": string;
3398+
"emptyOnlyModeDesc": string;
3399+
"allMode": string;
3400+
"allModeDesc": string;
3401+
"saveOnlyMode": string;
3402+
"saveOnlyModeDesc": string;
3403+
"fillEmptyCells": string;
3404+
"generateAll": string;
3405+
"recommended": string;
3406+
"taskLimited": string;
3407+
"limitWarning": string;
33563408
};
33573409
"action": {
33583410
"addAttachment": string;
@@ -3858,6 +3910,7 @@ export type I18nTranslations = {
38583910
"tokens": string;
38593911
"totalTimeCost": string;
38603912
"totalCreditCost": string;
3913+
"customModel": string;
38613914
};
38623915
"tools": {
38633916
"getTeableApi": string;

apps/nestjs-backend/test/conditional-lookup.e2e-spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2753,6 +2753,8 @@ describe('OpenAPI Conditional Lookup field (e2e)', () => {
27532753
it('retrieves filtered values and mirrors formatting', async () => {
27542754
const hostRecord = await getRecord(host.id, host.records[0].id);
27552755
expect(hostRecord.fields[conditionalLookupMirrorField.id]).toEqual([5, 4]);
2756+
const lookupValues = hostRecord.fields[conditionalLookupMirrorField.id] as unknown[];
2757+
expect(lookupValues.every((value) => typeof value === 'number')).toBe(true);
27562758

27572759
const hostFieldDetail = await getField(host.id, conditionalLookupMirrorField.id);
27582760
const foreignFieldDetail = await getField(products.id, supplierRatingConditionalLookup.id);

apps/nextjs-app/src/features/app/blocks/admin/setting/components/ai-config/AIProviderCard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface IAIProviderCardProps {
2020
onToggleImageModel?: (modelKey: string, isImageModel: boolean) => void;
2121
onTestProvider?: (provider: LLMProvider) => void;
2222
testingProviders?: Set<string>;
23+
/** Hide model rates config (for space-level settings where billing doesn't apply) */
24+
hideModelRates?: boolean;
2325
}
2426

2527
export const AIProviderCard = ({
@@ -30,6 +32,7 @@ export const AIProviderCard = ({
3032
onToggleImageModel,
3133
onTestProvider,
3234
testingProviders,
35+
hideModelRates,
3336
}: IAIProviderCardProps) => {
3437
return (
3538
<Card className="pt-6 shadow-sm">
@@ -48,6 +51,7 @@ export const AIProviderCard = ({
4851
onToggleImageModel={onToggleImageModel}
4952
onTestProvider={onTestProvider}
5053
testingProviders={testingProviders}
54+
hideModelRates={hideModelRates}
5155
/>
5256
</FormControl>
5357
<FormMessage />

apps/nextjs-app/src/features/app/blocks/admin/setting/components/ai-config/AiForm.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ import {
2525
import { useTranslation } from 'next-i18next';
2626
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2727
import { useForm } from 'react-hook-form';
28+
import { useIsCloud } from '@/features/app/hooks/useIsCloud';
2829
import { AIControlCard } from './AIControlCard';
2930
import { AIModelPreferencesCard } from './AIModelPreferencesCard';
3031
import { AIProviderCard } from './AIProviderCard';
3132
import { BatchTestModels } from './BatchTestModels';
33+
import { FetchPricing } from './FetchPricing';
3234
import type { IModelTestResult } from './LlmproviderManage';
3335
import { generateModelKeyList, parseModelKey } from './utils';
3436

@@ -57,6 +59,7 @@ export function AIConfigForm({
5759
const models = generateModelKeyList(llmProviders);
5860
const { reset } = form;
5961
const { t } = useTranslation(['common', 'space']);
62+
const isCloud = useIsCloud();
6063
const [modelTestResults, setModelTestResults] = useState<Map<string, IModelTestResult>>(
6164
new Map()
6265
);
@@ -168,6 +171,9 @@ export function AIConfigForm({
168171
const enableAi = form.watch('enable');
169172

170173
const switchEnable = useMemo(() => {
174+
if (enableAi) {
175+
return false;
176+
}
171177
if (!aiConfig?.chatModel?.lg && enableAi) {
172178
return false;
173179
}
@@ -263,16 +269,22 @@ export function AIConfigForm({
263269
<div>
264270
<div className="flex items-center justify-between pb-2">
265271
<div className="text-lg font-medium">{t('admin.setting.ai.provider')}</div>
266-
<BatchTestModels
267-
providers={llmProviders}
268-
disabled={!llmProviders?.length}
269-
onResultsChange={setModelTestResults}
270-
onSaveResult={onSaveTestResult}
271-
onTestingProvidersChange={setTestingProviders}
272-
onTestProvider={(callback) => {
273-
testProviderCallbackRef.current = callback;
274-
}}
275-
/>
272+
<div className="flex items-center gap-2">
273+
{/* Fetch Pricing - Cloud only (for billing) */}
274+
{isCloud && (
275+
<FetchPricing providers={llmProviders} onUpdateProviders={updateProviders} />
276+
)}
277+
<BatchTestModels
278+
providers={llmProviders}
279+
disabled={!llmProviders?.length}
280+
onResultsChange={setModelTestResults}
281+
onSaveResult={onSaveTestResult}
282+
onTestingProvidersChange={setTestingProviders}
283+
onTestProvider={(callback) => {
284+
testProviderCallbackRef.current = callback;
285+
}}
286+
/>
287+
</div>
276288
</div>
277289
<AIProviderCard
278290
control={form.control}

0 commit comments

Comments
 (0)