Skip to content

Commit c7cff19

Browse files
os-zhuangclaude
andauthored
feat(plugin-grid): "Import as historical data" option in the Import Wizard (framework #3479) (#2815)
Adds a checkbox to the Import Wizard's options panel (next to "Run automations & triggers") that sends `treatAsHistorical` on the import request. When on, the server skips the object's `state_machine` rule so mid-lifecycle rows — already-`closed` tickets, `closed_won` deals — aren't rejected by `initialStates`. Off by default: a normal import still walks the FSM, so the exemption is always an explicit opt-in. - types: `ImportRequestOptions.treatAsHistorical?`. - ImportWizard: option state (default off), checkbox + hint, threaded through `assembleImportRequest` (sent only when on) for both inline and named-mapping shapes. Pairs with the framework side (objectstack #3483). Verified in the plugin-grid demo: the checkbox renders in the Preview step's options panel, defaults off, and toggles. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d62fb1f commit c7cff19

4 files changed

Lines changed: 62 additions & 2 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@object-ui/plugin-grid": patch
3+
"@object-ui/types": patch
4+
---
5+
6+
feat(plugin-grid): "Import as historical data" option in the Import Wizard (framework #3479)
7+
8+
Adds a checkbox to the Import Wizard's options panel that sends `treatAsHistorical`
9+
on the import request. When on, the server skips the object's `state_machine` rule so
10+
mid-lifecycle rows — a batch of already-`closed` tickets, `closed_won` deals — aren't
11+
rejected by `initialStates`. Off by default: a normal import still walks the FSM, so
12+
the exemption is always an explicit opt-in.
13+
14+
Pairs with the framework side (objectstack #3483). `ImportRequestOptions.treatAsHistorical`
15+
is added to `@object-ui/types`, and `assembleImportRequest` threads it through both the
16+
inline and named-mapping request shapes (sent only when on).

packages/plugin-grid/src/ImportWizard.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ const IMPORT_DEFAULT_TRANSLATIONS: Record<string, string> = {
120120
'grid.import.needMatchFields': 'Select at least one field to match on.',
121121
'grid.import.optCreateOptions': 'Keep unknown option values',
122122
'grid.import.optRunAutomations': 'Run automations & triggers',
123+
'grid.import.optTreatHistorical': 'Import as historical data',
124+
'grid.import.optTreatHistoricalHint': '(skip state-machine checks so already-completed records import as-is)',
123125
'grid.import.optSkipBlankKey': 'Skip rows with a blank match value',
124126
'grid.import.optBackground': 'Import in the background',
125127
'grid.import.optBackgroundHint': '(runs as an undoable job)',
@@ -516,6 +518,7 @@ function assembleImportRequest(
516518
matchFields: string[];
517519
createMissingOptions: boolean;
518520
runAutomations: boolean;
521+
treatAsHistorical?: boolean;
519522
skipBlankMatchKey: boolean;
520523
dryRun?: boolean;
521524
/** When set, the server resolves this registered mapping and owns the
@@ -532,6 +535,7 @@ function assembleImportRequest(
532535
rows,
533536
mappingName: opts.mappingName,
534537
runAutomations: opts.runAutomations,
538+
...(opts.treatAsHistorical ? { treatAsHistorical: true } : {}),
535539
...(opts.dryRun ? { dryRun: true } : {}),
536540
};
537541
}
@@ -542,6 +546,7 @@ function assembleImportRequest(
542546
...(opts.writeMode !== 'insert' ? { matchFields: opts.matchFields } : {}),
543547
createMissingOptions: opts.createMissingOptions,
544548
runAutomations: opts.runAutomations,
549+
...(opts.treatAsHistorical ? { treatAsHistorical: true } : {}),
545550
skipBlankMatchKey: opts.skipBlankMatchKey,
546551
...(opts.dryRun ? { dryRun: true } : {}),
547552
};
@@ -1228,6 +1233,8 @@ const ImportOptions: React.FC<{
12281233
onCreateMissingOptions: (v: boolean) => void;
12291234
runAutomations: boolean;
12301235
onRunAutomations: (v: boolean) => void;
1236+
treatAsHistorical: boolean;
1237+
onTreatAsHistorical: (v: boolean) => void;
12311238
skipBlankMatchKey: boolean;
12321239
onSkipBlankMatchKey: (v: boolean) => void;
12331240
showBackground: boolean;
@@ -1236,6 +1243,7 @@ const ImportOptions: React.FC<{
12361243
}> = ({
12371244
fields, mapping, writeMode, onWriteMode, matchFields, onToggleMatchField,
12381245
createMissingOptions, onCreateMissingOptions, runAutomations, onRunAutomations,
1246+
treatAsHistorical, onTreatAsHistorical,
12391247
skipBlankMatchKey, onSkipBlankMatchKey,
12401248
showBackground, backgroundImport, onBackgroundImport,
12411249
}) => {
@@ -1301,6 +1309,13 @@ const ImportOptions: React.FC<{
13011309
<Checkbox checked={runAutomations} onCheckedChange={(v) => onRunAutomations(v === true)} />
13021310
{t('grid.import.optRunAutomations')}
13031311
</label>
1312+
<label className="flex items-center gap-2 text-xs" data-testid="import-opt-treat-historical">
1313+
<Checkbox checked={treatAsHistorical} onCheckedChange={(v) => onTreatAsHistorical(v === true)} />
1314+
<span>
1315+
{t('grid.import.optTreatHistorical')}
1316+
<span className="ml-1 text-[11px] text-muted-foreground">{t('grid.import.optTreatHistoricalHint')}</span>
1317+
</span>
1318+
</label>
13041319
{showBackground && (
13051320
<label className="flex items-center gap-2 text-xs" data-testid="import-opt-background">
13061321
<Checkbox checked={backgroundImport} onCheckedChange={(v) => onBackgroundImport(v === true)} />
@@ -1513,6 +1528,10 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
15131528
// Default ON: automations always ran on import before framework#2922 wired
15141529
// the flag up server-side, so preserving behavior means opt-out, not opt-in.
15151530
const [runAutomations, setRunAutomations] = useState(true);
1531+
// Default OFF (opt-in): a normal import must still walk the state machine —
1532+
// only an explicit "historical" import skips it so mid-lifecycle rows aren't
1533+
// rejected by initialStates (framework #3479). The strict behavior is the default.
1534+
const [treatAsHistorical, setTreatAsHistorical] = useState(false);
15161535
const [skipBlankMatchKey, setSkipBlankMatchKey] = useState(false);
15171536
// Opt-in: route this import through a background job even when the row count
15181537
// is under the async threshold. This is the only way to obtain an undoable
@@ -1876,11 +1895,11 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
18761895
// hand-mapped, field-keyed rows are for the manual path only.
18771896
mappingName ? buildSourceRows(headers, rows, corrections) : buildRawRows(),
18781897
{
1879-
writeMode, matchFields, createMissingOptions, runAutomations, skipBlankMatchKey, dryRun,
1898+
writeMode, matchFields, createMissingOptions, runAutomations, treatAsHistorical, skipBlankMatchKey, dryRun,
18801899
...(mappingName ? { mappingName } : {}),
18811900
},
18821901
),
1883-
[buildRawRows, mappingName, headers, rows, corrections, writeMode, matchFields, createMissingOptions, runAutomations, skipBlankMatchKey]);
1902+
[buildRawRows, mappingName, headers, rows, corrections, writeMode, matchFields, createMissingOptions, runAutomations, treatAsHistorical, skipBlankMatchKey]);
18841903

18851904
const handleImport = useCallback(async () => {
18861905
setImporting(true); setProgress(0);
@@ -2146,6 +2165,8 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
21462165
onCreateMissingOptions={setCreateMissingOptions}
21472166
runAutomations={runAutomations}
21482167
onRunAutomations={setRunAutomations}
2168+
treatAsHistorical={treatAsHistorical}
2169+
onTreatAsHistorical={setTreatAsHistorical}
21492170
skipBlankMatchKey={skipBlankMatchKey}
21502171
onSkipBlankMatchKey={setSkipBlankMatchKey}
21512172
showBackground={supportsImportJob && rows.length <= ASYNC_IMPORT_THRESHOLD}

packages/plugin-grid/src/importDryRun.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ describe('assembleImportRequest', () => {
5151
expect(req.runAutomations).toBe(true);
5252
expect(req.skipBlankMatchKey).toBe(true);
5353
});
54+
55+
// framework #3479 — the "historical import" toggle. Sent only when on, so a
56+
// normal import carries no flag and the server keeps enforcing the FSM.
57+
it('sends treatAsHistorical:true only when the option is on', () => {
58+
const on = assembleImportRequest(rows, { ...baseOpts, treatAsHistorical: true });
59+
expect(on.treatAsHistorical).toBe(true);
60+
61+
const off = assembleImportRequest(rows, { ...baseOpts, treatAsHistorical: false });
62+
expect(off).not.toHaveProperty('treatAsHistorical');
63+
64+
const omitted = assembleImportRequest(rows, baseOpts);
65+
expect(omitted).not.toHaveProperty('treatAsHistorical');
66+
});
67+
68+
it('sends treatAsHistorical through the named-mapping path too', () => {
69+
const req = assembleImportRequest(rows, { ...baseOpts, mappingName: 'my-map', treatAsHistorical: true });
70+
expect(req.mappingName).toBe('my-map');
71+
expect(req.treatAsHistorical).toBe(true);
72+
});
5473
});
5574

5675
describe('formatDryRunError', () => {

packages/types/src/data.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,10 @@ export interface ImportRequestOptions {
819819
matchFields?: string[];
820820
/** Fire triggers/hooks for each imported row (off by default for bulk). */
821821
runAutomations?: boolean;
822+
/** Import as established historical facts: skip the `state_machine` rule so
823+
* mid-lifecycle rows (already-closed tickets, closed_won deals) aren't rejected
824+
* by `initialStates` (framework #3479). @default false */
825+
treatAsHistorical?: boolean;
822826
/** Trim leading/trailing whitespace from string cells. @default true */
823827
trimWhitespace?: boolean;
824828
/** Strings treated as null/blank besides the empty string. */

0 commit comments

Comments
 (0)