mirror of
https://github.com/databasus/databasus.git
synced 2026-04-05 16:22:00 +02:00
FEATURE (clipboard): Add parsing from clipboard via dialog in HTTP\no navigator mode
This commit is contained in:
@@ -1566,6 +1566,13 @@ export const ReactComponent = ({ someValue }: Props): JSX.Element => {
|
||||
- **Calculated values** - Derived data from props/state
|
||||
- **Return** - JSX markup
|
||||
|
||||
### Clipboard operations
|
||||
|
||||
Always use `ClipboardHelper` (`shared/lib/ClipboardHelper.ts`) for clipboard operations — never call `navigator.clipboard` directly.
|
||||
|
||||
- **Copy:** `ClipboardHelper.copyToClipboard(text)` — uses `navigator.clipboard` with `execCommand('copy')` fallback for non-secure contexts (HTTP).
|
||||
- **Paste:** Check `ClipboardHelper.isClipboardApiAvailable()` first. If available, use `ClipboardHelper.readFromClipboard()`. If not, show `ClipboardPasteModalComponent` (`shared/ui`) which lets the user paste manually via a text input modal.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package secrets
|
||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
||||
import { getApplicationServer } from '../../../constants';
|
||||
import { type Backup, PgWalBackupType } from '../../../entity/backups';
|
||||
import { type Database } from '../../../entity/databases';
|
||||
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||
import { getUserTimeFormat } from '../../../shared/time';
|
||||
|
||||
interface Props {
|
||||
@@ -26,7 +27,7 @@ export const AgentRestoreComponent = ({ database, backup }: Props) => {
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
await ClipboardHelper.copyToClipboard(text);
|
||||
message.success('Copied to clipboard');
|
||||
} catch {
|
||||
message.error('Failed to copy');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||
|
||||
interface DbSizeCommand {
|
||||
label: string;
|
||||
code: string;
|
||||
@@ -44,7 +46,7 @@ export function DbSizeCommands({ commands }: Props) {
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(cmd.code);
|
||||
await ClipboardHelper.copyToClipboard(cmd.code);
|
||||
setCopiedIndex(index);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
} catch {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
|
||||
import { getApplicationServer } from '../../../constants';
|
||||
import { type Database, databaseApi } from '../../../entity/databases';
|
||||
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||
|
||||
type Architecture = 'amd64' | 'arm64';
|
||||
type PgDeploymentType = 'system' | 'folder' | 'docker';
|
||||
@@ -42,7 +43,7 @@ export const AgentInstallationComponent = ({ database, onTokenGenerated }: Props
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
await ClipboardHelper.copyToClipboard(text);
|
||||
message.success('Copied to clipboard');
|
||||
} catch {
|
||||
message.error('Failed to copy');
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
||||
import { IS_CLOUD } from '../../../../constants';
|
||||
import { type Database, databaseApi } from '../../../../entity/databases';
|
||||
import { MariadbConnectionStringParser } from '../../../../entity/databases/model/mariadb/MariadbConnectionStringParser';
|
||||
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||
import { ToastHelper } from '../../../../shared/toast';
|
||||
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||
|
||||
interface Props {
|
||||
database: Database;
|
||||
@@ -49,41 +51,52 @@ export const EditMariaDbSpecificDataComponent = ({
|
||||
const hasAdvancedValues = !!database.mariadb?.isExcludeEvents;
|
||||
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
||||
|
||||
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||
|
||||
const applyConnectionString = (text: string) => {
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = MariadbConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.mariadb) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
mariadb: {
|
||||
...editingDatabase.mariadb,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
database: result.database,
|
||||
isHttps: result.isHttps,
|
||||
},
|
||||
};
|
||||
|
||||
setEditingDatabase(updatedDatabase);
|
||||
setIsConnectionTested(false);
|
||||
message.success('Connection string parsed successfully');
|
||||
};
|
||||
|
||||
const parseFromClipboard = async () => {
|
||||
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||
setIsShowPasteModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = MariadbConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.mariadb) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
mariadb: {
|
||||
...editingDatabase.mariadb,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
database: result.database,
|
||||
isHttps: result.isHttps,
|
||||
},
|
||||
};
|
||||
|
||||
setEditingDatabase(updatedDatabase);
|
||||
setIsConnectionTested(false);
|
||||
message.success('Connection string parsed successfully');
|
||||
const text = await ClipboardHelper.readFromClipboard();
|
||||
applyConnectionString(text);
|
||||
} catch {
|
||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||
}
|
||||
@@ -408,6 +421,15 @@ export const EditMariaDbSpecificDataComponent = ({
|
||||
list.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ClipboardPasteModalComponent
|
||||
open={isShowPasteModal}
|
||||
onSubmit={(text) => {
|
||||
setIsShowPasteModal(false);
|
||||
applyConnectionString(text);
|
||||
}}
|
||||
onCancel={() => setIsShowPasteModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
||||
import { IS_CLOUD } from '../../../../constants';
|
||||
import { type Database, databaseApi } from '../../../../entity/databases';
|
||||
import { MongodbConnectionStringParser } from '../../../../entity/databases/model/mongodb/MongodbConnectionStringParser';
|
||||
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||
import { ToastHelper } from '../../../../shared/toast';
|
||||
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||
|
||||
interface Props {
|
||||
database: Database;
|
||||
@@ -52,56 +54,65 @@ export const EditMongoDbSpecificDataComponent = ({
|
||||
!!database.mongodb?.isDirectConnection;
|
||||
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
||||
|
||||
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||
|
||||
const applyConnectionString = (text: string) => {
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = MongodbConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.mongodb) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
mongodb: {
|
||||
...editingDatabase.mongodb,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password || '',
|
||||
database: result.database,
|
||||
authDatabase: result.authDatabase,
|
||||
isHttps: result.useTls,
|
||||
isSrv: result.isSrv,
|
||||
isDirectConnection: result.isDirectConnection,
|
||||
cpuCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
if (result.isSrv || result.isDirectConnection) {
|
||||
setShowAdvanced(true);
|
||||
}
|
||||
|
||||
setEditingDatabase(updatedDatabase);
|
||||
setIsConnectionTested(false);
|
||||
|
||||
if (!result.password) {
|
||||
message.warning('Connection string parsed successfully. Please enter the password manually.');
|
||||
} else {
|
||||
message.success('Connection string parsed successfully');
|
||||
}
|
||||
};
|
||||
|
||||
const parseFromClipboard = async () => {
|
||||
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||
setIsShowPasteModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = MongodbConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.mongodb) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
mongodb: {
|
||||
...editingDatabase.mongodb,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password || '',
|
||||
database: result.database,
|
||||
authDatabase: result.authDatabase,
|
||||
isHttps: result.useTls,
|
||||
isSrv: result.isSrv,
|
||||
isDirectConnection: result.isDirectConnection,
|
||||
cpuCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
if (result.isSrv || result.isDirectConnection) {
|
||||
setShowAdvanced(true);
|
||||
}
|
||||
|
||||
setEditingDatabase(updatedDatabase);
|
||||
setIsConnectionTested(false);
|
||||
|
||||
if (!result.password) {
|
||||
message.warning(
|
||||
'Connection string parsed successfully. Please enter the password manually.',
|
||||
);
|
||||
} else {
|
||||
message.success('Connection string parsed successfully');
|
||||
}
|
||||
const text = await ClipboardHelper.readFromClipboard();
|
||||
applyConnectionString(text);
|
||||
} catch {
|
||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||
}
|
||||
@@ -501,6 +512,15 @@ export const EditMongoDbSpecificDataComponent = ({
|
||||
list.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ClipboardPasteModalComponent
|
||||
open={isShowPasteModal}
|
||||
onSubmit={(text) => {
|
||||
setIsShowPasteModal(false);
|
||||
applyConnectionString(text);
|
||||
}}
|
||||
onCancel={() => setIsShowPasteModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
||||
import { IS_CLOUD } from '../../../../constants';
|
||||
import { type Database, databaseApi } from '../../../../entity/databases';
|
||||
import { MySqlConnectionStringParser } from '../../../../entity/databases/model/mysql/MySqlConnectionStringParser';
|
||||
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||
import { ToastHelper } from '../../../../shared/toast';
|
||||
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||
|
||||
interface Props {
|
||||
database: Database;
|
||||
@@ -46,41 +48,52 @@ export const EditMySqlSpecificDataComponent = ({
|
||||
const [isTestingConnection, setIsTestingConnection] = useState(false);
|
||||
const [isConnectionFailed, setIsConnectionFailed] = useState(false);
|
||||
|
||||
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||
|
||||
const applyConnectionString = (text: string) => {
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = MySqlConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.mysql) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
mysql: {
|
||||
...editingDatabase.mysql,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
database: result.database,
|
||||
isHttps: result.isHttps,
|
||||
},
|
||||
};
|
||||
|
||||
setEditingDatabase(updatedDatabase);
|
||||
setIsConnectionTested(false);
|
||||
message.success('Connection string parsed successfully');
|
||||
};
|
||||
|
||||
const parseFromClipboard = async () => {
|
||||
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||
setIsShowPasteModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = MySqlConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.mysql) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
mysql: {
|
||||
...editingDatabase.mysql,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
database: result.database,
|
||||
isHttps: result.isHttps,
|
||||
},
|
||||
};
|
||||
|
||||
setEditingDatabase(updatedDatabase);
|
||||
setIsConnectionTested(false);
|
||||
message.success('Connection string parsed successfully');
|
||||
const text = await ClipboardHelper.readFromClipboard();
|
||||
applyConnectionString(text);
|
||||
} catch {
|
||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||
}
|
||||
@@ -359,6 +372,15 @@ export const EditMySqlSpecificDataComponent = ({
|
||||
list.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ClipboardPasteModalComponent
|
||||
open={isShowPasteModal}
|
||||
onSubmit={(text) => {
|
||||
setIsShowPasteModal(false);
|
||||
applyConnectionString(text);
|
||||
}}
|
||||
onCancel={() => setIsShowPasteModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
||||
import { IS_CLOUD } from '../../../../constants';
|
||||
import { type Database, PostgresBackupType, databaseApi } from '../../../../entity/databases';
|
||||
import { ConnectionStringParser } from '../../../../entity/databases/model/postgresql/ConnectionStringParser';
|
||||
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||
import { ToastHelper } from '../../../../shared/toast';
|
||||
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||
|
||||
interface Props {
|
||||
database: Database;
|
||||
@@ -54,42 +56,53 @@ export const EditPostgreSqlSpecificDataComponent = ({
|
||||
|
||||
const [hasAutoAddedPublicSchema, setHasAutoAddedPublicSchema] = useState(false);
|
||||
|
||||
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||
|
||||
const applyConnectionString = (text: string) => {
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = ConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.postgresql) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
postgresql: {
|
||||
...editingDatabase.postgresql,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
database: result.database,
|
||||
isHttps: result.isHttps,
|
||||
cpuCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
setEditingDatabase(autoAddPublicSchemaForSupabase(updatedDatabase));
|
||||
setIsConnectionTested(false);
|
||||
message.success('Connection string parsed successfully');
|
||||
};
|
||||
|
||||
const parseFromClipboard = async () => {
|
||||
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||
setIsShowPasteModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = ConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.postgresql) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
postgresql: {
|
||||
...editingDatabase.postgresql,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
database: result.database,
|
||||
isHttps: result.isHttps,
|
||||
cpuCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
setEditingDatabase(autoAddPublicSchemaForSupabase(updatedDatabase));
|
||||
setIsConnectionTested(false);
|
||||
message.success('Connection string parsed successfully');
|
||||
const text = await ClipboardHelper.readFromClipboard();
|
||||
applyConnectionString(text);
|
||||
} catch {
|
||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||
}
|
||||
@@ -603,6 +616,15 @@ export const EditPostgreSqlSpecificDataComponent = ({
|
||||
<div>
|
||||
{renderBackupTypeSelector()}
|
||||
{renderFormContent()}
|
||||
|
||||
<ClipboardPasteModalComponent
|
||||
open={isShowPasteModal}
|
||||
onSubmit={(text) => {
|
||||
setIsShowPasteModal(false);
|
||||
applyConnectionString(text);
|
||||
}}
|
||||
onCancel={() => setIsShowPasteModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import type { Backup } from '../../../entity/backups';
|
||||
import { type Database, DatabaseType } from '../../../entity/databases';
|
||||
import { type Restore, RestoreStatus, restoreApi } from '../../../entity/restores';
|
||||
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||
import { getUserTimeFormat } from '../../../shared/time';
|
||||
import { ConfirmationComponent } from '../../../shared/ui';
|
||||
import { EditDatabaseSpecificDataComponent } from '../../databases/ui/edit/EditDatabaseSpecificDataComponent';
|
||||
@@ -328,7 +329,7 @@ export const RestoresComponent = ({ database, backup }: Props) => {
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(showingRestoreError.failMessage || '');
|
||||
ClipboardHelper.copyToClipboard(showingRestoreError.failMessage || '');
|
||||
message.success('Error message copied to clipboard');
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { IS_CLOUD, getApplicationServer } from '../../../constants';
|
||||
import { settingsApi } from '../../../entity/users/api/settingsApi';
|
||||
import type { UsersSettings } from '../../../entity/users/model/UsersSettings';
|
||||
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||
import { AuditLogsComponent } from './AuditLogsComponent';
|
||||
|
||||
interface Props {
|
||||
@@ -247,7 +248,9 @@ export function SettingsComponent({ contentHeight }: Props) {
|
||||
size="small"
|
||||
className="ml-2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${getApplicationServer()}/api/v1/system/health`);
|
||||
ClipboardHelper.copyToClipboard(
|
||||
`${getApplicationServer()}/api/v1/system/health`,
|
||||
);
|
||||
message.success('Health-check endpoint copied to clipboard');
|
||||
}}
|
||||
>
|
||||
|
||||
26
frontend/src/shared/lib/ClipboardHelper.ts
Normal file
26
frontend/src/shared/lib/ClipboardHelper.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export class ClipboardHelper {
|
||||
static isClipboardApiAvailable(): boolean {
|
||||
return !!(navigator.clipboard && window.isSecureContext);
|
||||
}
|
||||
|
||||
static async copyToClipboard(text: string): Promise<void> {
|
||||
if (this.isClipboardApiAvailable()) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
static async readFromClipboard(): Promise<string> {
|
||||
const text = await navigator.clipboard.readText();
|
||||
return text;
|
||||
}
|
||||
}
|
||||
52
frontend/src/shared/ui/ClipboardPasteModalComponent.tsx
Normal file
52
frontend/src/shared/ui/ClipboardPasteModalComponent.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Button, Input, Modal } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onSubmit(text: string): void;
|
||||
onCancel(): void;
|
||||
}
|
||||
|
||||
export function ClipboardPasteModalComponent({ open, onSubmit, onCancel }: Props) {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
onSubmit(trimmed);
|
||||
setValue('');
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setValue('');
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Paste from clipboard"
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={handleCancel}>Cancel</Button>
|
||||
<Button type="primary" disabled={!value.trim()} onClick={handleSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Automatic clipboard access is not available. Please paste your content below.
|
||||
</p>
|
||||
<Input.TextArea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Paste your connection string here..."
|
||||
rows={4}
|
||||
autoFocus
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { ClipboardPasteModalComponent } from './ClipboardPasteModalComponent';
|
||||
export { CloudflareTurnstileWidget } from './CloudflareTurnstileWidget';
|
||||
export { ConfirmationComponent } from './ConfirmationComponent';
|
||||
export { StarButtonComponent } from './StarButtonComponent';
|
||||
|
||||
Reference in New Issue
Block a user