Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
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
11 changes: 11 additions & 0 deletions .changeset/12985-import-optouts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/i18n': patch
---

The Import Wizard no longer offers "Run automations & triggers" or "Import as historical data" (objectui#12985). Both import doors (`POST /api/v1/data/:object/import` and `/import/jobs`) honour `runAutomations: false` and `treatAsHistorical: true` for the system principal only and refuse every signed-in caller `403 PERMISSION_DENIED` (objectstack-ai/objectstack#24348). A user who unticked the first box or ticked the second had the whole import refused.

- **The two options are gone** from the wizard's options panel, with their state. The wizard's import, dry-run and background-job requests carry neither `runAutomations` nor `treatAsHistorical`. Leaving both keys out is the server's declared default, so the default import is unchanged: automations run and the state machine is walked.
- **Language packs.** The three keys only those options read, `grid.import.optRunAutomations`, `grid.import.optTreatHistorical` and `grid.import.optTreatHistoricalHint`, are removed from all ten packs and from the wizard's English fallback map. The locale objects exported under `@object-ui/i18n/locales/*` lose those three members.

Nothing else on either package entry changes: `ImportWizardProps` and `ImportResult` are unchanged, and `ImportRequestOptions` in `@object-ui/types` still declares both fields, because the server still accepts them from the system principal.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
* "automation". The platform's manual automation surfaces are real and are
* deliberately NOT covered: `home.build.subtitle` (its card opens Studio, which
* genuinely authors flows), `engine.studio.landing.description`,
* `dataImport.optRunAutomations`, `packagedAutomation.*`, the marketplace
* `packagedAutomation.*`, the marketplace
* `automation` category, and the approvals / flow-runner groups.
*
* REVERT the wording (and relax this suite) when ADR-0112 v2 re-adds flows and
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,9 +667,6 @@ const ar = {
needMatchFields: "اختر حقلًا واحدًا على الأقل للمطابقة.",
matchOnlyField: "(للمطابقة فقط)",
optCreateOptions: "الاحتفاظ بقيم الخيارات غير المعروفة",
optRunAutomations: "تشغيل الأتمتة والمشغّلات",
optTreatHistorical: "الاستيراد كبيانات تاريخية",
optTreatHistoricalHint: "(استيراد السجلات المكتملة كما هي — تخطي فحوصات آلة الحالات والاحتفاظ بالطوابع الزمنية والمؤلف الأصليين بدل ختمها الآن)",
optSkipBlankKey: "تخطي الصفوف التي تكون قيمة المطابقة فيها فارغة",
optBackground: "الاستيراد في الخلفية",
optBackgroundHint: "(يعمل كمهمة قابلة للتراجع)",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,6 @@ const de = {
needMatchFields: "Wählen Sie mindestens ein Feld für den Abgleich aus.",
matchOnlyField: "(nur Abgleich)",
optCreateOptions: "Unbekannte Optionswerte beibehalten",
optRunAutomations: "Automatisierungen und Trigger ausführen",
optTreatHistorical: "Als historische Daten importieren",
optTreatHistoricalHint: "(abgeschlossene Datensätze unverändert importieren — Zustandsautomat-Prüfungen überspringen und die ursprünglichen Zeitstempel und Autoren beibehalten, statt jetzt zu stempeln)",
optSkipBlankKey: "Zeilen mit leerem Abgleichswert überspringen",
optBackground: "Im Hintergrund importieren",
optBackgroundHint: "(läuft als rückgängig machbarer Auftrag)",
Expand Down
5 changes: 1 addition & 4 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,9 +864,6 @@ const en = {
needMatchFields: 'Select at least one field to match on.',
matchOnlyField: '(match only)',
optCreateOptions: 'Keep unknown option values',
optRunAutomations: 'Run automations & triggers',
optTreatHistorical: 'Import as historical data',
optTreatHistoricalHint: '(import completed records as-is — skip state-machine checks and keep their original timestamps & author instead of stamping now)',
optSkipBlankKey: 'Skip rows with a blank match value',
optBackground: 'Import in the background',
optBackgroundHint: '(runs as an undoable job)',
Expand Down Expand Up @@ -2365,7 +2362,7 @@ const en = {
// NOT the same thing as the platform's manual automation surfaces, which
// are real and stay as they are: `home.build.subtitle` (that card opens
// Studio, which does author flows), `engine.studio.landing.description`,
// `dataImport.optRunAutomations`, `packagedAutomation.*` and the
// `packagedAutomation.*` and the
// marketplace `automation` category. The boundary is v1 AI AUTHORING, not
// the word "automation".
//
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,9 +605,6 @@ const es = {
needMatchFields: "Seleccione al menos un campo por el que coincidir.",
matchOnlyField: "(solo coincidencia)",
optCreateOptions: "Conservar los valores de opción desconocidos",
optRunAutomations: "Ejecutar automatizaciones y disparadores",
optTreatHistorical: "Importar como datos históricos",
optTreatHistoricalHint: "(importa los registros completados tal cual: omite las comprobaciones de la máquina de estados y conserva sus marcas de tiempo y autor originales en lugar de sellarlos ahora)",
optSkipBlankKey: "Omitir las filas con valor de coincidencia vacío",
optBackground: "Importar en segundo plano",
optBackgroundHint: "(se ejecuta como un trabajo reversible)",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,6 @@ const fr = {
needMatchFields: "Sélectionnez au moins un champ de correspondance.",
matchOnlyField: "(correspondance seule)",
optCreateOptions: "Conserver les valeurs d'option inconnues",
optRunAutomations: "Exécuter les automatisations et déclencheurs",
optTreatHistorical: "Importer comme données historiques",
optTreatHistoricalHint: "(importer les enregistrements terminés tels quels — ignorer les contrôles de la machine à états et conserver leurs horodatages et auteurs d'origine au lieu d'horodater maintenant)",
optSkipBlankKey: "Ignorer les lignes dont la valeur de correspondance est vide",
optBackground: "Importer en arrière-plan",
optBackgroundHint: "(s'exécute comme une tâche annulable)",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,6 @@ const ja = {
needMatchFields: "照合する項目を 1 つ以上選択してください。",
matchOnlyField: "(照合のみ)",
optCreateOptions: "未知の選択肢の値を保持する",
optRunAutomations: "自動化とトリガーを実行する",
optTreatHistorical: "過去データとしてインポートする",
optTreatHistoricalHint: "(完了済みレコードをそのまま取り込みます — ステートマシンの検査を省略し、現在時刻を打刻せずに元のタイムスタンプと作成者を保持します)",
optSkipBlankKey: "照合値が空の行をスキップする",
optBackground: "バックグラウンドでインポートする",
optBackgroundHint: "(取り消し可能なジョブとして実行されます)",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,6 @@ const ko = {
needMatchFields: "일치 기준 필드를 하나 이상 선택하세요.",
matchOnlyField: "(일치 전용)",
optCreateOptions: "알 수 없는 선택 값 유지",
optRunAutomations: "자동화 및 트리거 실행",
optTreatHistorical: "과거 데이터로 가져오기",
optTreatHistoricalHint: "(완료된 레코드를 그대로 가져옵니다 — 상태 머신 검사를 건너뛰고 현재 시각을 찍는 대신 원래 타임스탬프와 작성자를 유지합니다)",
optSkipBlankKey: "일치 값이 비어 있는 행 건너뛰기",
optBackground: "백그라운드에서 가져오기",
optBackgroundHint: "(되돌릴 수 있는 작업으로 실행됩니다)",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,9 +600,6 @@ const pt = {
needMatchFields: "Selecione pelo menos um campo de correspondência.",
matchOnlyField: "(somente correspondência)",
optCreateOptions: "Manter valores de opção desconhecidos",
optRunAutomations: "Executar automações e gatilhos",
optTreatHistorical: "Importar como dados históricos",
optTreatHistoricalHint: "(importa registros concluídos como estão — ignora as verificações da máquina de estados e mantém os carimbos de data/hora e o autor originais em vez de carimbar agora)",
optSkipBlankKey: "Ignorar linhas com valor de correspondência em branco",
optBackground: "Importar em segundo plano",
optBackgroundHint: "(executa como um trabalho reversível)",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,9 +627,6 @@ const ru = {
needMatchFields: "Выберите хотя бы одно поле для сопоставления.",
matchOnlyField: "(только сопоставление)",
optCreateOptions: "Сохранять неизвестные значения списка",
optRunAutomations: "Выполнять автоматизации и триггеры",
optTreatHistorical: "Импортировать как исторические данные",
optTreatHistoricalHint: "(импортирует завершённые записи как есть — пропускает проверки конечного автомата и сохраняет исходные отметки времени и автора вместо текущих)",
optSkipBlankKey: "Пропускать строки с пустым значением сопоставления",
optBackground: "Импортировать в фоновом режиме",
optBackgroundHint: "(выполняется как отменяемая задача)",
Expand Down
3 changes: 0 additions & 3 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,8 +592,6 @@ const zh = {
importNRows_other: '导入 {{count}} 行',
importingProgress: '导入中…',
requiredMark: '*',
optTreatHistorical: '作为历史数据导入',
optTreatHistoricalHint: '(按原样导入已完结的记录——跳过状态机校验,保留原始时间戳与作者,而不是记为当前时间)',
missingRequiredHint: '无法继续——以下必填字段未映射:{{fields}}。请在文件中添加对应的列,或返回上一步重新上传包含该列的文件。',
legacyFallbackNotice: '已通过兼容模式导入:当前连接不支持服务端导入接口,因此所有值均以文本写入,未做服务端类型转换。请升级后端/客户端以获得完整的导入支持(类型转换与关联查找)。',
required: '必填',
Expand Down Expand Up @@ -648,7 +646,6 @@ const zh = {
matchFieldsHint: '按这些字段将行匹配到已有记录。',
needMatchFields: '请至少选择一个匹配字段。',
matchOnlyField: '(仅匹配)',
optRunAutomations: '运行自动化与触发器',
optCreateOptions: '保留未知选项值',
optSkipBlankKey: '跳过匹配值为空的行',
optBackground: '后台导入',
Expand Down
106 changes: 106 additions & 0 deletions packages/plugin-grid/src/ImportWizard.controlOptOuts-12985.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* ObjectUI – Copyright (c) 2024-present ObjectStack Inc.
* Licensed under MIT.
*/

/**
* The Import Wizard offers neither control opt-out (objectui#12985).
*
* Both import doors (`POST /api/v1/data/:object/import` and `/import/jobs`)
* honour `runAutomations: false` and `treatAsHistorical: true` for the system
* principal only, and refuse every other caller `403 PERMISSION_DENIED`
* (objectstack-ai/objectstack#24348). A browser request is never the system
* principal, so a wizard that offered either option turned a user's click into
* a refusal of the whole import. The wizard therefore draws neither option and
* its request carries neither opt-out. The default import, which runs
* automations and walks the state machine, is unchanged: omitting both keys is
* the server's declared default.
*/
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import React from 'react';

import { ImportWizard } from './ImportWizard';
import type { ImportRecordsResult, ImportRequestOptions } from '@object-ui/types';

type WizardField = React.ComponentProps<typeof ImportWizard>['fields'][number];

const FIELDS: WizardField[] = [{ name: 'title', label: 'Title', type: 'text' }];

/** Paste TSV into the upload step via the wizard's window-level handler. */
function pasteRows(text: string) {
const evt = new Event('paste', { bubbles: true, cancelable: true }) as Event & {
clipboardData: { getData: (type: string) => string };
};
evt.clipboardData = { getData: (type: string) => (type === 'text/plain' ? text : '') };
act(() => { window.dispatchEvent(evt); });
}

/** Open the wizard over `dataSource`, paste two rows, go to the preview step. */
async function openPreview(dataSource: object, onComplete?: (r: unknown) => void) {
render(
<ImportWizard
objectName="task"
fields={FIELDS}
dataSource={dataSource as never}
open
onOpenChange={() => {}}
onComplete={onComplete as never}
/>,
);
pasteRows('Title\nA\nB');
const next = await screen.findByTestId('import-next-btn');
await waitFor(() => expect(next).toBeEnabled());
fireEvent.click(next);
return screen.findByTestId('import-run-btn');
}

describe('ImportWizard control opt-outs (objectui#12985)', () => {
it('draws neither the run-automations nor the treat-as-historical option', async () => {
await openPreview({});
// CONTROL: the options panel and its other options are drawn.
expect(screen.getByTestId('import-options')).toBeInTheDocument();
expect(screen.getByTestId('import-opt-create-options')).toBeInTheDocument();

expect(screen.queryByTestId('import-opt-run-automations')).toBeNull();
expect(screen.queryByTestId('import-opt-treat-historical')).toBeNull();
expect(screen.queryByText(/automations/i)).toBeNull();
expect(screen.queryByText(/historical/i)).toBeNull();
});

it('a completed default import sends neither opt-out and still imports', async () => {
const sent: ImportRequestOptions[] = [];
const importRecords = vi.fn(async (_object: string, request: ImportRequestOptions): Promise<ImportRecordsResult> => {
sent.push(request);
return {
object: 'task', dryRun: false, writeMode: 'insert', total: 2,
ok: 2, errors: 0, created: 2, updated: 0, skipped: 0,
results: [{ row: 1, ok: true, action: 'created' }, { row: 2, ok: true, action: 'created' }],
} as ImportRecordsResult;
});
const onComplete = vi.fn();
const run = await openPreview({ importRecords }, onComplete);
await waitFor(() => expect(run).toBeEnabled());
fireEvent.click(run);
await waitFor(() => expect(onComplete).toHaveBeenCalled());

// The real import request (a dry-run pre-check, if any, carries dryRun).
const real = sent.filter((r) => !r.dryRun);
expect(real).toHaveLength(1);
const request = real[0];
expect(request).not.toHaveProperty('runAutomations');
expect(request).not.toHaveProperty('treatAsHistorical');
// CONTROL: the rest of the request is the default import's, unchanged.
expect(request).toEqual({
format: 'json',
rows: [{ title: 'A' }, { title: 'B' }],
writeMode: 'insert',
createMissingOptions: false,
skipBlankMatchKey: false,
});

// The default import still runs to completion.
expect(onComplete.mock.calls[0][0]).toMatchObject({ totalRows: 2, importedRows: 2, skippedRows: 0, errors: [] });
});
});
Loading
Loading