Skip to content

Commit 2b0c4df

Browse files
authored
[Website] MySQL admin tool improvements (#2971)
## Motivation for the change, related issues Fixes row editing in phpMyAdmin: <img width="903" height="507" alt="Screenshot 2025-12-01 at 20 31 50" src="https://github.com/user-attachments/assets/717b6329-d650-46dc-bf9b-c6c56564c05f" /> ## Implementation details PhpMyAdmin needs MySQLi-like column metadata for row editing. I also added more tests to cover this functionality. ## Testing Instructions (or ideally a Blueprint) 1. Run `npx nx dev playground-website`. 2. Go to http://127.0.0.1:5400/website-server/, open settings and go to the Database panel. 3. Click on phpMyAdmin, browse some table data, and click the row "Edit" button. 4. Editing should work and save the data.
1 parent 38c4f7b commit 2b0c4df

File tree

3 files changed

+147
-5
lines changed

3 files changed

+147
-5
lines changed

packages/playground/website/playwright/e2e/website-ui.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,56 @@ test.describe('Database panel', () => {
377377
expect(newPage.url()).toContain('/adminer/');
378378
await expect(newPage.locator('body')).toContainText('Adminer');
379379
await expect(newPage.locator('body')).toContainText('wp_posts');
380+
381+
// Browse the "wp_posts" table
382+
await newPage
383+
.locator('#tables a.structure[title="Show structure"]')
384+
.filter({ hasText: 'wp_posts' })
385+
.click();
386+
await newPage.waitForLoadState();
387+
await newPage.getByRole('link', { name: 'select data' }).click();
388+
await newPage.waitForLoadState();
389+
const adminerRows = newPage.locator('table.checkable tbody tr');
390+
await expect(adminerRows.first()).toContainText(
391+
'Welcome to WordPress.'
392+
);
393+
394+
// Click "edit" on a row
395+
await adminerRows.first().getByRole('link', { name: 'edit' }).click();
396+
await newPage.waitForLoadState();
397+
await expect(newPage.locator('form#form')).toBeVisible();
398+
await expect(newPage.locator('form#form')).toContainText(
399+
'Welcome to WordPress.'
400+
);
401+
402+
// Update the post content
403+
const postContentTextarea = newPage.locator(
404+
'textarea[name="fields[post_content]"]'
405+
);
406+
await postContentTextarea.click();
407+
await postContentTextarea.clear();
408+
await postContentTextarea.fill('Updated post content.');
409+
await newPage
410+
.getByRole('button', { name: 'Save', exact: true })
411+
.click();
412+
await newPage.waitForLoadState();
413+
414+
// Go back row listing and verify the updated content
415+
await newPage.getByRole('link', { name: 'Select data' }).click();
416+
await newPage.waitForLoadState();
417+
await expect(
418+
newPage.locator('table.checkable tbody tr').first()
419+
).toContainText('Updated post content.');
420+
421+
// Go to SQL tab and execute "SHOW TABLES"
422+
await newPage.getByRole('link', { name: 'SQL command' }).click();
423+
await newPage.waitForLoadState();
424+
const sqlTextarea = newPage.locator('textarea[name="query"]');
425+
await sqlTextarea.fill('SHOW TABLES', { force: true });
426+
await newPage.getByRole('button', { name: 'Execute' }).click();
427+
await newPage.waitForLoadState();
428+
await expect(newPage.locator('body')).toContainText('wp_posts');
429+
380430
await newPage.close();
381431
});
382432

@@ -399,6 +449,60 @@ test.describe('Database panel', () => {
399449
expect(newPage.url()).toContain('/phpmyadmin');
400450
await expect(newPage.locator('body')).toContainText('phpMyAdmin');
401451
await expect(newPage.locator('body')).toContainText('wp_posts');
452+
453+
// Browse the "wp_posts" table
454+
const wpPostsRow = newPage
455+
.locator('tr')
456+
.filter({ hasText: 'wp_posts' })
457+
.first();
458+
await expect(wpPostsRow).toBeVisible({ timeout: 10000 });
459+
await wpPostsRow.getByRole('link', { name: 'Browse' }).click();
460+
await newPage.waitForLoadState();
461+
const pmaRows = newPage.locator('table.table_results tbody tr');
462+
await expect(pmaRows.first()).toContainText('Welcome to WordPress.');
463+
464+
// Click "edit" on a row
465+
await pmaRows
466+
.first()
467+
.getByRole('link', { name: 'Edit' })
468+
.first()
469+
.click();
470+
await newPage.waitForLoadState();
471+
const pmaForm = newPage.locator(
472+
'form#insertForm, form[name="insertForm"]'
473+
);
474+
await expect(pmaForm).toBeVisible({ timeout: 10000 });
475+
await expect(pmaForm).toContainText('Welcome to WordPress.');
476+
477+
// Update the post content
478+
const postContentRow = pmaForm
479+
.locator('tr')
480+
.filter({ hasText: 'post_content' })
481+
.first();
482+
const postContentTextarea = postContentRow.locator('textarea').first();
483+
await postContentTextarea.click();
484+
await postContentTextarea.clear();
485+
await postContentTextarea.fill('Updated post content.');
486+
await newPage.getByRole('button', { name: 'Go' }).first().click();
487+
488+
// Verify the updated content
489+
await newPage.waitForLoadState();
490+
await expect(
491+
newPage.locator('table.table_results tbody tr').first()
492+
).toContainText('Updated post content.');
493+
494+
// Go to SQL tab and execute "SHOW TABLES"
495+
await newPage
496+
.locator('#topmenu')
497+
.getByRole('link', { name: 'SQL' })
498+
.click();
499+
await newPage.waitForLoadState();
500+
await newPage.locator('.CodeMirror').click();
501+
await newPage.keyboard.type('SHOW TABLES');
502+
await newPage.getByRole('button', { name: 'Go' }).click();
503+
await newPage.waitForLoadState();
504+
await expect(newPage.locator('body')).toContainText('wp_posts');
505+
402506
await newPage.close();
403507
});
404508
});

