Skip to content
Draft
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: 9 additions & 2 deletions apps/cli/src/commands/drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,18 @@ export async function runDriversInstall(name: string): Promise<void> {
const cwd = webWorkspaceRoot();

// Prefer installing into the @foxschema/web package directory.
// ibm_db must run install scripts (--foreground-scripts) or clidriver never
// downloads and Windows connects fail with SQL1042C / missing native binding.
const npmArgs =
entry.pkg === 'ibm_db'
? ['install', 'ibm_db@4.0.1', '--foreground-scripts', '--prefix', cwd]
: ['install', entry.pkg, '--foreground-scripts', '--prefix', cwd];

await new Promise<void>((resolve, reject) => {
const child = spawn('npm', ['install', entry.pkg, '--prefix', cwd], {
const child = spawn('npm', npmArgs, {
stdio: 'inherit',
shell: process.platform === 'win32',
env: process.env,
env: { ...process.env, npm_config_ignore_scripts: '' },
});
child.on('error', reject);
child.on('exit', (code) => {
Expand Down
13 changes: 9 additions & 4 deletions apps/e2e/src/pages/SqlEditorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,18 +192,23 @@ export class SqlEditorPage {
tableName,
{ timeout: 45_000 }
);
// Prefer the blueprint button nearest the table name label.
// Edit table sits under the name (always visible — no hover / group class).
const nameLabel = explorer.getByText(tableName, { exact: true }).first();
await nameLabel.scrollIntoViewIfNeeded();
const row = nameLabel.locator('xpath=ancestor::div[contains(@class,"group")][1]');
await row.hover();
const row = nameLabel.locator('xpath=ancestor::div[./button[@data-testid="sql-open-blueprint"] or .//button[@data-testid="sql-open-blueprint"]][1]');
await row.locator('[data-testid="sql-open-blueprint"]').click({ force: true });
await waitFor(this.page, '[data-testid="table-blueprint-modal"]', 15_000);
}

async openNewTableBlueprint(): Promise<void> {
await this.dismissOverlays();
await clickWhen(this.page, '[data-testid="sql-new-table"]');
// Prefer Schema header action; fall back to explorer New control.
const header = this.page.locator('[data-testid="sql-schema-new-table"]');
if (await header.count()) {
await header.click();
} else {
await clickWhen(this.page, '[data-testid="sql-new-table"]');
}
await waitFor(this.page, '[data-testid="table-blueprint-modal"]', 15_000);
}

Expand Down
50 changes: 50 additions & 0 deletions apps/e2e/src/tests/sql-editor-blueprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,56 @@ describe.skipIf(!ready)('SQL Editor · blueprint + paging (SQLite)', () => {
expect(editorText.toLowerCase()).toMatch(/create\s*table/);
});

it('Indexes section can add a UNIQUE index on a column', async () => {
await sql.openNewTableBlueprint();
const modal = driver.locator('[data-testid="table-blueprint-modal"]');
expect(await modal.isVisible()).toBe(true);
await driver.locator('[data-testid="blueprint-table-name"]').fill(`t_idx_${RUN}`);

await modal.getByRole('button', { name: /add column/i }).click();
await waitForColumnForm(driver);
await modal.locator('[data-testid="blueprint-column-form"] input').first().fill('email');
await modal.getByRole('button', { name: /^save$/i }).click();
await driver.waitForFunction(() => {
const m = document.querySelector('[data-testid="table-blueprint-modal"]');
return !!m && /email/i.test(m.textContent ?? '') && !m.querySelector('[data-testid="blueprint-column-form"]');
});

const indexes = modal.locator('[data-testid="blueprint-indexes"]');
expect(await indexes.isVisible()).toBe(true);
await indexes.locator('[data-testid="blueprint-add-index"]').click();
await driver.waitForSelector('[data-testid="blueprint-index-form"]', { timeout: 5_000 });

// openAddIndex already selects the first column — only toggle email on if missing.
const form = modal.locator('[data-testid="blueprint-index-form"]');
const formText = await form.innerText();
if (!/\(email/i.test(formText)) {
await form.getByRole('button', { name: /^email$/i }).click();
}
const unique = form.locator('input[type="checkbox"]').first();
if (await unique.count()) {
if (!(await unique.isChecked())) await unique.check();
}
await modal.locator('[data-testid="blueprint-index-save"]').click();

await driver.waitForFunction(() => {
const root = document.querySelector('[data-testid="blueprint-indexes"]');
if (!root) return false;
if (root.querySelector('[data-testid="blueprint-index-form"]')) return false;
return /email/i.test(root.textContent ?? '');
}, { timeout: 5_000 });

await sql.blueprintInsertSql();
await sql.closeBlueprint().catch(() => undefined);
await driver.waitForTimeout(500);
const editorText = await driver.evaluate(() => {
const lines = document.querySelector('.monaco-editor .view-lines');
const ta = document.querySelector('.monaco-editor textarea') as HTMLTextAreaElement | null;
return `${lines?.textContent ?? ''}\n${ta?.value ?? ''}`;
});
expect(editorText.toLowerCase()).toMatch(/create\s+unique\s+index|create\s+index/);
});

it('pages SELECT results with Next/Prev', async () => {
await driver.locator('[data-testid="sql-max-rows"]').fill('2');
await sql.setSql('SELECT id1, id2, label FROM parent ORDER BY id1;');
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/backend/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DriverDetector,
buildConnectionString,
normalizeTableSchemas,
getProviderSettings,
type MigrationStep,
type ConnectionOptions,
type DbObjectType,
Expand Down Expand Up @@ -358,6 +359,13 @@ export function createApiRoutes(connectionModule: ConnectionModule, connectionSt
const { scope, ...ref } = req.body as ConnectionRef & { scope: DbObjectType[] };
try {
const { dialect, option, schema } = await resolveRef((req as AuthedRequest).userId, ref);
const settings = getProviderSettings(dialect);
if (settings.schemaRequired && !schema?.trim()) {
res.status(400).json({
error: `${settings.label} requires a schema. Load schemas for the connection, then pick one before browsing or editing tables.`,
});
return;
}
const { tables, warnings } = await loadScopedTables(dialect, option, schema, scope);
res.json(warnings.length ? { tables, warnings } : { tables });
} catch (error: unknown) {
Expand Down
31 changes: 27 additions & 4 deletions apps/web/src/frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,37 @@ const App: React.FC = () => {
init();
}, [init, apply]);

// Once signed in, load the user's saved connections and appearance
// Once signed in, load the user's saved connections and appearance.
// Wait for sync-store persist rehydrate so selected connection IDs are
// restored before loadConnections reapplies source/target configs.
useEffect(() => {
if (status === 'ready') {
useSyncStore.getState().loadConnections();
if (status !== 'ready') return;

let cancelled = false;
const load = () => {
if (cancelled) return;
void useSyncStore.getState().loadConnections();
apiGetPreferences()
.then((p) => hydrateFromServer(p.theme))
.then((p) => {
if (!cancelled) hydrateFromServer(p.theme);
})
.catch(() => undefined);
};

if (useSyncStore.persist.hasHydrated()) {
load();
return () => {
cancelled = true;
};
}

const unsub = useSyncStore.persist.onFinishHydration(() => {
load();
});
return () => {
cancelled = true;
unsub();
};
}, [status, hydrateFromServer]);

if (status === 'loading') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SQL_ICON_STROKE } from './sqlIconStyle';
*/
export const ConnectionChecklist: React.FC = () => {
const connections = useSyncStore((s) => s.connections);
const connectionsLoaded = useSyncStore((s) => s.connectionsLoaded);
const tabs = useSqlEditorStore((s) => s.tabs);
const activeTabId = useSqlEditorStore((s) => s.activeTabId);
const shareDestinations = useSqlEditorStore((s) => s.shareDestinations);
Expand Down Expand Up @@ -59,7 +60,9 @@ export const ConnectionChecklist: React.FC = () => {
</span>
</label>

{connections.length === 0 ? (
{!connectionsLoaded ? (
<p className="text-[13px] font-medium text-slate-500">Loading connections…</p>
) : connections.length === 0 ? (
<p className="text-[13px] font-medium text-slate-500">
No saved connections yet — add one via the Credentials button in the toolbar.
</p>
Expand Down
18 changes: 16 additions & 2 deletions apps/web/src/frontend/components/sql-editor/SqlEditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
KeyRound,
PanelLeftClose,
PanelLeftOpen,
Plus,
} from 'lucide-react';
import { useSyncStore } from '../../store/useSyncStore';
import { useSqlEditorStore } from '../../store/useSqlEditorStore';
Expand All @@ -31,7 +32,7 @@ import { SqlBookmarksPanel } from './SqlBookmarksPanel';
import { SqlVariablesPanel } from './SqlVariablesPanel';
import { SqlSecretsPanel, type SqlSecretsPanelHandle } from './SqlSecretsPanel';
import { SQL_ICON_STROKE } from './sqlIconStyle';
import { SqlSchemaExplorer } from './SqlSchemaExplorer';
import { SqlSchemaExplorer, type SqlSchemaExplorerHandle } from './SqlSchemaExplorer';
import {
SqlSidebarSection,
useSidebarSectionHeights,
Expand Down Expand Up @@ -129,6 +130,7 @@ export const SqlEditorView: React.FC = () => {
const [sidebarOpen, toggleSidebar] = useSidebarSectionsOpen();
const [sectionHeights, setSectionHeight] = useSidebarSectionHeights();
const secretsPanelRef = useRef<SqlSecretsPanelHandle>(null);
const schemaExplorerRef = useRef<SqlSchemaExplorerHandle>(null);
const [secretsRefreshing, setSecretsRefreshing] = useState(false);

const onSecretsRefresh = useCallback(async () => {
Expand Down Expand Up @@ -377,8 +379,20 @@ export const SqlEditorView: React.FC = () => {
grow
height={sectionHeights.schema}
onResizeHeight={(h) => setSectionHeight('schema', h)}
actions={
<button
type="button"
data-testid="sql-schema-new-table"
title="Create table — opens blueprint to add columns"
onClick={() => schemaExplorerRef.current?.openCreateTable()}
className="flex items-center gap-0.5 text-[12px] font-bold text-[#059669] hover:text-[#047857] transition"
>
<Plus className="w-3.5 h-3.5 text-[#059669]" strokeWidth={SQL_ICON_STROKE} />
New table
</button>
}
>
<SqlSchemaExplorer />
<SqlSchemaExplorer ref={schemaExplorerRef} />
</SqlSidebarSection>
</div>
<div
Expand Down
Loading
Loading