diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index d007bfb074..43726b0b8b 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -162,15 +162,8 @@ const getContainerScrollForElement = ( return scroll } -const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange, selected } = useMultiSelect( - fields, - data, - $$(editEnabled), - isPkAvail, - clearCell, - makeEditable, - scrollToCell, - (e: KeyboardEvent) => { +const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyValue, isCellSelected, selectedCell } = + useMultiSelect(fields, data, $$(editEnabled), isPkAvail, clearCell, makeEditable, scrollToCell, (e: KeyboardEvent) => { // ignore navigating if picker(Date, Time, DateTime, Year) // or single/multi select options is open const activePickerOrDropdownEl = document.querySelector( @@ -188,9 +181,9 @@ const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey if (e.key === ' ') { - if (selected.row !== null && !editEnabled) { + if (selectedCell.row !== null && !editEnabled) { e.preventDefault() - const row = data.value[selected.row] + const row = data.value[selectedCell.row] expandForm(row) return true } @@ -208,37 +201,37 @@ const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange if (cmdOrCtrl) { switch (e.key) { case 'ArrowUp': - selected.row = 0 - selected.col = selected.col ?? 0 + selectedCell.row = 0 + selectedCell.col = selectedCell.col ?? 0 scrollToCell?.() editEnabled = false return true case 'ArrowDown': - selected.row = data.value.length - 1 - selected.col = selected.col ?? 0 + selectedCell.row = data.value.length - 1 + selectedCell.col = selectedCell.col ?? 0 scrollToCell?.() editEnabled = false return true case 'ArrowRight': - selected.row = selected.row ?? 0 - selected.col = fields.value?.length - 1 + selectedCell.row = selectedCell.row ?? 0 + selectedCell.col = fields.value?.length - 1 scrollToCell?.() editEnabled = false return true case 'ArrowLeft': - selected.row = selected.row ?? 0 - selected.col = 0 + selectedCell.row = selectedCell.row ?? 0 + selectedCell.col = 0 scrollToCell?.() editEnabled = false return true } } - }, -) + }) function scrollToCell(row?: number | null, col?: number | null) { - row = row ?? selected.row - col = col ?? selected.col + row = row ?? selectedCell.row + col = col ?? selectedCell.col + if (row !== undefined && col !== undefined && row !== null && col !== null) { // get active cell const rows = tbodyEl.value?.querySelectorAll('tr') @@ -393,10 +386,12 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => { /** On clicking outside of table reset active cell */ const smartTable = ref(null) onClickOutside(smartTable, (e) => { - clearRangeRows() - if (selected.col === null) return + // do nothing if context menu was open + if (contextMenu.value) return + clearSelectedRange() + if (selectedCell.col === null) return - const activeCol = fields.value[selected.col] + const activeCol = fields.value[selectedCell.col] if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return @@ -417,25 +412,25 @@ onClickOutside(smartTable, (e) => { return } - selected.row = null - selected.col = null + selectedCell.row = null + selectedCell.col = null }) const onNavigate = (dir: NavigateDir) => { - if (selected.row === null || selected.col === null) return + if (selectedCell.row === null || selectedCell.col === null) return editEnabled = false switch (dir) { case NavigateDir.NEXT: - if (selected.row < data.value.length - 1) { - selected.row++ + if (selectedCell.row < data.value.length - 1) { + selectedCell.row++ } else { addEmptyRow() - selected.row++ + selectedCell.row++ } break case NavigateDir.PREV: - if (selected.row > 0) { - selected.row-- + if (selectedCell.row > 0) { + selectedCell.row-- } break } @@ -696,9 +691,7 @@ watch( :key="columnObj.id" class="cell relative cursor-pointer nc-grid-cell" :class="{ - 'active': - (hasEditPermission && selected.col === colIndex && selected.row === rowIndex) || - (hasEditPermission && selectedRange(rowIndex, colIndex)), + 'active': hasEditPermission && isCellSelected(rowIndex, colIndex), 'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row), }" :data-testid="`cell-${columnObj.title}-${rowIndex}`" @@ -708,7 +701,7 @@ watch( @click="selectCell(rowIndex, colIndex)" @dblclick="makeEditable(row, columnObj)" @mousedown="startSelectRange($event, rowIndex, colIndex)" - @mouseover="selectBlock(rowIndex, colIndex)" + @mouseover="endSelectRange(rowIndex, colIndex)" @contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })" >
@@ -716,7 +709,7 @@ watch( v-if="isVirtualCol(columnObj)" v-model="row.row[columnObj.title]" :column="columnObj" - :active="selected.col === colIndex && selected.row === rowIndex" + :active="selectedCell.col === colIndex && selectedCell.row === rowIndex" :row="row" @navigate="onNavigate" /> @@ -726,10 +719,10 @@ watch( v-model="row.row[columnObj.title]" :column="columnObj" :edit-enabled=" - !!hasEditPermission && !!editEnabled && selected.col === colIndex && selected.row === rowIndex + !!hasEditPermission && !!editEnabled && selectedCell.col === colIndex && selectedCell.row === rowIndex " :row-index="rowIndex" - :active="selected.col === colIndex && selected.row === rowIndex" + :active="selectedCell.col === colIndex && selectedCell.row === rowIndex" @update:edit-enabled="editEnabled = $event" @save="updateOrSaveRow(row, columnObj.title, state)" @navigate="onNavigate" @@ -794,6 +787,13 @@ watch( {{ $t('activity.insertRow') }}
+ + +
+ + {{ $t('general.copy') }} +
+
diff --git a/packages/nc-gui/composables/useMultiSelect/cellRange.ts b/packages/nc-gui/composables/useMultiSelect/cellRange.ts new file mode 100644 index 0000000000..c059cbfe12 --- /dev/null +++ b/packages/nc-gui/composables/useMultiSelect/cellRange.ts @@ -0,0 +1,54 @@ +export interface Cell { + row: number | null + col: number | null +} + +export class CellRange { + _start: Cell | null + _end: Cell | null + + constructor(start = null, end = null) { + this._start = start + this._end = end ?? this._start + } + + get start() { + return { + row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN), + col: Math.min(this._start?.col ?? NaN, this._end?.col ?? NaN), + } + } + + get end() { + return { + row: Math.max(this._start?.row ?? NaN, this._end?.row ?? NaN), + col: Math.max(this._start?.col ?? NaN, this._end?.col ?? NaN), + } + } + + startRange(value: Cell) { + if (value == null) { + return + } + + this._start = value + this._end = value + } + + endRange(value: Cell) { + if (value == null) { + return + } + + this._end = value + } + + clear() { + this._start = null + this._end = null + } + + isEmpty() { + return this._start == null || this._end == null + } +} diff --git a/packages/nc-gui/composables/useMultiSelect/copyValue.ts b/packages/nc-gui/composables/useMultiSelect/copyValue.ts new file mode 100644 index 0000000000..fec82fb7c9 --- /dev/null +++ b/packages/nc-gui/composables/useMultiSelect/copyValue.ts @@ -0,0 +1,29 @@ +import { UITypes } from '../../../nocodb-sdk' +import type { ColumnType } from '../../../nocodb-sdk' +import type { Row } from '~/lib' + +export const copyTable = async (rows: Row[], cols: ColumnType[]) => { + let copyHTML = '' + let copyPlainText = '' + + rows.forEach((row) => { + let copyRow = '' + cols.forEach((col) => { + let value = (col.title && row.row[col.title]) ?? '' + if (typeof value === 'object') { + value = JSON.stringify(value) + } + copyRow += `` + copyPlainText = `${copyPlainText} ${value} \t` + }) + copyHTML += `${copyRow}` + copyPlainText = `${copyPlainText.trim()}\n` + }) + copyHTML += '
${value}
' + copyPlainText.trim() + + const blobHTML = new Blob([copyHTML], { type: 'text/html' }) + const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' }) + + return navigator.clipboard.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText })]) +} diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index 9b08d29554..c27ea269ed 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -1,18 +1,16 @@ import type { MaybeRef } from '@vueuse/core' -import { UITypes } from 'nocodb-sdk' -import { message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports' - -interface SelectedBlock { - row: number | null - col: number | null -} +import type { ColumnType } from 'nocodb-sdk' +import type { Cell } from './cellRange' +import { CellRange } from './cellRange' +import { copyTable, message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports' +import type { Row } from '~/lib' /** * Utility to help with multi-selecting rows/cells in the smartsheet */ export function useMultiSelect( - fields: MaybeRef, - data: MaybeRef, + fields: MaybeRef, + data: MaybeRef, _editEnabled: MaybeRef, isPkAvail: MaybeRef, clearCell: Function, @@ -26,99 +24,92 @@ export function useMultiSelect( const editEnabled = ref(_editEnabled) - const selected = reactive({ row: null, col: null }) - - // save the first and the last column where the mouse is down while the value isSelectedRow is true - const selectedRows = reactive({ startCol: NaN, endCol: NaN, startRow: NaN, endRow: NaN }) - - // calculate the min and the max column where the mouse is down while the value isSelectedRow is true - const rangeRows = reactive({ minRow: NaN, maxRow: NaN, minCol: NaN, maxCol: NaN }) - - // check if mouse is down or up false=mouseup and true=mousedown - let isSelectedBlock = $ref(false) + const selectedCell = reactive({ row: null, col: null }) + const selectedRange = reactive(new CellRange()) + let isMouseDown = $ref(false) const columnLength = $computed(() => unref(fields)?.length) + async function copyValue(ctx?: Cell) { + try { + if (!selectedRange.isEmpty()) { + const cprows = unref(data).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy + const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy + + await copyTable(cprows, cpcols) + message.success(t('msg.info.copiedToClipboard')) + } else { + // if copy was called with context (right click position) - copy value from context + // else if there is just one selected cell, copy it's value + const cpRow = ctx?.row ?? selectedCell?.row + const cpCol = ctx?.col ?? selectedCell?.col + + if (cpRow != null && cpCol != null) { + const rowObj = unref(data)[cpRow] + const columnObj = unref(fields)[cpCol] + + let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || '' + if (typeof textToCopy === 'object') { + textToCopy = JSON.stringify(textToCopy) + } + await copy(textToCopy) + message.success(t('msg.info.copiedToClipboard')) + } + } + } catch { + message.error(t('msg.error.copyToClipboardError')) + } + } + function selectCell(row: number, col: number) { - clearRangeRows() - if (selected.row === row && selected.col === col) return + selectedRange.clear() + if (selectedCell.row === row && selectedCell.col === col) return editEnabled.value = false - selected.row = row - selected.col = col + selectedCell.row = row + selectedCell.col = col } - function selectBlock(row: number, col: number) { - // if selected.col and selected.row are null and isSelectedBlock is true that means you are selecting a block - if (selected.col === null || selected.row === null) { - if (isSelectedBlock) { - // save the next value after the selectionStart - selectedRows.endCol = col - selectedRows.endRow = row - } - } else if (selected.col !== col || selected.row !== row) { - // if selected.col and selected.row is not null but the selected col and row is not equal at the row and col where the mouse is clicking - // and isSelectedBlock is true that means you are selecting a block - if (isSelectedBlock) { - selected.col = null - selected.row = null - // save the next value after the selectionStart - selectedRows.endCol = col - selectedRows.endRow = row - } + function endSelectRange(row: number, col: number) { + if (!isMouseDown) { + return } + selectedCell.row = null + selectedCell.col = null + selectedRange.endRange({ row, col }) } - function selectedRange(row: number, col: number) { - if ( - !isNaN(selectedRows.startRow) && - !isNaN(selectedRows.startCol) && - !isNaN(selectedRows.endRow) && - !isNaN(selectedRows.endCol) - ) { - // check if column selection is up or down - rangeRows.minRow = Math.min(selectedRows.startRow, selectedRows.endRow) - rangeRows.maxRow = Math.max(selectedRows.startRow, selectedRows.endRow) - rangeRows.minCol = Math.min(selectedRows.startCol, selectedRows.endCol) - rangeRows.maxCol = Math.max(selectedRows.startCol, selectedRows.endCol) + function isCellSelected(row: number, col: number) { + if (selectedCell?.row === row && selectedCell?.col === col) { + return true + } - // return if the column is in between the selection - return col >= rangeRows.minCol && col <= rangeRows.maxCol && row >= rangeRows.minRow && row <= rangeRows.maxRow - } else { + if (selectedRange.isEmpty()) { return false } + + return ( + col >= selectedRange.start.col && + col <= selectedRange.end.col && + row >= selectedRange.start.row && + row <= selectedRange.end.row + ) } function startSelectRange(event: MouseEvent, row: number, col: number) { - // if editEnabled but the selected col or the selected row is not equal like the actual row or col, enabled selected multiple rows - if (unref(editEnabled) && (selected.col !== col || selected.row !== row)) { - event.preventDefault() - } else if (!unref(editEnabled)) { - // if editEnabled is not true, enabled selected multiple rows - event.preventDefault() + // if there was a right click on selected range, don't restart the selection + const leftClickButton = 0 + if (event?.button !== leftClickButton && isCellSelected(row, col)) { + return } - // clear the selection when the mouse is down - selectedRows.startCol = NaN - selectedRows.endCol = NaN - selectedRows.startRow = NaN - selectedRows.endRow = NaN - // asing where the selection start - selectedRows.startCol = col - selectedRows.startRow = row - isSelectedBlock = true - } + if (unref(editEnabled)) { + event.preventDefault() + return + } - function clearRangeRows() { - // when the selection starts or ends or when enter/arrow/tab is pressed - // this clear the previous selection - rangeRows.minCol = NaN - rangeRows.maxCol = NaN - rangeRows.minRow = NaN - rangeRows.maxRow = NaN - selectedRows.startRow = NaN - selectedRows.startCol = NaN - selectedRows.endRow = NaN - selectedRows.endCol = NaN + isMouseDown = true + selectedRange.clear() + selectedRange.startRange({ row, col }) } useEventListener(document, 'mouseup', (e) => { @@ -127,7 +118,7 @@ export function useMultiSelect( e.preventDefault() } - isSelectedBlock = false + isMouseDown = false }) const onKeyDown = async (e: KeyboardEvent) => { @@ -136,41 +127,36 @@ export function useMultiSelect( return true } - if ( - !isNaN(selectedRows.startRow) && - !isNaN(selectedRows.startCol) && - !isNaN(selectedRows.endRow) && - !isNaN(selectedRows.endCol) - ) { + if (!selectedRange.isEmpty()) { // In case the user press tabs or arrows keys - selected.row = selectedRows.startRow - selected.col = selectedRows.startCol + selectedCell.row = selectedRange.start.row + selectedCell.col = selectedRange.start.col } - if (selected.row === null || selected.col === null) return + if (selectedCell.row === null || selectedCell.col === null) return /** on tab key press navigate through cells */ switch (e.key) { case 'Tab': e.preventDefault() - clearRangeRows() + selectedRange.clear() if (e.shiftKey) { - if (selected.col > 0) { - selected.col-- + if (selectedCell.col > 0) { + selectedCell.col-- editEnabled.value = false - } else if (selected.row > 0) { - selected.row-- - selected.col = unref(columnLength) - 1 + } else if (selectedCell.row > 0) { + selectedCell.row-- + selectedCell.col = unref(columnLength) - 1 editEnabled.value = false } } else { - if (selected.col < unref(columnLength) - 1) { - selected.col++ + if (selectedCell.col < unref(columnLength) - 1) { + selectedCell.col++ editEnabled.value = false - } else if (selected.row < unref(data).length - 1) { - selected.row++ - selected.col = 0 + } else if (selectedCell.row < unref(data).length - 1) { + selectedCell.row++ + selectedCell.col = 0 editEnabled.value = false } } @@ -179,90 +165,63 @@ export function useMultiSelect( /** on enter key press make cell editable */ case 'Enter': e.preventDefault() - clearRangeRows() - makeEditable(unref(data)[selected.row], unref(fields)[selected.col]) + selectedRange.clear() + makeEditable(unref(data)[selectedCell.row], unref(fields)[selectedCell.col]) break /** on delete key press clear cell */ case 'Delete': e.preventDefault() - clearRangeRows() - await clearCell(selected as { row: number; col: number }) + selectedRange.clear() + await clearCell(selectedCell as { row: number; col: number }) break /** on arrow key press navigate through cells */ case 'ArrowRight': e.preventDefault() - clearRangeRows() - if (selected.col < unref(columnLength) - 1) { - selected.col++ + selectedRange.clear() + if (selectedCell.col < unref(columnLength) - 1) { + selectedCell.col++ scrollToActiveCell?.() editEnabled.value = false } break case 'ArrowLeft': - clearRangeRows() + selectedRange.clear() e.preventDefault() - clearRangeRows() - if (selected.col > 0) { - selected.col-- + if (selectedCell.col > 0) { + selectedCell.col-- scrollToActiveCell?.() editEnabled.value = false } break case 'ArrowUp': - clearRangeRows() + selectedRange.clear() e.preventDefault() - clearRangeRows() - if (selected.row > 0) { - selected.row-- + if (selectedCell.row > 0) { + selectedCell.row-- scrollToActiveCell?.() editEnabled.value = false } break case 'ArrowDown': - clearRangeRows() + selectedRange.clear() e.preventDefault() - clearRangeRows() - if (selected.row < unref(data).length - 1) { - selected.row++ + if (selectedCell.row < unref(data).length - 1) { + selectedCell.row++ scrollToActiveCell?.() editEnabled.value = false } break default: { - const rowObj = unref(data)[selected.row] + const rowObj = unref(data)[selectedCell.row] - const columnObj = unref(fields)[selected.col] - - let cptext = '' // variable for save the text to be copy - - if (!isNaN(rangeRows.minRow) && !isNaN(rangeRows.maxRow) && !isNaN(rangeRows.minCol) && !isNaN(rangeRows.maxCol)) { - const cprows = unref(data).slice(rangeRows.minRow, rangeRows.maxRow + 1) // slice the selected rows for copy - - const cpcols = unref(fields).slice(rangeRows.minCol, rangeRows.maxCol + 1) // slice the selected cols for copy - - cprows.forEach((row) => { - cpcols.forEach((col) => { - // todo: JSON stringify the attachment cell and LTAR contents for copy - // filter attachment cells and LATR cells from copy - if (col.uidt !== UITypes.Attachment && col.uidt !== UITypes.LinkToAnotherRecord) { - cptext = `${cptext} ${row.row[col.title]} \t` - } - }) - - cptext = `${cptext.trim()}\n` - }) - - cptext.trim() - } else { - cptext = rowObj.row[columnObj.title] || '' - } + const columnObj = unref(fields)[selectedCell.col] if ((!unref(editEnabled) && e.metaKey) || e.ctrlKey) { switch (e.keyCode) { // copy - ctrl/cmd +c case 67: - await copy(cptext) + await copyValue() break } } @@ -277,7 +236,7 @@ export function useMultiSelect( // Update not allowed for table which doesn't have primary Key return message.info(t('msg.info.updateNotAllowedWithoutPK')) } - if (makeEditable(rowObj, columnObj)) { + if (makeEditable(rowObj, columnObj) && columnObj.title) { rowObj.row[columnObj.title] = '' } // editEnabled = true @@ -291,12 +250,11 @@ export function useMultiSelect( return { selectCell, - selectBlock, - selectedRange, - clearRangeRows, startSelectRange, - selected, - selectedRows, - rangeRows, + endSelectRange, + clearSelectedRange: selectedRange.clear.bind(selectedRange), + copyValue, + isCellSelected, + selectedCell, } } diff --git a/packages/nc-gui/context/index.ts b/packages/nc-gui/context/index.ts index 21278b9876..5cbff053fa 100644 --- a/packages/nc-gui/context/index.ts +++ b/packages/nc-gui/context/index.ts @@ -26,7 +26,7 @@ export const ReloadViewDataHookInj: InjectionKey> = Sy export const ReloadViewMetaHookInj: InjectionKey> = Symbol('reload-view-meta-injection') export const ReloadRowDataHookInj: InjectionKey> = Symbol('reload-row-data-injection') export const OpenNewRecordFormHookInj: InjectionKey> = Symbol('open-new-record-form-injection') -export const FieldsInj: InjectionKey> = Symbol('fields-injection') +export const FieldsInj: InjectionKey> = Symbol('fields-injection') export const EditModeInj: InjectionKey> = Symbol('edit-mode-injection') export const SharedViewPasswordInj: InjectionKey> = Symbol('shared-view-password-injection') export const CellUrlDisableOverlayInj: InjectionKey> = Symbol('cell-url-disable-url') diff --git a/packages/nc-gui/lang/ar.json b/packages/nc-gui/lang/ar.json index 4b065ce319..51d9564f32 100644 --- a/packages/nc-gui/lang/ar.json +++ b/packages/nc-gui/lang/ar.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "تصدير البيانات الوصفية للمشروع بنجاح", diff --git a/packages/nc-gui/lang/bn_IN.json b/packages/nc-gui/lang/bn_IN.json index aaa7d443f5..6bd3dbea4f 100644 --- a/packages/nc-gui/lang/bn_IN.json +++ b/packages/nc-gui/lang/bn_IN.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "প্রকল্প মেটাডেটা সফলভাবে রফতানি করেছে", diff --git a/packages/nc-gui/lang/da.json b/packages/nc-gui/lang/da.json index 5e07bf3d1f..25b4a074a2 100644 --- a/packages/nc-gui/lang/da.json +++ b/packages/nc-gui/lang/da.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Project Metadata eksporteres med succes", diff --git a/packages/nc-gui/lang/de.json b/packages/nc-gui/lang/de.json index 79ef0efc39..f4e43c0726 100644 --- a/packages/nc-gui/lang/de.json +++ b/packages/nc-gui/lang/de.json @@ -667,7 +667,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Projektmetadaten erfolgreich exportiert", diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index e26dd5bc9e..50b78578ca 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -674,7 +674,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Project metadata exported successfully", diff --git a/packages/nc-gui/lang/es.json b/packages/nc-gui/lang/es.json index 8e98ddc391..a98475ea72 100644 --- a/packages/nc-gui/lang/es.json +++ b/packages/nc-gui/lang/es.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Metadatos del proyecto exportados con éxito.", diff --git a/packages/nc-gui/lang/fa.json b/packages/nc-gui/lang/fa.json index 5fe34c4a89..5207705649 100644 --- a/packages/nc-gui/lang/fa.json +++ b/packages/nc-gui/lang/fa.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "فراداده پروژه با موفقیت خارج شد", diff --git a/packages/nc-gui/lang/fi.json b/packages/nc-gui/lang/fi.json index e19380b494..02bfe2c8dc 100644 --- a/packages/nc-gui/lang/fi.json +++ b/packages/nc-gui/lang/fi.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Project Metadata viedään onnistuneesti", diff --git a/packages/nc-gui/lang/fr.json b/packages/nc-gui/lang/fr.json index 188d120826..4aea9a7be6 100644 --- a/packages/nc-gui/lang/fr.json +++ b/packages/nc-gui/lang/fr.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Les métadonnées de projet sont exportées avec succès", diff --git a/packages/nc-gui/lang/he.json b/packages/nc-gui/lang/he.json index f9501712f7..b023a59001 100644 --- a/packages/nc-gui/lang/he.json +++ b/packages/nc-gui/lang/he.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "פרויקט Metadata מיוצא בהצלחה", diff --git a/packages/nc-gui/lang/hi.json b/packages/nc-gui/lang/hi.json index fdd21d3c66..3036ba6bc7 100644 --- a/packages/nc-gui/lang/hi.json +++ b/packages/nc-gui/lang/hi.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "परियोजना मेटाडेटा सफलतापूर्वक निर्यात की गई", diff --git a/packages/nc-gui/lang/hr.json b/packages/nc-gui/lang/hr.json index 01cd148906..44d3e0bdc6 100644 --- a/packages/nc-gui/lang/hr.json +++ b/packages/nc-gui/lang/hr.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Projektni metapodaci su uspješno izvozili", diff --git a/packages/nc-gui/lang/id.json b/packages/nc-gui/lang/id.json index afe46061b1..15657f7320 100644 --- a/packages/nc-gui/lang/id.json +++ b/packages/nc-gui/lang/id.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Metadata proyek berhasil diekspor", diff --git a/packages/nc-gui/lang/it.json b/packages/nc-gui/lang/it.json index 8714d8618a..76864f1326 100644 --- a/packages/nc-gui/lang/it.json +++ b/packages/nc-gui/lang/it.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Metadati del progetto esportati con successo", diff --git a/packages/nc-gui/lang/ja.json b/packages/nc-gui/lang/ja.json index b2806c2065..cb0333f206 100644 --- a/packages/nc-gui/lang/ja.json +++ b/packages/nc-gui/lang/ja.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "プロジェクトメタデータは正常にエクスポートされました", diff --git a/packages/nc-gui/lang/ko.json b/packages/nc-gui/lang/ko.json index 959693e71e..b91b3ea389 100644 --- a/packages/nc-gui/lang/ko.json +++ b/packages/nc-gui/lang/ko.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "프로젝트 메타 데이터를 성공적으로 내보냈습니다.", diff --git a/packages/nc-gui/lang/lv.json b/packages/nc-gui/lang/lv.json index 0fa407eab1..07081cb1bd 100644 --- a/packages/nc-gui/lang/lv.json +++ b/packages/nc-gui/lang/lv.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Projekta metadati eksportēti veiksmīgi", diff --git a/packages/nc-gui/lang/nl.json b/packages/nc-gui/lang/nl.json index 130f087c3e..9c714e9ec1 100644 --- a/packages/nc-gui/lang/nl.json +++ b/packages/nc-gui/lang/nl.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Project metadata met succes geëxporteerd", diff --git a/packages/nc-gui/lang/no.json b/packages/nc-gui/lang/no.json index bc4f661903..a30561c1c0 100644 --- a/packages/nc-gui/lang/no.json +++ b/packages/nc-gui/lang/no.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Prosjektmetadata eksporteres vellykket", diff --git a/packages/nc-gui/lang/pl.json b/packages/nc-gui/lang/pl.json index e089c10f0d..6b8e0bca13 100644 --- a/packages/nc-gui/lang/pl.json +++ b/packages/nc-gui/lang/pl.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Klucz parametru nie może być pusty", "duplicateParameterKeysAreNotAllowed": "Zduplikowane klucze parametrów są niedozwolone", "fieldRequired": "{value} nie może być puste.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Pomyślnie wyeksportowano metadane projektu", diff --git a/packages/nc-gui/lang/pt.json b/packages/nc-gui/lang/pt.json index 1d340c27c4..9de8d9c21f 100644 --- a/packages/nc-gui/lang/pt.json +++ b/packages/nc-gui/lang/pt.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Metadados do projeto exportado com sucesso", diff --git a/packages/nc-gui/lang/pt_BR.json b/packages/nc-gui/lang/pt_BR.json index 96e7305f29..932669389f 100644 --- a/packages/nc-gui/lang/pt_BR.json +++ b/packages/nc-gui/lang/pt_BR.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Metadados do projeto exportado com sucesso", diff --git a/packages/nc-gui/lang/ru.json b/packages/nc-gui/lang/ru.json index d175e86a89..70df80d496 100644 --- a/packages/nc-gui/lang/ru.json +++ b/packages/nc-gui/lang/ru.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Метаданные проекта успешно экспортированы", diff --git a/packages/nc-gui/lang/sl.json b/packages/nc-gui/lang/sl.json index 02504f569e..c051e52cb3 100644 --- a/packages/nc-gui/lang/sl.json +++ b/packages/nc-gui/lang/sl.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Projekt Metapodatki se je uspešno izvozil", diff --git a/packages/nc-gui/lang/sv.json b/packages/nc-gui/lang/sv.json index 90c5518c41..05e67399e3 100644 --- a/packages/nc-gui/lang/sv.json +++ b/packages/nc-gui/lang/sv.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Projektmetadata exporterades framgångsrikt", diff --git a/packages/nc-gui/lang/th.json b/packages/nc-gui/lang/th.json index ff5be0bf66..e2e51c7050 100644 --- a/packages/nc-gui/lang/th.json +++ b/packages/nc-gui/lang/th.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "ข้อมูลเมตาของโครงการส่งออกเรียบร้อยแล้ว", diff --git a/packages/nc-gui/lang/tr.json b/packages/nc-gui/lang/tr.json index 83ea91fea2..cbdf0ec321 100644 --- a/packages/nc-gui/lang/tr.json +++ b/packages/nc-gui/lang/tr.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Proje metaverileri başarıyla dışa aktarıldı", diff --git a/packages/nc-gui/lang/uk.json b/packages/nc-gui/lang/uk.json index 9b24d55df9..0c8d5d034d 100644 --- a/packages/nc-gui/lang/uk.json +++ b/packages/nc-gui/lang/uk.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Метадані проекту успішно експортується", diff --git a/packages/nc-gui/lang/vi.json b/packages/nc-gui/lang/vi.json index 6a8806bfc0..87d7ca6301 100644 --- a/packages/nc-gui/lang/vi.json +++ b/packages/nc-gui/lang/vi.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "Metadata dự án xuất khẩu thành công", diff --git a/packages/nc-gui/lang/zh-Hans.json b/packages/nc-gui/lang/zh-Hans.json index 017bd56ede..078389b1e0 100644 --- a/packages/nc-gui/lang/zh-Hans.json +++ b/packages/nc-gui/lang/zh-Hans.json @@ -368,7 +368,7 @@ "setPrimary": "设置为主要值", "addRow": "添加新行", "saveRow": "保存行", - "saveAndExit": "Save & Exit", + "saveAndExit": "保存并退出", "saveAndStay": "Save & Stay", "insertRow": "插入新行", "deleteRow": "删除行", @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "参数键不能为空", "duplicateParameterKeysAreNotAllowed": "不允许重复的参数键", "fieldRequired": "{value} 不能为空。", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "项目元数据成功导出", diff --git a/packages/nc-gui/lang/zh-Hant.json b/packages/nc-gui/lang/zh-Hant.json index 708ce4e23e..2682ea92ce 100644 --- a/packages/nc-gui/lang/zh-Hant.json +++ b/packages/nc-gui/lang/zh-Hant.json @@ -666,7 +666,8 @@ "parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "fieldRequired": "{value} cannot be empty.", - "projectNotAccessible": "Project not accessible" + "projectNotAccessible": "Project not accessible", + "copyToClipboardError": "Failed to copy to clipboard" }, "toast": { "exportMetadata": "專案中繼資料已成功匯出", diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json index 1c5596e655..4198863821 100644 --- a/packages/nc-gui/package-lock.json +++ b/packages/nc-gui/package-lock.json @@ -33,6 +33,7 @@ "sortablejs": "^1.15.0", "tinycolor2": "^1.4.2", "unique-names-generator": "^4.7.1", + "validator": "^13.7.0", "vue-dompurify-html": "^3.0.0", "vue-github-button": "^3.0.3", "vue-i18n": "^9.2.2", @@ -63,6 +64,7 @@ "@types/papaparse": "^5.3.2", "@types/sortablejs": "^1.13.0", "@types/tinycolor2": "^1.4.3", + "@types/validator": "^13.7.10", "@vitest/ui": "^0.18.0", "@vue/compiler-sfc": "^3.2.37", "@vue/test-utils": "^2.0.2", @@ -85,6 +87,28 @@ "windicss": "^3.5.6" } }, + "../nocodb-sdk": { + "version": "0.99.2", + "license": "AGPL-3.0-or-later", + "dependencies": { + "axios": "^0.21.1", + "jsep": "^1.3.6" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.1", + "@typescript-eslint/parser": "^4.0.1", + "cspell": "^4.1.0", + "eslint": "^7.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^3.0.2", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^4.0.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.1.1", + "typescript": "^4.0.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -3078,6 +3102,12 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.7.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.10.tgz", + "integrity": "sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==", + "dev": true + }, "node_modules/@types/web-bluetooth": { "version": "0.0.15", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz", @@ -8383,6 +8413,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "devOptional": true, "funding": [ { "type": "individual", @@ -11738,21 +11769,8 @@ } }, "node_modules/nocodb-sdk": { - "version": "0.99.2", - "resolved": "file:../nocodb-sdk", - "license": "AGPL-3.0-or-later", - "dependencies": { - "axios": "^0.21.1", - "jsep": "^1.3.6" - } - }, - "node_modules/nocodb-sdk/node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } + "resolved": "../nocodb-sdk", + "link": true }, "node_modules/node-abi": { "version": "3.23.0", @@ -16047,6 +16065,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vite": { "version": "2.9.15", "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz", @@ -19769,6 +19795,12 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "@types/validator": { + "version": "13.7.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.10.tgz", + "integrity": "sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==", + "dev": true + }, "@types/web-bluetooth": { "version": "0.0.15", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz", @@ -23561,7 +23593,8 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "devOptional": true }, "form-data": { "version": "4.0.0", @@ -26029,20 +26062,22 @@ } }, "nocodb-sdk": { - "version": "0.99.2", + "version": "file:../nocodb-sdk", "requires": { + "@typescript-eslint/eslint-plugin": "^4.0.1", + "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.1", - "jsep": "^1.3.6" - }, - "dependencies": { - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - } + "cspell": "^4.1.0", + "eslint": "^7.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^3.0.2", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^4.0.0", + "jsep": "^1.3.6", + "npm-run-all": "^4.1.5", + "prettier": "^2.1.1", + "typescript": "^4.0.2" } }, "node-abi": { @@ -29245,6 +29280,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + }, "vite": { "version": "2.9.15", "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz", diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index a1cb82564b..ab0ed240b7 100644 --- a/packages/nc-gui/package.json +++ b/packages/nc-gui/package.json @@ -56,6 +56,7 @@ "sortablejs": "^1.15.0", "tinycolor2": "^1.4.2", "unique-names-generator": "^4.7.1", + "validator": "^13.7.0", "vue-dompurify-html": "^3.0.0", "vue-github-button": "^3.0.3", "vue-i18n": "^9.2.2", @@ -86,6 +87,7 @@ "@types/papaparse": "^5.3.2", "@types/sortablejs": "^1.13.0", "@types/tinycolor2": "^1.4.3", + "@types/validator": "^13.7.10", "@vitest/ui": "^0.18.0", "@vue/compiler-sfc": "^3.2.37", "@vue/test-utils": "^2.0.2", diff --git a/packages/nc-gui/utils/cell.ts b/packages/nc-gui/utils/cell.ts index 9214578bf2..4b516e7d6d 100644 --- a/packages/nc-gui/utils/cell.ts +++ b/packages/nc-gui/utils/cell.ts @@ -46,8 +46,7 @@ export const isAutoSaved = (column: ColumnType) => UITypes.Duration, ].includes(column.uidt as UITypes) -export const isManualSaved = (column: ColumnType) => - [UITypes.Currency, UITypes.Year, UITypes.Time].includes(column.uidt as UITypes) +export const isManualSaved = (column: ColumnType) => [UITypes.Currency].includes(column.uidt as UITypes) export const isPrimary = (column: ColumnType) => !!column.pv diff --git a/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts b/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts index c9048cb724..d3d60b5359 100644 --- a/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts +++ b/packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts @@ -132,15 +132,15 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { if (column.uidt === UITypes.SingleLineText) { // check for long text - if (isMultiLineTextType(rows)) { + if (isMultiLineTextType(rows, col)) { column.uidt = UITypes.LongText } - if (isEmailType(rows)) { + if (isEmailType(rows, col)) { column.uidt = UITypes.Email } - if (isUrlType(rows)) { + if (isUrlType(rows, col)) { column.uidt = UITypes.URL } else { const vals = rows @@ -148,7 +148,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator { .map((r: any) => r[col]) .filter((v: any) => v !== null && v !== undefined && v.toString().trim() !== '') - const checkboxType = isCheckboxType(vals) + const checkboxType = isCheckboxType(vals, col) if (checkboxType.length === 1) { column.uidt = UITypes.Checkbox } else { diff --git a/packages/nc-gui/utils/parsers/parserHelpers.ts b/packages/nc-gui/utils/parsers/parserHelpers.ts index 7df86117c0..b0759389b6 100644 --- a/packages/nc-gui/utils/parsers/parserHelpers.ts +++ b/packages/nc-gui/utils/parsers/parserHelpers.ts @@ -20,11 +20,11 @@ const booleanOptions = [ ] const aggBooleanOptions: any = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {}) -const getColVal = (row: any, col = null) => { +const getColVal = (row: any, col?: number) => { return row && col ? row[col] : row } -export const isCheckboxType: any = (values: [], col = null) => { +export const isCheckboxType: any = (values: [], col?: number) => { let options = booleanOptions for (let i = 0; i < values.length; i++) { const val = getColVal(values[i], col) @@ -45,7 +45,7 @@ export const getCheckboxValue = (value: any) => { return value && aggBooleanOptions[value] } -export const isMultiLineTextType = (values: [], col = null) => { +export const isMultiLineTextType = (values: [], col?: number) => { return values.some( (r) => (getColVal(r, col) || '').toString().match(/[\r\n]/) || (getColVal(r, col) || '').toString().length > 255, ) @@ -107,13 +107,15 @@ export const isDecimalType = (colData: []) => return v && parseInt(v) !== +v }) -export const isEmailType = (colData: []) => - colData.some((v: any) => { +export const isEmailType = (colData: [], col?: number) => + colData.some((r: any) => { + const v = getColVal(r, col) return v && validateEmail(v) }) -export const isUrlType = (colData: []) => - colData.some((v: any) => { +export const isUrlType = (colData: [], col?: number) => + colData.some((r: any) => { + const v = getColVal(r, col) return v && isValidURL(v) }) diff --git a/packages/nc-gui/utils/urlUtils.ts b/packages/nc-gui/utils/urlUtils.ts index e6bf9d8c74..d9d807d446 100644 --- a/packages/nc-gui/utils/urlUtils.ts +++ b/packages/nc-gui/utils/urlUtils.ts @@ -1,3 +1,5 @@ +import isURL from 'validator/lib/isURL' + export const replaceUrlsWithLink = (text: string): boolean | string => { if (!text) { return false @@ -17,12 +19,8 @@ export const replaceUrlsWithLink = (text: string): boolean | string => { return found && out } -// ref : https://stackoverflow.com/a/5717133 export const isValidURL = (str: string) => { - const pattern = - /^(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)(?:\.(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)*(?:\.(?:[a-z\u00A1-\uFFFF]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i - - return pattern.test(str) + return isURL(str) } export const openLink = (path: string, baseURL?: string, target = '_blank') => { diff --git a/packages/nc-gui/windi.config.ts b/packages/nc-gui/windi.config.ts index 306b909724..7508c05def 100644 --- a/packages/nc-gui/windi.config.ts +++ b/packages/nc-gui/windi.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ }, darkMode: 'class', - + safelist: ['text-yellow-500', 'text-sky-500', 'text-red-500'], plugins: [ scrollbar, animations, diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index 79d260b449..5586097908 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -151,6 +151,28 @@ "vuedraggable": "^2.24.3" } }, + "../nocodb-sdk": { + "version": "0.99.2", + "license": "AGPL-3.0-or-later", + "dependencies": { + "axios": "^0.21.1", + "jsep": "^1.3.6" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.1", + "@typescript-eslint/parser": "^4.0.1", + "cspell": "^4.1.0", + "eslint": "^7.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^3.0.2", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^4.0.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.1.1", + "typescript": "^4.0.2" + } + }, "node_modules/@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -10634,13 +10656,8 @@ "dev": true }, "node_modules/nocodb-sdk": { - "version": "0.99.2", - "resolved": "file:../nocodb-sdk", - "license": "AGPL-3.0-or-later", - "dependencies": { - "axios": "^0.21.1", - "jsep": "^1.3.6" - } + "resolved": "../nocodb-sdk", + "link": true }, "node_modules/node-abort-controller": { "version": "3.0.1", @@ -26042,10 +26059,22 @@ "dev": true }, "nocodb-sdk": { - "version": "0.99.2", + "version": "file:../nocodb-sdk", "requires": { + "@typescript-eslint/eslint-plugin": "^4.0.1", + "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.1", - "jsep": "^1.3.6" + "cspell": "^4.1.0", + "eslint": "^7.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^3.0.2", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^4.0.0", + "jsep": "^1.3.6", + "npm-run-all": "^4.1.5", + "prettier": "^2.1.1", + "typescript": "^4.0.2" } }, "node-abort-controller": {