packages/playground/website/src/components/site-manager/site-database-panel/adminer-extensions/adminer-mysql-on-sqlite-driver.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,16 @@ function fetch_row() {
193193
}
194194

195195
function fetch_field(): \stdClass {
196-
$column = (object) $this->columns[$this->col_offset++];
197-
$column->type = $column->{'mysqli:type'};
198-
$column->charsetnr = $column->{'mysqli:charsetnr'};
199-
return $column;
196+
$column = $this->columns[$this->col_offset++];
197+
198+
// Adminer expects MySQLi-like column metadata rather than PDO syntax.
199+
// The SQLite driver provides it in "mysqli:" prefixed metadata keys.
200+
foreach ($column as $key => $value) {
201+
if (strpos($key, 'mysqli:') === 0) {
202+
$column[substr($key, 7)] = $value;
203+
}
204+
}
205+
return (object) $column;
200206
}
201207
}
202208

packages/playground/website/src/components/site-manager/site-database-panel/phpmyadmin-extensions/DbiMysqli.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,39 @@ public function getFieldNames(): array {
193193
public function getFieldsMeta(): array {
194194
$meta = array();
195195
foreach ($this->columns as $column) {
196-
$meta[] = new FieldMetadata($column['mysqli:type'], $column['mysqli:flags'], (object) $column);
196+
$flags = $column['flags'] ?? array();
197+
198+
// PhpMyAdmin expects MySQLi-like column metadata rather than PDO syntax.
199+
// The SQLite driver provides it in "mysqli:" prefixed metadata keys.
200+
foreach ($column as $key => $value) {
201+
if (strpos($key, 'mysqli:') === 0) {
202+
$column[substr($key, 7)] = $value;
203+
}
204+
}
205+
206+
// Convert PDO-style flags array to MySQLi-style integer bitmask.
207+
// TODO: Remove this when the driver implements "mysqli:flags".
208+
$mysqli_flags = 0;
209+
foreach ($flags as $flag) {
210+
switch ($flag) {
211+
case 'primary_key':
212+
$mysqli_flags |= \MYSQLI_PRI_KEY_FLAG;
213+
break;
214+
case 'unique_key':
215+
$mysqli_flags |= \MYSQLI_UNIQUE_KEY_FLAG;
216+
break;
217+
case 'not_null':
218+
$mysqli_flags |= \MYSQLI_NOT_NULL_FLAG;
219+
break;
220+
case 'auto_increment':
221+
$mysqli_flags |= \MYSQLI_AUTO_INCREMENT_FLAG;
222+
break;
223+
}
224+
}
225+
$column['flags'] = $mysqli_flags;
226+
227+
$field = (object) $column;
228+
$meta[] = new FieldMetadata($field->type, $field->flags, $field);
197229
}
198230
return $meta;
199231
}

0 commit comments

Comments
 (0)