Add copy structure controller and clipboard functionality

Signed-off-by: Satyam <satyam.gupta756@gmail.com>
This commit is contained in:
Satyam 2026-04-10 13:25:55 +05:30
parent e18b197a11
commit f8e914075b
7 changed files with 168 additions and 2 deletions

View File

@ -326,6 +326,10 @@ return [
'class' => Database\Structure\ShowCreateController::class,
'arguments' => [ResponseRenderer::class, Template::class, DatabaseInterface::class],
],
Database\Structure\CopyStructureController::class => [
'class' => Database\Structure\CopyStructureController::class,
'arguments' => [ResponseRenderer::class, DatabaseInterface::class, DbTableExists::class],
],
Database\StructureController::class => [
'class' => Database\StructureController::class,
'arguments' => [
@ -1131,6 +1135,10 @@ return [
DbTableExists::class,
],
],
Table\Structure\CopyStructureController::class => [
'class' => Table\Structure\CopyStructureController::class,
'arguments' => [ResponseRenderer::class, DatabaseInterface::class, DbTableExists::class],
],
Table\Structure\ReservedWordCheckController::class => [
'class' => Table\Structure\ReservedWordCheckController::class,
'arguments' => [ResponseRenderer::class, Config::class],

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import * as bootstrap from 'bootstrap';
import { AJAX } from '../modules/ajax.ts';
import { getForeignKeyCheckboxLoader, loadForeignKeyCheckbox } from '../modules/functions.ts';
import { copyToClipboard, displayCopyNotification, getForeignKeyCheckboxLoader, loadForeignKeyCheckbox } from '../modules/functions.ts';
import { Navigation } from '../modules/navigation.ts';
import { CommonParams } from '../modules/common.ts';
import { ajaxRemoveMessage, ajaxShowMessage } from '../modules/ajax-message.ts';
@ -33,6 +33,7 @@ AJAX.registerTeardown('database/structure.js', function () {
$(document).off('click', 'a.truncate_table_anchor.ajax');
$(document).off('click', 'a.drop_table_anchor.ajax');
$(document).off('click', 'a.favorite_table_anchor.ajax');
$(document).off('click', '#copyStructureBtn');
$('a.real_row_count').off('click');
$('a.row_count_sum').off('click');
$('select[name=submit_mult]').off('change');
@ -323,4 +324,40 @@ AJAX.registerOnload('database/structure.js', function () {
event.preventDefault();
fetchRealRowCount($(this));
});
function copyStructureSql (sql: string): void {
if (typeof navigator.clipboard !== 'undefined' && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(sql).then(() => {
displayCopyNotification(true);
}).catch(() => {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
});
} else {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
}
}
$(document).on('click', '#copyStructureBtn', function (event) {
event.preventDefault();
const argsep = CommonParams.get('arg_separator');
const db = CommonParams.get('db');
const data =
'ajax_request=true' + argsep +
'ajax_page_request=true' + argsep +
'token=' + encodeURIComponent(CommonParams.get('token')) + argsep +
'db=' + encodeURIComponent(db);
const $msg = ajaxShowMessage(window.Messages.strProcessingRequest);
$.post('index.php?route=/database/structure/copy-structure', data, function (response) {
ajaxRemoveMessage($msg);
if (typeof response !== 'undefined' && response.success === true && typeof response.sql === 'string') {
copyStructureSql(response.sql);
} else {
const err = typeof response !== 'undefined' && typeof response.error === 'string' ? response.error : '';
ajaxShowMessage(window.Messages.strErrorProcessingRequest + ' : ' + err, false);
}
}, 'json').fail(function () {
ajaxRemoveMessage($msg);
ajaxShowMessage(window.Messages.strErrorProcessingRequest, false);
});
});
});

View File

@ -1,6 +1,6 @@
import $ from 'jquery';
import { AJAX } from '../modules/ajax.ts';
import { checkReservedWordColumns, checkTableEditForm, prepareForAjaxRequest } from '../modules/functions.ts';
import { checkReservedWordColumns, checkTableEditForm, copyToClipboard, displayCopyNotification, prepareForAjaxRequest } from '../modules/functions.ts';
import { Navigation } from '../modules/navigation.ts';
import { CommonParams } from '../modules/common.ts';
import highlightSql from '../modules/sql-highlight.ts';
@ -60,6 +60,7 @@ AJAX.registerTeardown('table/structure.js', function () {
$('body').off('click', '#fieldsForm button.mult_submit');
$(document).off('click', 'a[id^=partition_action].ajax');
$(document).off('click', '#remove_partitioning.ajax');
$(document).off('click', '#copyTableStructureBtn');
});
AJAX.registerOnload('table/structure.js', function () {
@ -486,4 +487,42 @@ AJAX.registerOnload('table/structure.js', function () {
$(document).on('change', 'select[name=after_field]', function () {
checkFirst();
});
function copyTableStructureSql (sql: string): void {
if (typeof navigator.clipboard !== 'undefined' && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(sql).then(() => {
displayCopyNotification(true);
}).catch(() => {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
});
} else {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
}
}
$(document).on('click', '#copyTableStructureBtn', function (event) {
event.preventDefault();
const argsep = CommonParams.get('arg_separator');
const db = CommonParams.get('db');
const table = CommonParams.get('table');
const data =
'ajax_request=true' + argsep +
'ajax_page_request=true' + argsep +
'token=' + encodeURIComponent(CommonParams.get('token')) + argsep +
'db=' + encodeURIComponent(db) + argsep +
'table=' + encodeURIComponent(table);
const $msg = ajaxShowMessage(window.Messages.strProcessingRequest);
$.post('index.php?route=/table/structure/copy-structure', data, function (response) {
ajaxRemoveMessage($msg);
if (typeof response !== 'undefined' && response.success === true && typeof response.sql === 'string') {
copyTableStructureSql(response.sql);
} else {
const err = typeof response !== 'undefined' && typeof response.error === 'string' ? response.error : '';
ajaxShowMessage(window.Messages.strErrorProcessingRequest + ' : ' + err, false);
}
}, 'json').fail(function () {
ajaxRemoveMessage($msg);
ajaxShowMessage(window.Messages.strErrorProcessingRequest, false);
});
});
});

View File

@ -11,6 +11,9 @@
<hr>
<p class="d-print-none">
<button type="button" class="btn btn-link p-0 jsPrintButton">{{ get_icon('b_print', t('Print'), true) }}</button>
<button type="button" id="copyStructureBtn" class="btn btn-link p-0">
{{ get_icon('b_export', t('Copy Structure'), true) }}
</button>
<a href="{{ url('/database/data-dictionary', {'db': database, 'goto': url('/database/structure')}) }}">
{{ get_icon('b_tblanalyse', t('Data dictionary'), true) }}
</a>

View File

@ -624,5 +624,12 @@
{% if show_stats %}
{{ table_stats|raw }}
{% endif %}
{% if not db_is_system_schema %}
<p class="d-print-none mt-2">
<button type="button" id="copyTableStructureBtn" class="btn btn-link p-0">
{{ get_icon('b_export', t('Copy Table Structure'), true) }}
</button>
</p>
{% endif %}
<div class="clearfloat"></div>
{% endblock %}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\InvocableController;
use PhpMyAdmin\Current;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Identifiers\DatabaseName;
use PhpMyAdmin\Identifiers\TableName;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Routing\Route;
use function __;
#[Route('/table/structure/copy-structure', ['POST'])]
final readonly class CopyStructureController implements InvocableController
{
public function __construct(
private ResponseRenderer $response,
private DatabaseInterface $dbi,
private DbTableExists $dbTableExists,
) {
}
public function __invoke(ServerRequest $request): Response
{
if (Current::$database === '') {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No databases selected.')));
return $this->response->response();
}
if (Current::$table === '') {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No table selected.')));
return $this->response->response();
}
$this->dbi->selectDb(Current::$database);
$databaseName = DatabaseName::tryFrom($request->getParam('db'));
if ($databaseName === null || ! $this->dbTableExists->selectDatabase($databaseName)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No databases selected.')));
return $this->response->response();
}
$tableName = TableName::tryFrom($request->getParam('table'));
if ($tableName === null || ! $this->dbTableExists->hasTable($databaseName, $tableName)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No table selected.')));
return $this->response->response();
}
$object = $this->dbi->getTable(Current::$database, Current::$table);
$this->response->addJSON('sql', $object->showCreate());
return $this->response->response();
}
}

View File

@ -239,6 +239,7 @@ final class RoutesTest extends TestCase
'/database/structure/central-columns/remove' => CentralColumns\RemoveController::class,
'/database/structure/change-prefix-form' => Database\Structure\ChangePrefixFormController::class,
'/database/structure/copy-form' => Database\Structure\CopyFormController::class,
'/database/structure/copy-structure' => Database\Structure\CopyStructureController::class,
'/database/structure/copy-table' => Database\Structure\CopyTableController::class,
'/database/structure/copy-table-with-prefix' => Database\Structure\CopyTableWithPrefixController::class,
'/database/structure/drop-form' => Database\Structure\DropFormController::class,
@ -357,6 +358,7 @@ final class RoutesTest extends TestCase
'/table/structure/central-columns-add' => Table\Structure\CentralColumnsAddController::class,
'/table/structure/central-columns-remove' => Table\Structure\CentralColumnsRemoveController::class,
'/table/structure/change' => Table\Structure\ChangeController::class,
'/table/structure/copy-structure' => Table\Structure\CopyStructureController::class,
'/table/structure/drop' => Table\DropColumnController::class,
'/table/structure/drop-confirm' => Table\DropColumnConfirmationController::class,
'/table/structure/fulltext' => Table\Structure\FulltextController::class,