feat(grid): support batch pasting into new rows
This commit is contained in:
parent
9173cb754d
commit
ffde91499a
|
|
@ -2826,6 +2826,7 @@ const {
|
|||
cancelEdit,
|
||||
onEditKeydown,
|
||||
addRow: addEditorRow,
|
||||
appendPastedRowsToNewRow,
|
||||
cloneRow,
|
||||
showDeleteRowConfirm,
|
||||
requestDeleteRow,
|
||||
|
|
@ -2854,6 +2855,7 @@ const {
|
|||
previewChanges,
|
||||
} = editor;
|
||||
const pendingQuickEntryDraftCellFocus = ref<{ rowId: number; col: number } | null>(null);
|
||||
const batchAppendPasteRowId = ref<number | null>(null);
|
||||
|
||||
const showSqlPreview = ref(false);
|
||||
const previewSqlText = ref("");
|
||||
|
|
@ -4819,7 +4821,7 @@ function onCanvasMouseDown(event: MouseEvent) {
|
|||
commitHiddenCanvasEditBeforeCellInteraction();
|
||||
if (!item) return;
|
||||
if (hit.rowNumber) {
|
||||
beginRowSelection(item.displayIndex, item.id, event);
|
||||
onRowNumberMouseDown(item, event);
|
||||
} else {
|
||||
handleDataCellMousedown(item.displayIndex, hit.visibleColIdx, item.id, event);
|
||||
}
|
||||
|
|
@ -5544,7 +5546,51 @@ async function pasteClipboardIntoSelection() {
|
|||
const operation = dataGridResultLifecycle.beginOperation();
|
||||
const text = await readTextFromClipboard();
|
||||
if (!dataGridResultLifecycle.isCurrent(operation)) return;
|
||||
pasteTextIntoSelection(text);
|
||||
pasteTextIntoGrid(text);
|
||||
}
|
||||
|
||||
function batchAppendPasteTargetRowId(): number | null {
|
||||
const rowId = batchAppendPasteRowId.value;
|
||||
if (rowId === null) return null;
|
||||
const item = getRowItem(rowId);
|
||||
if ((!item?.isNew && !item?.isDraft) || item.isDeleted || selectedRowIds.value.size !== 1 || !selectedRowIds.value.has(rowId)) {
|
||||
batchAppendPasteRowId.value = null;
|
||||
return null;
|
||||
}
|
||||
return rowId;
|
||||
}
|
||||
|
||||
function canAppendPastedRows(): boolean {
|
||||
return !!props.editable && batchAppendPasteTargetRowId() !== null;
|
||||
}
|
||||
|
||||
function batchAppendPasteError(reason: string): string {
|
||||
const messages: Record<string, string> = {
|
||||
"not-editable": "grid.batchAppendPasteNotEditable",
|
||||
"invalid-target": "grid.batchAppendPasteInvalidTarget",
|
||||
"target-not-empty": "grid.batchAppendPasteTargetNotEmpty",
|
||||
"empty-paste": "grid.batchAppendPasteEmpty",
|
||||
"readonly-column": "grid.batchAppendPasteReadonlyColumn",
|
||||
};
|
||||
return t(messages[reason] ?? "grid.batchAppendPasteInvalidTarget");
|
||||
}
|
||||
|
||||
function pasteTextIntoGrid(text: string): boolean {
|
||||
const targetRowId = batchAppendPasteTargetRowId();
|
||||
if (targetRowId !== null) {
|
||||
const result = appendPastedRowsToNewRow(targetRowId, parseDataGridClipboard(text), visibleColumnIndexes.value);
|
||||
if (!result.ok) {
|
||||
if (result.reason === "invalid-target" || result.reason === "target-not-empty") {
|
||||
batchAppendPasteRowId.value = null;
|
||||
}
|
||||
toast(batchAppendPasteError(result.reason), 5000);
|
||||
return false;
|
||||
}
|
||||
batchAppendPasteRowId.value = null;
|
||||
toast(t("grid.pasted"));
|
||||
return true;
|
||||
}
|
||||
return pasteTextIntoSelection(text);
|
||||
}
|
||||
|
||||
function pasteTextIntoSelection(text: string): boolean {
|
||||
|
|
@ -5570,12 +5616,12 @@ function pasteTextIntoSelection(text: string): boolean {
|
|||
}
|
||||
|
||||
function onGridPaste(event: ClipboardEvent) {
|
||||
const intent = claimDataGridPaste(event, props.editable, !!selectedRange.value || hasColumnSelection.value);
|
||||
const intent = claimDataGridPaste(event, props.editable, !!selectedRange.value || hasColumnSelection.value || canAppendPastedRows());
|
||||
if (intent === "native") return;
|
||||
if (intent === "block") return;
|
||||
const text = event.clipboardData?.getData("text/plain");
|
||||
if (text === undefined) return;
|
||||
pasteTextIntoSelection(text);
|
||||
pasteTextIntoGrid(text);
|
||||
}
|
||||
|
||||
function pasteStartCell() {
|
||||
|
|
@ -5984,7 +6030,13 @@ async function commitEditFromCellBlur() {
|
|||
await commitEditFromBlur();
|
||||
}
|
||||
|
||||
function onRowNumberMouseDown(item: RowItem, event: MouseEvent) {
|
||||
beginRowSelection(item.displayIndex, item.id, event);
|
||||
batchAppendPasteRowId.value = item.isNew || item.isDraft ? item.id : null;
|
||||
}
|
||||
|
||||
function prepareDataCellMouseDown(item: RowItem, actualColIdx: number) {
|
||||
batchAppendPasteRowId.value = null;
|
||||
const editing = editingCell.value;
|
||||
if (editing?.rowId === quickEntryDraftRowId && item.isDraft && item.id === quickEntryDraftRowId && editing.col !== actualColIdx) {
|
||||
pendingQuickEntryDraftCellFocus.value = { rowId: item.id, col: actualColIdx };
|
||||
|
|
@ -6146,7 +6198,7 @@ async function onGridKeydown(event: KeyboardEvent) {
|
|||
return;
|
||||
}
|
||||
if (clipboardShortcut(event, "v")) {
|
||||
const intent = claimDataGridPaste(event, props.editable, !!selectedRange.value || hasColumnSelection.value);
|
||||
const intent = claimDataGridPaste(event, props.editable, !!selectedRange.value || hasColumnSelection.value || canAppendPastedRows());
|
||||
if (intent === "native") return;
|
||||
// A focused grid owns the shortcut even when read-only; otherwise the webview may paste into the previously focused SQL editor.
|
||||
if (intent === "block") return;
|
||||
|
|
@ -6860,6 +6912,7 @@ watch(editingCell, (cell) => {
|
|||
watch(editValue, scheduleActiveCellEditTextareaResize);
|
||||
|
||||
function onRowContext(rowId: number, rowIndex: number) {
|
||||
batchAppendPasteRowId.value = null;
|
||||
contextHeaderColumn.value = null;
|
||||
contextHeaderColumnIndex.value = null;
|
||||
contextHeaderVisibleColIdx.value = null;
|
||||
|
|
@ -8661,7 +8714,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<div
|
||||
class="data-grid-row-number w-(--row-num-w) shrink-0 px-2 py-1 border-r text-center select-none cursor-default sticky left-0 z-10"
|
||||
:class="[rowNumberStatusClass(item), { 'data-grid-row-number--selected': isRowSelected(item.id) }]"
|
||||
@mousedown="beginRowSelection(item.displayIndex, item.id, $event)"
|
||||
@mousedown="onRowNumberMouseDown(item, $event)"
|
||||
@dblclick.stop="toggleTranspose(item.displayIndex)"
|
||||
@contextmenu="onRowContext(item.id, item.displayIndex)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
import { computed, ref } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, useDataGridEditor } from "@/composables/useDataGridEditor";
|
||||
import type { CellValue } from "@/lib/dataGrid/cellValue";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({}));
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => ({ getConfig: mocks.getConfig }),
|
||||
}));
|
||||
vi.mock("@/stores/historyStore", () => ({
|
||||
useHistoryStore: () => ({}),
|
||||
}));
|
||||
vi.mock("@/stores/productionSafetyStore", () => ({
|
||||
useProductionSafetyStore: () => ({}),
|
||||
}));
|
||||
|
||||
function createEditor(sourceColumns?: Array<string | undefined>) {
|
||||
let editor: ReturnType<typeof useDataGridEditor>;
|
||||
const result = ref<{ columns: string[]; rows: CellValue[][] }>({
|
||||
columns: ["first", "hidden", "last"],
|
||||
rows: [],
|
||||
});
|
||||
|
||||
editor = useDataGridEditor({
|
||||
result: computed(() => result.value),
|
||||
editable: computed(() => true),
|
||||
databaseType: computed(() => "postgres"),
|
||||
connectionId: computed(() => "connection-1"),
|
||||
database: computed(() => "app"),
|
||||
tableMeta: computed(() => ({
|
||||
tableName: "people",
|
||||
columns: [
|
||||
{ name: "first", data_type: "varchar" },
|
||||
{ name: "hidden", data_type: "varchar" },
|
||||
{ name: "last", data_type: "varchar" },
|
||||
],
|
||||
primaryKeys: [],
|
||||
})),
|
||||
sourceColumns: computed(() => sourceColumns),
|
||||
onExecuteSql: computed(() => undefined),
|
||||
sql: computed(() => undefined),
|
||||
searchText: ref(""),
|
||||
whereFilterInput: ref(""),
|
||||
currentWhereInput: computed(() => undefined),
|
||||
orderByInput: ref(""),
|
||||
rowStatusFilter: ref("all"),
|
||||
pageSize: ref(100),
|
||||
currentPage: ref(1),
|
||||
getRowItem: (rowId) => {
|
||||
if (rowId === DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID) {
|
||||
return {
|
||||
id: rowId,
|
||||
data: editor.quickEntryDraftRow.value,
|
||||
isNew: false,
|
||||
isDraft: true,
|
||||
isDeleted: false,
|
||||
isDirtyCol: [false, false, false],
|
||||
status: "draft",
|
||||
};
|
||||
}
|
||||
const newIndex = -rowId - 1;
|
||||
const row = editor.newRows.value[newIndex];
|
||||
if (!row) return undefined;
|
||||
return {
|
||||
id: rowId,
|
||||
newIndex,
|
||||
data: row,
|
||||
isNew: true,
|
||||
isDeleted: false,
|
||||
isDirtyCol: [false, false, false],
|
||||
status: "new",
|
||||
};
|
||||
},
|
||||
emit: vi.fn(),
|
||||
});
|
||||
|
||||
editor.newRows.value = [[null, null, null]];
|
||||
return editor;
|
||||
}
|
||||
|
||||
describe("useDataGridEditor appendPastedRowsToNewRow", () => {
|
||||
beforeEach(() => {
|
||||
mocks.getConfig.mockReturnValue({ id: "connection-1", db_type: "postgres" });
|
||||
});
|
||||
|
||||
it("fills the selected blank new row and appends remaining rows using visible columns", () => {
|
||||
const editor = createEditor();
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(
|
||||
-1,
|
||||
[
|
||||
["Ada", "Lovelace"],
|
||||
["Grace", "Hopper"],
|
||||
],
|
||||
[0, 2],
|
||||
);
|
||||
|
||||
expect(result).toEqual({ ok: true, rowCount: 2 });
|
||||
expect(editor.newRows.value).toEqual([
|
||||
["Ada", null, "Lovelace"],
|
||||
["Grace", null, "Hopper"],
|
||||
]);
|
||||
expect(editor.hasPendingChanges.value).toBe(true);
|
||||
});
|
||||
|
||||
it("fills following blank new rows before adding more rows", () => {
|
||||
const editor = createEditor();
|
||||
editor.newRows.value = [
|
||||
[null, null, null],
|
||||
[null, null, null],
|
||||
];
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(-1, [["Ada"], ["Grace"]], [0, 2]);
|
||||
|
||||
expect(result).toEqual({ ok: true, rowCount: 2 });
|
||||
expect(editor.newRows.value).toEqual([
|
||||
["Ada", null, null],
|
||||
["Grace", null, null],
|
||||
]);
|
||||
});
|
||||
|
||||
it("turns rows pasted into the terminal new-row draft into pending rows", () => {
|
||||
const editor = createEditor();
|
||||
editor.newRows.value = [];
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(
|
||||
DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID,
|
||||
[
|
||||
["Ada", "Lovelace"],
|
||||
["Grace", "Hopper"],
|
||||
],
|
||||
[0, 2],
|
||||
);
|
||||
|
||||
expect(result).toEqual({ ok: true, rowCount: 2 });
|
||||
expect(editor.newRows.value).toEqual([
|
||||
["Ada", null, "Lovelace"],
|
||||
["Grace", null, "Hopper"],
|
||||
]);
|
||||
expect(editor.quickEntryDraftRow.value).toEqual([null, null, null]);
|
||||
expect(editor.hasPendingChanges.value).toBe(true);
|
||||
|
||||
editor.undoPendingChange();
|
||||
expect(editor.newRows.value).toEqual([]);
|
||||
expect(editor.quickEntryDraftRow.value).toEqual([null, null, null]);
|
||||
|
||||
editor.redoPendingChange();
|
||||
expect(editor.newRows.value).toEqual([
|
||||
["Ada", null, "Lovelace"],
|
||||
["Grace", null, "Hopper"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects a non-empty terminal new-row draft", () => {
|
||||
const editor = createEditor();
|
||||
editor.newRows.value = [];
|
||||
editor.quickEntryDraftRow.value = ["already", null, null];
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, [["Ada"]], [0, 2]);
|
||||
|
||||
expect(result).toEqual({ ok: false, reason: "target-not-empty" });
|
||||
expect(editor.newRows.value).toEqual([]);
|
||||
expect(editor.quickEntryDraftRow.value).toEqual(["already", null, null]);
|
||||
});
|
||||
|
||||
it("truncates pasted columns that exceed the visible table columns", () => {
|
||||
const editor = createEditor();
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(-1, [["Ada", "Byron", "Lovelace"]], [0, 2]);
|
||||
|
||||
expect(result).toEqual({ ok: true, rowCount: 1 });
|
||||
expect(editor.newRows.value).toEqual([["Ada", null, "Byron"]]);
|
||||
expect(editor.canUndoPendingChange.value).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects an empty textual clipboard payload without changing pending rows", () => {
|
||||
const editor = createEditor();
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(-1, [[""]], [0, 2]);
|
||||
|
||||
expect(result).toEqual({ ok: false, reason: "empty-paste" });
|
||||
expect(editor.newRows.value).toEqual([[null, null, null]]);
|
||||
expect(editor.canUndoPendingChange.value).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a paste that targets a read-only visible column", () => {
|
||||
const editor = createEditor(["first", undefined, "last"]);
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(-1, [["Ada"]], [1]);
|
||||
|
||||
expect(result).toEqual({ ok: false, reason: "readonly-column" });
|
||||
expect(editor.newRows.value).toEqual([[null, null, null]]);
|
||||
});
|
||||
|
||||
it("does not overwrite an existing new row selected as the append target", () => {
|
||||
const editor = createEditor();
|
||||
editor.newRows.value = [["already", null, null]];
|
||||
|
||||
const result = editor.appendPastedRowsToNewRow(-1, [["Ada"]], [0, 2]);
|
||||
|
||||
expect(result).toEqual({ ok: false, reason: "target-not-empty" });
|
||||
expect(editor.newRows.value).toEqual([["already", null, null]]);
|
||||
});
|
||||
|
||||
it("treats a batch append as one undoable change", () => {
|
||||
const editor = createEditor();
|
||||
|
||||
editor.appendPastedRowsToNewRow(-1, [["Ada"], ["Grace"]], [0, 2]);
|
||||
editor.undoPendingChange();
|
||||
expect(editor.newRows.value).toEqual([[null, null, null]]);
|
||||
|
||||
editor.redoPendingChange();
|
||||
expect(editor.newRows.value).toEqual([
|
||||
["Ada", null, null],
|
||||
["Grace", null, null],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -31,6 +31,8 @@ export const DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID = Number.MIN_SAFE_INTEGER;
|
|||
|
||||
type RowKind = "none" | "existing" | "new" | "draft";
|
||||
|
||||
export type DataGridAppendPastedRowsResult = { ok: true; rowCount: number } | { ok: false; reason: "not-editable" | "invalid-target" | "target-not-empty" | "empty-paste" | "readonly-column" };
|
||||
|
||||
type CommitEditResult =
|
||||
| {
|
||||
changed: false;
|
||||
|
|
@ -944,6 +946,67 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
});
|
||||
}
|
||||
|
||||
function isBlankNewRow(row: readonly CellValue[]): boolean {
|
||||
return row.every((value) => value === null || (typeof value === "string" && value.trim() === ""));
|
||||
}
|
||||
|
||||
function appendPastedRowsToNewRow(targetRowId: number, pastedRows: readonly (readonly (string | null)[])[], columnIndexes: readonly number[]): DataGridAppendPastedRowsResult {
|
||||
if (!editable.value) return { ok: false, reason: "not-editable" };
|
||||
if (pastedRows.every((row) => row.every((value) => value === ""))) {
|
||||
return { ok: false, reason: "empty-paste" };
|
||||
}
|
||||
|
||||
const target = getRowItem(targetRowId);
|
||||
if ((!target?.isNew && !target?.isDraft) || target.isDeleted || isSavingNewRow(target)) {
|
||||
return { ok: false, reason: "invalid-target" };
|
||||
}
|
||||
|
||||
const targetIsDraft = target.isDraft === true;
|
||||
if (targetIsDraft) ensureQuickEntryDraftRow();
|
||||
const targetNewIndex = target.newIndex;
|
||||
const targetRow = targetIsDraft ? quickEntryDraftRow.value : targetNewIndex === undefined ? undefined : newRows.value[targetNewIndex];
|
||||
if (!targetRow || !isBlankNewRow(targetRow)) return { ok: false, reason: "target-not-empty" };
|
||||
|
||||
const pastedColumnCount = Math.max(...pastedRows.map((row) => row.length));
|
||||
if (pastedColumnCount <= 0) return { ok: false, reason: "empty-paste" };
|
||||
|
||||
const targetColumns = columnIndexes.slice(0, pastedColumnCount);
|
||||
if (targetColumns.some((columnIndex) => !canEditColumn(columnIndex))) return { ok: false, reason: "readonly-column" };
|
||||
|
||||
const nextRows = newRows.value.map((row) => [...row]);
|
||||
let reusableNewRowCount = 0;
|
||||
if (!targetIsDraft) {
|
||||
for (let rowIndex = targetNewIndex!; rowIndex < nextRows.length && reusableNewRowCount < pastedRows.length; rowIndex++) {
|
||||
if (!isBlankNewRow(nextRows[rowIndex]!)) break;
|
||||
reusableNewRowCount++;
|
||||
}
|
||||
}
|
||||
const mappedRows = pastedRows.map((pastedRow, rowIndex) => {
|
||||
const nextRow = rowIndex < reusableNewRowCount ? nextRows[targetNewIndex! + rowIndex]! : emptyDraftRow();
|
||||
for (let columnOffset = 0; columnOffset < Math.min(pastedRow.length, targetColumns.length); columnOffset++) {
|
||||
const columnIndex = targetColumns[columnOffset]!;
|
||||
const value = pastedRow[columnOffset];
|
||||
nextRow[columnIndex] = value === null ? null : coerceCellValue(value, nextRow[columnIndex], columnIndex);
|
||||
}
|
||||
return nextRow;
|
||||
});
|
||||
|
||||
pushUndoSnapshot();
|
||||
if (targetIsDraft) {
|
||||
nextRows.push(...mappedRows);
|
||||
quickEntryDraftRow.value = emptyDraftRow();
|
||||
} else {
|
||||
nextRows.splice(targetNewIndex!, reusableNewRowCount, ...mappedRows);
|
||||
}
|
||||
newRows.value = nextRows;
|
||||
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
|
||||
touchPendingChanges();
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
return { ok: true, rowCount: mappedRows.length };
|
||||
}
|
||||
|
||||
function clonedRowData(item: RowItem): CellValue[] {
|
||||
const columnInfoByName = new Map((tableMeta.value?.columns ?? []).map((column) => [column.name.toLowerCase(), column]));
|
||||
return item.data.map((val, i) => {
|
||||
|
|
@ -1566,6 +1629,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
cancelEdit,
|
||||
onEditKeydown,
|
||||
addRow,
|
||||
appendPastedRowsToNewRow,
|
||||
cloneRow,
|
||||
cloneRows,
|
||||
applyDeleteRows,
|
||||
|
|
|
|||
|
|
@ -1227,6 +1227,11 @@ export default {
|
|||
freezeSelectedColumns: "Freeze Selected Columns",
|
||||
unfreezeColumns: "Unfreeze Columns",
|
||||
pasted: "Pasted!",
|
||||
batchAppendPasteNotEditable: "This result cannot be edited.",
|
||||
batchAppendPasteInvalidTarget: "Select the row number of an empty new row before pasting.",
|
||||
batchAppendPasteTargetNotEmpty: "The selected new row already contains data.",
|
||||
batchAppendPasteEmpty: "The clipboard does not contain rows to add.",
|
||||
batchAppendPasteReadonlyColumn: "The pasted data includes a column that cannot be edited.",
|
||||
search: "Search...",
|
||||
searchOrWhere: "Search, or enter a WHERE clause...",
|
||||
applyWhere: "Apply WHERE",
|
||||
|
|
|
|||
|
|
@ -1343,6 +1343,11 @@ export default withEnglishFallback({
|
|||
numericColumnAlign: "Alineación de columna numérica",
|
||||
numericColumnAlignLeft: "Alineación izquierda",
|
||||
numericColumnAlignRight: "Alineación derecha",
|
||||
batchAppendPasteNotEditable: "El resultado actual no se puede editar.",
|
||||
batchAppendPasteInvalidTarget: "Primero seleccione una celda de número de fila de una nueva fila vacía y luego pegue.",
|
||||
batchAppendPasteTargetNotEmpty: "La nueva fila seleccionada ya contiene datos.",
|
||||
batchAppendPasteEmpty: "No hay filas nuevas en el portapapeles.",
|
||||
batchAppendPasteReadonlyColumn: "Los datos pegados contienen columnas no editables.",
|
||||
xlsxHeaderTitle: "Formato de encabezado",
|
||||
xlsxHeaderPrompt: "Seleccione el formato de encabezado al exportar a Excel:",
|
||||
xlsxHeaderOriginal: "Encabezado usando nombres de campos",
|
||||
|
|
|
|||
|
|
@ -1341,6 +1341,11 @@ export default withEnglishFallback({
|
|||
numericColumnAlign: "Allineamento colonna numerica",
|
||||
numericColumnAlignLeft: "Allineamento a sinistra",
|
||||
numericColumnAlignRight: "Allineamento a destra",
|
||||
batchAppendPasteNotEditable: "Il risultato corrente non è modificabile.",
|
||||
batchAppendPasteInvalidTarget: "Seleziona prima una cella vuota della riga nuova, poi incolla.",
|
||||
batchAppendPasteTargetNotEmpty: "La riga nuova selezionata contiene già dati.",
|
||||
batchAppendPasteEmpty: "Negli appunti non ci sono righe da aggiungere.",
|
||||
batchAppendPasteReadonlyColumn: "I dati incollati contengono colonne non modificabili.",
|
||||
xlsxHeaderTitle: "Formato intestazione",
|
||||
xlsxHeaderPrompt: "Seleziona il formato dell'intestazione da utilizzare per l'esportazione in Excel:",
|
||||
xlsxHeaderOriginal: "Intestazione con nome campo",
|
||||
|
|
|
|||
|
|
@ -1342,6 +1342,11 @@ export default withEnglishFallback({
|
|||
numericColumnAlign: "数値列の配置",
|
||||
numericColumnAlignLeft: "左揃え",
|
||||
numericColumnAlignRight: "右揃え",
|
||||
batchAppendPasteNotEditable: "現在の結果は編集できません。",
|
||||
batchAppendPasteInvalidTarget: "空の新規行の行番号セルを選択してから貼り付けてください。",
|
||||
batchAppendPasteTargetNotEmpty: "選択した新規行には既にデータが含まれています。",
|
||||
batchAppendPasteEmpty: "クリップボードに追加可能な行がありません。",
|
||||
batchAppendPasteReadonlyColumn: "貼り付けたデータに編集不可能な列が含まれています。",
|
||||
xlsxHeaderTitle: "ヘッダー形式",
|
||||
xlsxHeaderPrompt: "Excel エクスポート時に使用するヘッダー形式を選択してください:",
|
||||
xlsxHeaderOriginal: "ヘッダーにフィールド名を使用",
|
||||
|
|
|
|||
|
|
@ -1343,6 +1343,11 @@ export default withEnglishFallback({
|
|||
numericColumnAlign: "Alinhamento de colunas numéricas",
|
||||
numericColumnAlignLeft: "Alinhamento à esquerda",
|
||||
numericColumnAlignRight: "Alinhamento à direita",
|
||||
batchAppendPasteNotEditable: "O resultado atual não é editável.",
|
||||
batchAppendPasteInvalidTarget: "Selecione primeiro o número da linha de uma nova linha vazia e depois cole.",
|
||||
batchAppendPasteTargetNotEmpty: "A nova linha selecionada já contém dados.",
|
||||
batchAppendPasteEmpty: "Não há linhas para adicionar na área de transferência.",
|
||||
batchAppendPasteReadonlyColumn: "Os dados colados contêm colunas não editáveis.",
|
||||
xlsxHeaderTitle: "Formato do cabeçalho",
|
||||
xlsxHeaderPrompt: "Selecione o formato do cabeçalho a ser usado ao exportar Excel:",
|
||||
xlsxHeaderOriginal: "Cabeçalho usa nome do campo",
|
||||
|
|
|
|||
|
|
@ -1228,6 +1228,11 @@ export default withEnglishFallback({
|
|||
freezeSelectedColumns: "冻结选中列",
|
||||
unfreezeColumns: "取消冻结",
|
||||
pasted: "已粘贴!",
|
||||
batchAppendPasteNotEditable: "当前结果不可编辑。",
|
||||
batchAppendPasteInvalidTarget: "请先选中一个空新增行的行号单元格,再进行粘贴。",
|
||||
batchAppendPasteTargetNotEmpty: "选中的新增行已包含数据。",
|
||||
batchAppendPasteEmpty: "剪贴板中没有可新增的行。",
|
||||
batchAppendPasteReadonlyColumn: "粘贴的数据包含不可编辑的列。",
|
||||
search: "搜索...",
|
||||
searchOrWhere: "搜索,或输入 WHERE 条件...",
|
||||
applyWhere: "应用 WHERE",
|
||||
|
|
|
|||
|
|
@ -1342,6 +1342,11 @@ export default withEnglishFallback({
|
|||
numericColumnAlign: "數值列對齊",
|
||||
numericColumnAlignLeft: "左對齊",
|
||||
numericColumnAlignRight: "右對齊",
|
||||
batchAppendPasteNotEditable: "目前結果不可編輯。",
|
||||
batchAppendPasteInvalidTarget: "請先選取一個空白新增行的行號儲存格,再進行貼上。",
|
||||
batchAppendPasteTargetNotEmpty: "選取的新增行已包含資料。",
|
||||
batchAppendPasteEmpty: "剪貼簿中沒有可新增的行。",
|
||||
batchAppendPasteReadonlyColumn: "貼上的資料包含不可編輯的列。",
|
||||
xlsxHeaderTitle: "表頭格式",
|
||||
xlsxHeaderPrompt: "請選擇匯出 Excel 時使用的表頭格式:",
|
||||
xlsxHeaderOriginal: "表頭使用欄位名稱",
|
||||
|
|
|
|||
Loading…
Reference in New Issue