Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/import-historical-checkbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@object-ui/plugin-grid": patch
"@object-ui/types": patch
---

feat(plugin-grid): "Import as historical data" option in the Import Wizard (framework #3479)

Adds a checkbox to the Import Wizard's options panel that sends `treatAsHistorical`
on the import request. When on, the server skips the object's `state_machine` rule so
mid-lifecycle rows — a batch of 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.

Pairs with the framework side (objectstack #3483). `ImportRequestOptions.treatAsHistorical`
is added to `@object-ui/types`, and `assembleImportRequest` threads it through both the
inline and named-mapping request shapes (sent only when on).
25 changes: 23 additions & 2 deletions packages/plugin-grid/src/ImportWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ const IMPORT_DEFAULT_TRANSLATIONS: Record<string, string> = {
'grid.import.needMatchFields': 'Select at least one field to match on.',
'grid.import.optCreateOptions': 'Keep unknown option values',
'grid.import.optRunAutomations': 'Run automations & triggers',
'grid.import.optTreatHistorical': 'Import as historical data',
'grid.import.optTreatHistoricalHint': '(skip state-machine checks so already-completed records import as-is)',
'grid.import.optSkipBlankKey': 'Skip rows with a blank match value',
'grid.import.optBackground': 'Import in the background',
'grid.import.optBackgroundHint': '(runs as an undoable job)',
Expand Down Expand Up @@ -516,6 +518,7 @@ function assembleImportRequest(
matchFields: string[];
createMissingOptions: boolean;
runAutomations: boolean;
treatAsHistorical?: boolean;
skipBlankMatchKey: boolean;
dryRun?: boolean;
/** When set, the server resolves this registered mapping and owns the
Expand All @@ -532,6 +535,7 @@ function assembleImportRequest(
rows,
mappingName: opts.mappingName,
runAutomations: opts.runAutomations,
...(opts.treatAsHistorical ? { treatAsHistorical: true } : {}),
...(opts.dryRun ? { dryRun: true } : {}),
};
}
Expand All @@ -542,6 +546,7 @@ function assembleImportRequest(
...(opts.writeMode !== 'insert' ? { matchFields: opts.matchFields } : {}),
createMissingOptions: opts.createMissingOptions,
runAutomations: opts.runAutomations,
...(opts.treatAsHistorical ? { treatAsHistorical: true } : {}),
skipBlankMatchKey: opts.skipBlankMatchKey,
...(opts.dryRun ? { dryRun: true } : {}),
};
Expand Down Expand Up @@ -1228,6 +1233,8 @@ const ImportOptions: React.FC<{
onCreateMissingOptions: (v: boolean) => void;
runAutomations: boolean;
onRunAutomations: (v: boolean) => void;
treatAsHistorical: boolean;
onTreatAsHistorical: (v: boolean) => void;
skipBlankMatchKey: boolean;
onSkipBlankMatchKey: (v: boolean) => void;
showBackground: boolean;
Expand All @@ -1236,6 +1243,7 @@ const ImportOptions: React.FC<{
}> = ({
fields, mapping, writeMode, onWriteMode, matchFields, onToggleMatchField,
createMissingOptions, onCreateMissingOptions, runAutomations, onRunAutomations,
treatAsHistorical, onTreatAsHistorical,
skipBlankMatchKey, onSkipBlankMatchKey,
showBackground, backgroundImport, onBackgroundImport,
}) => {
Expand Down Expand Up @@ -1301,6 +1309,13 @@ const ImportOptions: React.FC<{
<Checkbox checked={runAutomations} onCheckedChange={(v) => onRunAutomations(v === true)} />
{t('grid.import.optRunAutomations')}
</label>
<label className="flex items-center gap-2 text-xs" data-testid="import-opt-treat-historical">
<Checkbox checked={treatAsHistorical} onCheckedChange={(v) => onTreatAsHistorical(v === true)} />
<span>
{t('grid.import.optTreatHistorical')}
<span className="ml-1 text-[11px] text-muted-foreground">{t('grid.import.optTreatHistoricalHint')}</span>
</span>
</label>
{showBackground && (
<label className="flex items-center gap-2 text-xs" data-testid="import-opt-background">
<Checkbox checked={backgroundImport} onCheckedChange={(v) => onBackgroundImport(v === true)} />
Expand Down Expand Up @@ -1513,6 +1528,10 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
// Default ON: automations always ran on import before framework#2922 wired
// the flag up server-side, so preserving behavior means opt-out, not opt-in.
const [runAutomations, setRunAutomations] = useState(true);
// Default OFF (opt-in): a normal import must still walk the state machine —
// only an explicit "historical" import skips it so mid-lifecycle rows aren't
// rejected by initialStates (framework #3479). The strict behavior is the default.
const [treatAsHistorical, setTreatAsHistorical] = useState(false);
const [skipBlankMatchKey, setSkipBlankMatchKey] = useState(false);
// Opt-in: route this import through a background job even when the row count
// is under the async threshold. This is the only way to obtain an undoable
Expand Down Expand Up @@ -1876,11 +1895,11 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
// hand-mapped, field-keyed rows are for the manual path only.
mappingName ? buildSourceRows(headers, rows, corrections) : buildRawRows(),
{
writeMode, matchFields, createMissingOptions, runAutomations, skipBlankMatchKey, dryRun,
writeMode, matchFields, createMissingOptions, runAutomations, treatAsHistorical, skipBlankMatchKey, dryRun,
...(mappingName ? { mappingName } : {}),
},
),
[buildRawRows, mappingName, headers, rows, corrections, writeMode, matchFields, createMissingOptions, runAutomations, skipBlankMatchKey]);
[buildRawRows, mappingName, headers, rows, corrections, writeMode, matchFields, createMissingOptions, runAutomations, treatAsHistorical, skipBlankMatchKey]);

const handleImport = useCallback(async () => {
setImporting(true); setProgress(0);
Expand Down Expand Up @@ -2146,6 +2165,8 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
onCreateMissingOptions={setCreateMissingOptions}
runAutomations={runAutomations}
onRunAutomations={setRunAutomations}
treatAsHistorical={treatAsHistorical}
onTreatAsHistorical={setTreatAsHistorical}
skipBlankMatchKey={skipBlankMatchKey}
onSkipBlankMatchKey={setSkipBlankMatchKey}
showBackground={supportsImportJob && rows.length <= ASYNC_IMPORT_THRESHOLD}
Expand Down
19 changes: 19 additions & 0 deletions packages/plugin-grid/src/importDryRun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ describe('assembleImportRequest', () => {
expect(req.runAutomations).toBe(true);
expect(req.skipBlankMatchKey).toBe(true);
});

// framework #3479 — the "historical import" toggle. Sent only when on, so a
// normal import carries no flag and the server keeps enforcing the FSM.
it('sends treatAsHistorical:true only when the option is on', () => {
const on = assembleImportRequest(rows, { ...baseOpts, treatAsHistorical: true });
expect(on.treatAsHistorical).toBe(true);

const off = assembleImportRequest(rows, { ...baseOpts, treatAsHistorical: false });
expect(off).not.toHaveProperty('treatAsHistorical');

const omitted = assembleImportRequest(rows, baseOpts);
expect(omitted).not.toHaveProperty('treatAsHistorical');
});

it('sends treatAsHistorical through the named-mapping path too', () => {
const req = assembleImportRequest(rows, { ...baseOpts, mappingName: 'my-map', treatAsHistorical: true });
expect(req.mappingName).toBe('my-map');
expect(req.treatAsHistorical).toBe(true);
});
});

describe('formatDryRunError', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,10 @@ export interface ImportRequestOptions {
matchFields?: string[];
/** Fire triggers/hooks for each imported row (off by default for bulk). */
runAutomations?: boolean;
/** Import as established historical facts: skip the `state_machine` rule so
* mid-lifecycle rows (already-closed tickets, closed_won deals) aren't rejected
* by `initialStates` (framework #3479). @default false */
treatAsHistorical?: boolean;
/** Trim leading/trailing whitespace from string cells. @default true */
trimWhitespace?: boolean;
/** Strings treated as null/blank besides the empty string. */
Expand Down
Loading