diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts index 21103e4be8..5b15a4c299 100644 --- a/packages/nc-gui/components.d.ts +++ b/packages/nc-gui/components.d.ts @@ -108,6 +108,8 @@ declare module '@vue/runtime-core' { MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default'] MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default'] MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default'] + MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default'] + MdiAdd: typeof import('~icons/mdi/add')['default'] MdiAlpha: typeof import('~icons/mdi/alpha')['default'] MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default'] MdiApi: typeof import('~icons/mdi/api')['default'] @@ -202,6 +204,7 @@ declare module '@vue/runtime-core' { MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default'] MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default'] MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default'] + MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default'] MdiSlack: typeof import('~icons/mdi/slack')['default'] MdiSort: typeof import('~icons/mdi/sort')['default'] MdiStar: typeof import('~icons/mdi/star')['default'] diff --git a/packages/nc-gui/components/cell/Checkbox.vue b/packages/nc-gui/components/cell/Checkbox.vue index 04c5d73521..1e533f3c05 100644 --- a/packages/nc-gui/components/cell/Checkbox.vue +++ b/packages/nc-gui/components/cell/Checkbox.vue @@ -1,5 +1,5 @@ @@ -53,7 +64,7 @@ function onClick() { 'nc-cell-hover-show': !vModel && !readOnly, 'opacity-0': readOnly && !vModel, }" - @click="onClick" + @click="onClick(false)" > @@ -62,6 +73,7 @@ function onClick() { :style="{ color: checkboxMeta.color, }" + @click.stop="onClick(true)" /> @@ -72,6 +84,7 @@ function onClick() { .nc-cell-hover-show { opacity: 0; transition: 0.3s opacity; + &:hover { opacity: 0.7; } diff --git a/packages/nc-gui/components/cell/Currency.vue b/packages/nc-gui/components/cell/Currency.vue index 5cccfb7cf1..da72a21fa9 100644 --- a/packages/nc-gui/components/cell/Currency.vue +++ b/packages/nc-gui/components/cell/Currency.vue @@ -61,6 +61,11 @@ onMounted(() => { v-model="vModel" class="w-full h-full border-none outline-none px-2" @blur="submitCurrency" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ currency }} diff --git a/packages/nc-gui/components/cell/DatePicker.vue b/packages/nc-gui/components/cell/DatePicker.vue index 08362605d7..f4e6508bce 100644 --- a/packages/nc-gui/components/cell/DatePicker.vue +++ b/packages/nc-gui/components/cell/DatePicker.vue @@ -1,6 +1,16 @@ @@ -68,9 +97,9 @@ const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : '')) :placeholder="placeholder" :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" - :dropdown-class-name="`${randomClass} nc-picker-date`" - :open="readOnly || (localState && isPk) ? false : open" - @click="open = !open" + :dropdown-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''}`" + :open="(readOnly || (localState && isPk)) && !active && !editable ? false : open" + @click="open = (active || editable) && !open" > diff --git a/packages/nc-gui/components/cell/DateTimePicker.vue b/packages/nc-gui/components/cell/DateTimePicker.vue index b28e7bf8d4..d9cb6e2dea 100644 --- a/packages/nc-gui/components/cell/DateTimePicker.vue +++ b/packages/nc-gui/components/cell/DateTimePicker.vue @@ -1,6 +1,6 @@ @@ -68,10 +87,10 @@ watch( :placeholder="isDateInvalid ? 'Invalid date' : ''" :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" - :dropdown-class-name="`${randomClass} nc-picker-datetime`" - :open="readOnly || (localState && isPk) ? false : open" + :dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`" + :open="readOnly || (localState && isPk) ? false : open && (active || editable)" :disabled="readOnly || (localState && isPk)" - @click="open = !open" + @click="open = (active || editable) && !open" @ok="open = !open" > diff --git a/packages/nc-gui/components/cell/Decimal.vue b/packages/nc-gui/components/cell/Decimal.vue index 9581665304..5591880433 100644 --- a/packages/nc-gui/components/cell/Decimal.vue +++ b/packages/nc-gui/components/cell/Decimal.vue @@ -30,6 +30,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() type="number" step="0.1" @blur="editEnabled = false" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ vModel }} diff --git a/packages/nc-gui/components/cell/Duration.vue b/packages/nc-gui/components/cell/Duration.vue index 4a8a833369..bcb32c34fc 100644 --- a/packages/nc-gui/components/cell/Duration.vue +++ b/packages/nc-gui/components/cell/Duration.vue @@ -84,6 +84,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() @blur="submitDuration" @keypress="checkDurationFormat($event)" @keydown.enter="submitDuration" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ localState }} diff --git a/packages/nc-gui/components/cell/Email.vue b/packages/nc-gui/components/cell/Email.vue index 2ab60c42f6..f7738e109f 100644 --- a/packages/nc-gui/components/cell/Email.vue +++ b/packages/nc-gui/components/cell/Email.vue @@ -24,7 +24,18 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() - + {{ vModel }} diff --git a/packages/nc-gui/components/cell/Float.vue b/packages/nc-gui/components/cell/Float.vue index 111f7beb72..a2a973d9b2 100644 --- a/packages/nc-gui/components/cell/Float.vue +++ b/packages/nc-gui/components/cell/Float.vue @@ -30,6 +30,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() type="number" step="0.1" @blur="editEnabled = false" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ vModel }} diff --git a/packages/nc-gui/components/cell/Integer.vue b/packages/nc-gui/components/cell/Integer.vue index 63c15a5546..d62b871d8f 100644 --- a/packages/nc-gui/components/cell/Integer.vue +++ b/packages/nc-gui/components/cell/Integer.vue @@ -34,6 +34,11 @@ function onKeyDown(evt: KeyboardEvent) { type="number" @blur="editEnabled = false" @keydown="onKeyDown" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ vModel }} diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index 0da8f1f871..6d5d704321 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -14,6 +14,7 @@ import { ref, useEventListener, useProject, + useSelectedCellKeyupListener, watch, } from '#imports' import MdiCloseCircle from '~icons/mdi/close-circle' @@ -85,17 +86,7 @@ const selectedTitles = computed(() => : [], ) -const handleKeys = (e: KeyboardEvent) => { - switch (e.key) { - case 'Escape': - e.preventDefault() - isOpen.value = false - break - case 'Enter': - e.stopPropagation() - break - } -} +const v = Math.floor(Math.random() * 1000) const handleClose = (e: MouseEvent) => { if (aselect.value && !aselect.value.$el.contains(e.target)) { @@ -131,7 +122,24 @@ watch( ) watch(isOpen, (n, _o) => { - if (!n) aselect.value?.$el.blur() + if (!n) { + aselect.value?.$el?.querySelector('input')?.blur() + } else { + aselect.value?.$el?.querySelector('input')?.focus() + } +}) + +useSelectedCellKeyupListener(active, (e) => { + switch (e.key) { + case 'Escape': + isOpen.value = false + break + case 'Enter': + if (active.value && !isOpen.value) { + isOpen.value = true + } + break + } }) @@ -139,17 +147,17 @@ watch(isOpen, (n, _o) => { { margin-right: -6px; margin-left: 3px; } + .ms-close-icon:before { display: block; } + .ms-close-icon:hover { color: rgba(0, 0, 0, 0.45); } + .rounded-tag { @apply py-0 px-[12px] rounded-[12px]; } + :deep(.ant-tag) { @apply "rounded-tag" my-[2px]; } + :deep(.ant-tag-close-icon) { @apply "text-slate-500"; } diff --git a/packages/nc-gui/components/cell/Percent.vue b/packages/nc-gui/components/cell/Percent.vue index 381b46df05..04bbfe2dcf 100644 --- a/packages/nc-gui/components/cell/Percent.vue +++ b/packages/nc-gui/components/cell/Percent.vue @@ -21,6 +21,11 @@ const vModel = useVModel(props, 'modelValue', emits) class="w-full !border-none text-base" :class="{ '!px-2': editEnabled }" type="number" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ vModel }} diff --git a/packages/nc-gui/components/cell/Rating.vue b/packages/nc-gui/components/cell/Rating.vue index bd9c923d38..44308292e6 100644 --- a/packages/nc-gui/components/cell/Rating.vue +++ b/packages/nc-gui/components/cell/Rating.vue @@ -1,5 +1,5 @@ - + diff --git a/packages/nc-gui/components/cell/SingleSelect.vue b/packages/nc-gui/components/cell/SingleSelect.vue index cb9258505c..cf7615bf05 100644 --- a/packages/nc-gui/components/cell/SingleSelect.vue +++ b/packages/nc-gui/components/cell/SingleSelect.vue @@ -3,6 +3,7 @@ import tinycolor from 'tinycolor2' import type { Select as AntSelect } from 'ant-design-vue' import type { SelectOptionType } from 'nocodb-sdk' import { ActiveCellInj, ColumnInj, IsKanbanInj, ReadonlyInj, computed, inject, ref, useEventListener, watch } from '#imports' +import { useSelectedCellKeyupListener } from '~/composables/useSelectedCellKeyupListener' interface Props { modelValue?: string | undefined @@ -44,15 +45,6 @@ const options = computed(() => { return [] }) -const handleKeys = (e: KeyboardEvent) => { - switch (e.key) { - case 'Escape': - e.preventDefault() - isOpen.value = false - break - } -} - const handleClose = (e: MouseEvent) => { if (aselect.value && !aselect.value.$el.contains(e.target)) { isOpen.value = false @@ -63,7 +55,24 @@ const handleClose = (e: MouseEvent) => { useEventListener(document, 'click', handleClose) watch(isOpen, (n, _o) => { - if (!n) aselect.value?.$el.blur() + if (!n) { + aselect.value?.$el?.querySelector('input')?.blur() + } else { + aselect.value?.$el?.querySelector('input')?.focus() + } +}) + +useSelectedCellKeyupListener(active, (e) => { + switch (e.key) { + case 'Escape': + isOpen.value = false + break + case 'Enter': + if (active.value && !isOpen.value) { + isOpen.value = true + } + break + } }) @@ -77,10 +86,10 @@ watch(isOpen, (n, _o) => { :open="isOpen" :disabled="readOnly" :show-arrow="!readOnly && (active || vModel === null)" - dropdown-class-name="nc-dropdown-single-select-cell" + :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`" @select="isOpen = false" - @keydown="handleKeys" - @click="isOpen = !isOpen" + @keydown.enter.stop + @click="isOpen = active && !isOpen" > { .rounded-tag { @apply py-0 px-[12px] rounded-[12px]; } + :deep(.ant-tag) { @apply "rounded-tag"; } + :deep(.ant-select-clear) { opacity: 1; } diff --git a/packages/nc-gui/components/cell/Text.vue b/packages/nc-gui/components/cell/Text.vue index 53e7aa02b1..465cfd73f9 100644 --- a/packages/nc-gui/components/cell/Text.vue +++ b/packages/nc-gui/components/cell/Text.vue @@ -27,6 +27,11 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() class="h-full w-full outline-none bg-transparent" :class="{ '!px-2': editEnabled }" @blur="editEnabled = false" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ vModel }} diff --git a/packages/nc-gui/components/cell/TextArea.vue b/packages/nc-gui/components/cell/TextArea.vue index db3b054bcf..4ed873947f 100644 --- a/packages/nc-gui/components/cell/TextArea.vue +++ b/packages/nc-gui/components/cell/TextArea.vue @@ -26,6 +26,11 @@ const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus() @blur="editEnabled = false" @keydown.alt.enter.stop @keydown.shift.enter.stop + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> {{ vModel }} diff --git a/packages/nc-gui/components/cell/TimePicker.vue b/packages/nc-gui/components/cell/TimePicker.vue index ed19d62277..6402de2bed 100644 --- a/packages/nc-gui/components/cell/TimePicker.vue +++ b/packages/nc-gui/components/cell/TimePicker.vue @@ -1,6 +1,6 @@ @@ -79,9 +98,9 @@ watch( :placeholder="isTimeInvalid ? 'Invalid time' : ''" :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" - :open="readOnly || (localState && isPk) ? false : open" - :popup-class-name="`${randomClass} nc-picker-time`" - @click="open = !open" + :open="(readOnly || (localState && isPk)) && !active && !editable ? false : open" + :popup-class-name="`${randomClass} nc-picker-time ${open ? 'active' : ''}`" + @click="open = (active || editable) && !open" @ok="open = !open" > diff --git a/packages/nc-gui/components/cell/Url.vue b/packages/nc-gui/components/cell/Url.vue index 31d0b99be7..9c8962465d 100644 --- a/packages/nc-gui/components/cell/Url.vue +++ b/packages/nc-gui/components/cell/Url.vue @@ -79,6 +79,11 @@ watch( v-model="vModel" class="outline-none text-sm w-full px-2" @blur="editEnabled = false" + @keydown.down.stop + @keydown.left.stop + @keydown.right.stop + @keydown.up.stop + @keydown.delete.stop /> import dayjs from 'dayjs' -import { ReadonlyInj, computed, inject, onClickOutside, ref, watch } from '#imports' +import { ActiveCellInj, ReadonlyInj, computed, inject, onClickOutside, ref, useSelectedCellKeyupListener, watch } from '#imports' interface Props { modelValue?: number | string | null @@ -13,6 +13,10 @@ const emit = defineEmits(['update:modelValue']) const readOnly = inject(ReadonlyInj, ref(false)) +const active = inject(ActiveCellInj, ref(false)) + +const editable = inject(EditModeInj, ref(false)) + let isYearInvalid = $ref(false) const localState = $computed({ @@ -55,6 +59,21 @@ watch( ) const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : '')) + +useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + e.stopPropagation() + open.value = true + break + case 'Escape': + if (open.value) { + e.stopPropagation() + open.value = false + } + break + } +}) @@ -66,10 +85,10 @@ const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : '')) :placeholder="placeholder" :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" - :open="readOnly || (localState && isPk) ? false : open" - :dropdown-class-name="`${randomClass} nc-picker-year`" - @click="open = !open" - @change="open = !open" + :open="(readOnly || (localState && isPk)) && !active && !editable ? false : open" + :dropdown-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''}`" + @click="open = (active || editable) && !open" + @change="open = (active || editable) && !open" > diff --git a/packages/nc-gui/components/cell/attachment/index.vue b/packages/nc-gui/components/cell/attachment/index.vue index f4be61efd5..1ce6ec6a51 100644 --- a/packages/nc-gui/components/cell/attachment/index.vue +++ b/packages/nc-gui/components/cell/attachment/index.vue @@ -3,6 +3,7 @@ import { onKeyDown } from '@vueuse/core' import { useProvideAttachmentCell } from './utils' import { useSortable } from './sort' import { + ActiveCellInj, DropZoneRef, IsGalleryInj, IsKanbanInj, @@ -12,6 +13,7 @@ import { openLink, ref, useDropZone, + useSelectedCellKeyupListener, useSmartsheetRowStoreOrThrow, useSmartsheetStoreOrThrow, watch, @@ -113,9 +115,17 @@ watch( attachments.value = [] } } + } else { + if (isPublic.value && isForm.value) { + storedFiles.value = [] + } else { + attachments.value = [] + } } }, - { immediate: true }, + { + immediate: true, + }, ) /** updates attachments array for autosave */ @@ -136,6 +146,13 @@ watch( rowState.value[column.value!.title!] = storedFiles.value }, ) + +useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => { + if (e.key === 'Enter' && !isReadonly.value) { + e.stopPropagation() + modalVisible.value = true + } +}) diff --git a/packages/nc-gui/components/smartsheet/Cell.vue b/packages/nc-gui/components/smartsheet/Cell.vue index 6d88f5370e..386dc05e11 100644 --- a/packages/nc-gui/components/smartsheet/Cell.vue +++ b/packages/nc-gui/components/smartsheet/Cell.vue @@ -112,6 +112,7 @@ const vModel = computed({ }) const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => { + console.log('syncAndNavigate', e.target) if (isJSON.value) return if (currentRow.value.rowMeta.changed || currentRow.value.rowMeta.new) { diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 52c97c1d87..7ce1602d06 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -22,6 +22,7 @@ import { extractPkFromRow, inject, isColumnRequiredAndNull, + isMac, message, onBeforeUnmount, onClickOutside, @@ -168,52 +169,118 @@ const { selectCell, selectBlock, selectedRange, clearRangeRows, startSelectRange isPkAvail, clearCell, makeEditable, - (row?: number | null, col?: number | null) => { - row = row ?? selected.row - col = col ?? selected.col - if (row !== undefined && col !== undefined && row !== null && col !== null) { - // get active cell - const rows = tbodyEl.value?.querySelectorAll('tr') - const cols = rows?.[row].querySelectorAll('td') - const td = cols?.[col === 0 ? 0 : col + 1] - - if (!td || !gridWrapper.value) return - - const { height: headerHeight } = tableHead.value!.getBoundingClientRect() - const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 }) - - if (rows && row === rows.length - 2) { - // if last row make 'Add New Row' visible - gridWrapper.value.scrollTo({ - top: gridWrapper.value.scrollHeight, - left: - cols && col === cols.length - 2 // if corner cell - ? gridWrapper.value.scrollWidth - : tdScroll.left, - behavior: 'smooth', - }) - return - } + scrollToCell, + (e: KeyboardEvent) => { + // ignore navigating if picker(Date, Time, DateTime, Year) + // or single/multi select options is open + const activePickerOrDropdownEl = document.querySelector( + '.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active', + ) + if (activePickerOrDropdownEl) { + e.preventDefault() + return true + } + + // if expanded form is active skip keyboard event handling + if (document.querySelector('.nc-drawer-expanded-form.active')) { + return true + } - if (cols && col === cols.length - 2) { - // if last column make 'Add New Column' visible - gridWrapper.value.scrollTo({ - top: tdScroll.top, - left: gridWrapper.value.scrollWidth, - behavior: 'smooth', - }) - return + const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey + if (e.key === ' ') { + if (selected.row !== null && !editEnabled) { + e.preventDefault() + const row = data.value[selected.row] + expandForm(row) + return true + } + } else if (e.key === 'Escape') { + if (editEnabled) { + editEnabled = false + return true + } + } else if (e.key === 'Enter') { + if (editEnabled) { + editEnabled = false + return true } + } + if (cmdOrCtrl) { + switch (e.key) { + case 'ArrowUp': + selected.row = 0 + selected.col = selected.col ?? 0 + scrollToCell?.() + editEnabled = false + return true + case 'ArrowDown': + selected.row = data.value.length - 1 + selected.col = selected.col ?? 0 + scrollToCell?.() + editEnabled = false + return true + case 'ArrowRight': + selected.row = selected.row ?? 0 + selected.col = fields.value?.length - 1 + scrollToCell?.() + editEnabled = false + return true + case 'ArrowLeft': + selected.row = selected.row ?? 0 + selected.col = 0 + scrollToCell?.() + editEnabled = false + return true + } + } + }, +) + +function scrollToCell(row?: number | null, col?: number | null) { + row = row ?? selected.row + col = col ?? selected.col + if (row !== undefined && col !== undefined && row !== null && col !== null) { + // get active cell + const rows = tbodyEl.value?.querySelectorAll('tr') + const cols = rows?.[row].querySelectorAll('td') + const td = cols?.[col === 0 ? 0 : col + 1] + + if (!td || !gridWrapper.value) return + + const { height: headerHeight } = tableHead.value!.getBoundingClientRect() + const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 }) - // scroll into the active cell + if (rows && row === rows.length - 2) { + // if last row make 'Add New Row' visible + gridWrapper.value.scrollTo({ + top: gridWrapper.value.scrollHeight, + left: + cols && col === cols.length - 2 // if corner cell + ? gridWrapper.value.scrollWidth + : tdScroll.left, + behavior: 'smooth', + }) + return + } + + if (cols && col === cols.length - 2) { + // if last column make 'Add New Column' visible gridWrapper.value.scrollTo({ top: tdScroll.top, - left: tdScroll.left, + left: gridWrapper.value.scrollWidth, behavior: 'smooth', }) + return } - }, -) + + // scroll into the active cell + gridWrapper.value.scrollTo({ + top: tdScroll.top, + left: tdScroll.left, + behavior: 'smooth', + }) + } +} onMounted(loadGridViewColumns) @@ -234,7 +301,7 @@ const showLoading = ref(true) const skipRowRemovalOnCancel = ref(false) -const expandForm = (row: Row, state?: Record, fromToolbar = false) => { +function expandForm(row: Row, state?: Record, fromToolbar = false) { const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) if (rowId) { @@ -325,7 +392,7 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => { /** On clicking outside of table reset active cell */ const smartTable = ref(null) -onClickOutside(smartTable, () => { +onClickOutside(smartTable, (e) => { clearRangeRows() if (selected.col === null) return @@ -333,6 +400,23 @@ onClickOutside(smartTable, () => { if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return + // ignore unselecting if clicked inside or on the picker(Date, Time, DateTime, Year) + // or single/multi select options + const activePickerOrDropdownEl = document.querySelector( + '.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active', + ) + if ( + e.target && + activePickerOrDropdownEl && + (activePickerOrDropdownEl === e.target || activePickerOrDropdownEl?.contains(e.target as Element)) + ) + return + + // if expanded form is active skip resetting the active cell + if (document.querySelector('.nc-drawer-expanded-form.active')) { + return + } + selected.row = null selected.col = null }) @@ -383,8 +467,8 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue? currentRow.rowMeta.changed = false for (const field of (args.metaValue || meta.value)?.columns ?? []) { if (isVirtualCol(field)) continue - if (field.title! in currentRow.row && currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) { - await updateOrSaveRow(currentRow, field.title!, {}, args) + if (currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) { + await updateOrSaveRow(currentRow, field.title!, args) } } } diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index 10e48615cf..8b862c0e50 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -137,6 +137,7 @@ export default { :body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }" :closable="false" class="nc-drawer-expanded-form" + :class="{ 'active': isExpanded }" > diff --git a/packages/nc-gui/components/virtual-cell/BelongsTo.vue b/packages/nc-gui/components/virtual-cell/BelongsTo.vue index 8a2eaed14f..6e2e68fce1 100644 --- a/packages/nc-gui/components/virtual-cell/BelongsTo.vue +++ b/packages/nc-gui/components/virtual-cell/BelongsTo.vue @@ -15,6 +15,7 @@ import { inject, ref, useProvideLTARStore, + useSelectedCellKeyupListener, useSmartsheetRowStoreOrThrow, useUIPermission, } from '#imports' @@ -70,6 +71,15 @@ const unlinkRef = async (rec: Record) => { await unlink(rec) } } + +useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + listItemsDlg.value = true + e.stopPropagation() + break + } +}) diff --git a/packages/nc-gui/components/virtual-cell/Formula.vue b/packages/nc-gui/components/virtual-cell/Formula.vue index f91a8a8cab..7d25764c04 100644 --- a/packages/nc-gui/components/virtual-cell/Formula.vue +++ b/packages/nc-gui/components/virtual-cell/Formula.vue @@ -1,7 +1,7 @@ @@ -35,15 +41,19 @@ const urls = computed(() => replaceUrlsWithLink(result.value)) ERR! - + {{ result }} - + Warning: Formula fields should be configured in the field menu dropdown. + + + Warning: Computed field - unable to clear text. + diff --git a/packages/nc-gui/components/virtual-cell/HasMany.vue b/packages/nc-gui/components/virtual-cell/HasMany.vue index 82d7cbbb06..5c6f7777fd 100644 --- a/packages/nc-gui/components/virtual-cell/HasMany.vue +++ b/packages/nc-gui/components/virtual-cell/HasMany.vue @@ -13,6 +13,7 @@ import { inject, ref, useProvideLTARStore, + useSelectedCellKeyupListener, useSmartsheetRowStoreOrThrow, useUIPermission, } from '#imports' @@ -81,6 +82,15 @@ const onAttachRecord = () => { childListDlg.value = false listItemsDlg.value = true } + +useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + listItemsDlg.value = true + e.stopPropagation() + break + } +}) diff --git a/packages/nc-gui/components/virtual-cell/Lookup.vue b/packages/nc-gui/components/virtual-cell/Lookup.vue index 789ff99187..942a0879db 100644 --- a/packages/nc-gui/components/virtual-cell/Lookup.vue +++ b/packages/nc-gui/components/virtual-cell/Lookup.vue @@ -11,6 +11,7 @@ import { computed, inject, provide, + refAutoReset, useColumn, useMetas, } from '#imports' @@ -46,45 +47,73 @@ provide(MetaInj, lookupTableMeta) provide(CellUrlDisableOverlayInj, ref(true)) const lookupColumnMetaProps = useColumn(lookupColumn) + +const timeout = 3000 // in ms + +const showEditWarning = refAutoReset(false, timeout) +const showClearWarning = refAutoReset(false, timeout) + +useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + showEditWarning.value = true + break + case 'Delete': + showClearWarning.value = true + break + } +}) - - - - - - + + + + + + + + + + + + + + + + class="min-w-max" + :class="{ + 'bg-gray-100 px-1 rounded-full flex-1': !lookupColumnMetaProps.isAttachment, + ' border-gray-200 rounded border-1': ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes( + lookupColumn.uidt, + ), + }" + > + + - - - - - - - - - - - + + + + + Warning: Computed field - unable to edit content. + + + + Warning: Computed field - unable to clear content. + + diff --git a/packages/nc-gui/components/virtual-cell/ManyToMany.vue b/packages/nc-gui/components/virtual-cell/ManyToMany.vue index 3464471789..e028f4fd2a 100644 --- a/packages/nc-gui/components/virtual-cell/ManyToMany.vue +++ b/packages/nc-gui/components/virtual-cell/ManyToMany.vue @@ -2,6 +2,7 @@ import type { ColumnType } from 'nocodb-sdk' import type { Ref } from 'vue' import { + ActiveCellInj, CellValueInj, ColumnInj, IsFormInj, @@ -14,6 +15,7 @@ import { inject, ref, useProvideLTARStore, + useSelectedCellKeyupListener, useSmartsheetRowStoreOrThrow, useUIPermission, } from '#imports' @@ -82,6 +84,15 @@ const onAttachRecord = () => { childListDlg.value = false listItemsDlg.value = true } + +useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + listItemsDlg.value = true + e.stopPropagation() + break + } +}) diff --git a/packages/nc-gui/components/virtual-cell/Rollup.vue b/packages/nc-gui/components/virtual-cell/Rollup.vue index 68a3e3259b..dea3a85e7c 100644 --- a/packages/nc-gui/components/virtual-cell/Rollup.vue +++ b/packages/nc-gui/components/virtual-cell/Rollup.vue @@ -1,11 +1,40 @@ - - {{ value }} - + + + {{ value }} + + + + + + Warning: Computed field - unable to edit content. + + + + Warning: Computed field - unable to clear content. + + + diff --git a/packages/nc-gui/components/virtual-cell/components/ListItems.vue b/packages/nc-gui/components/virtual-cell/components/ListItems.vue index df6700cd02..147627bc1b 100644 --- a/packages/nc-gui/components/virtual-cell/components/ListItems.vue +++ b/packages/nc-gui/components/virtual-cell/components/ListItems.vue @@ -1,18 +1,20 @@ @@ -115,6 +163,7 @@ watch(expandedFormDlg, (nexVal) => { :placeholder="$t('placeholder.filterQuery')" class="max-w-[200px]" size="small" + @keydown.capture.stop /> @@ -132,7 +181,9 @@ watch(expandedFormDlg, (nexVal) => { {{ refRow[relatedTablePrimaryValueProp] }} @@ -175,4 +226,8 @@ watch(expandedFormDlg, (nexVal) => { :deep(.ant-pagination-item a) { line-height: 21px !important; } + +:deep(.nc-selected-row) { + @apply !ring; +} diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index 23f634af3a..96bfdd80a0 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -1,6 +1,6 @@ import type { MaybeRef } from '@vueuse/core' import { UITypes } from 'nocodb-sdk' -import { message, reactive, unref, useCopy, useEventListener, useI18n } from '#imports' +import { message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports' interface SelectedBlock { row: number | null @@ -13,16 +13,19 @@ interface SelectedBlock { export function useMultiSelect( fields: MaybeRef, data: MaybeRef, - editEnabled: MaybeRef, + _editEnabled: MaybeRef, isPkAvail: MaybeRef, clearCell: Function, makeEditable: Function, scrollToActiveCell?: (row?: number | null, col?: number | null) => void, + keyEventHandler?: Function, ) { const { t } = useI18n() const { copy } = useCopy() + 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 @@ -38,6 +41,7 @@ export function useMultiSelect( function selectCell(row: number, col: number) { clearRangeRows() + editEnabled.value = false selected.row = row selected.col = col } @@ -126,6 +130,11 @@ export function useMultiSelect( }) const onKeyDown = async (e: KeyboardEvent) => { + // invoke the keyEventHandler if provided and return if it returns true + if (await keyEventHandler?.(e)) { + return + } + if ( !isNaN(selectedRows.startRow) && !isNaN(selectedRows.startCol) && @@ -148,16 +157,20 @@ export function useMultiSelect( if (e.shiftKey) { if (selected.col > 0) { selected.col-- + editEnabled.value = false } else if (selected.row > 0) { selected.row-- selected.col = unref(columnLength) - 1 + editEnabled.value = false } } else { if (selected.col < unref(columnLength) - 1) { selected.col++ + editEnabled.value = false } else if (selected.row < unref(data).length - 1) { selected.row++ selected.col = 0 + editEnabled.value = false } } scrollToActiveCell?.() @@ -170,11 +183,9 @@ export function useMultiSelect( break /** on delete key press clear cell */ case 'Delete': - if (!unref(editEnabled)) { - e.preventDefault() - clearRangeRows() - await clearCell(selected as { row: number; col: number }) - } + e.preventDefault() + clearRangeRows() + await clearCell(selected as { row: number; col: number }) break /** on arrow key press navigate through cells */ case 'ArrowRight': @@ -183,6 +194,7 @@ export function useMultiSelect( if (selected.col < unref(columnLength) - 1) { selected.col++ scrollToActiveCell?.() + editEnabled.value = false } break case 'ArrowLeft': @@ -192,6 +204,7 @@ export function useMultiSelect( if (selected.col > 0) { selected.col-- scrollToActiveCell?.() + editEnabled.value = false } break case 'ArrowUp': @@ -201,6 +214,7 @@ export function useMultiSelect( if (selected.row > 0) { selected.row-- scrollToActiveCell?.() + editEnabled.value = false } break case 'ArrowDown': @@ -210,6 +224,7 @@ export function useMultiSelect( if (selected.row < unref(data).length - 1) { selected.row++ scrollToActiveCell?.() + editEnabled.value = false } break default: diff --git a/packages/nc-gui/composables/useSelectedCellKeyupListener/index.ts b/packages/nc-gui/composables/useSelectedCellKeyupListener/index.ts new file mode 100644 index 0000000000..e8e696b96a --- /dev/null +++ b/packages/nc-gui/composables/useSelectedCellKeyupListener/index.ts @@ -0,0 +1,21 @@ +import { isClient } from '@vueuse/core' +import type { Ref } from 'vue' + +export function useSelectedCellKeyupListener(selected: Ref, handler: (e: KeyboardEvent) => void) { + if (isClient) { + watch(selected, (nextVal, _, cleanup) => { + // bind listener when `selected` is truthy + if (nextVal) { + document.addEventListener('keydown', handler, true) + // if `selected` is falsy then remove the event handler + } else { + document.removeEventListener('keydown', handler, true) + } + + // cleanup is called whenever the watcher is re-evaluated or stopped + cleanup(() => { + document.removeEventListener('keydown', handler, true) + }) + }) + } +} diff --git a/packages/nc-gui/composables/useTable.ts b/packages/nc-gui/composables/useTable.ts index a1799bc4dd..dbe2a05bad 100644 --- a/packages/nc-gui/composables/useTable.ts +++ b/packages/nc-gui/composables/useTable.ts @@ -79,6 +79,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) { okText: t('general.yes'), okType: 'danger', cancelText: t('general.no'), + width: 450, async onOk() { try { const meta = (await getMeta(table.id as string, true)) as TableType diff --git a/packages/nc-gui/utils/browserUtils.ts b/packages/nc-gui/utils/browserUtils.ts new file mode 100644 index 0000000000..a4efcc1801 --- /dev/null +++ b/packages/nc-gui/utils/browserUtils.ts @@ -0,0 +1,2 @@ +// refer - https://stackoverflow.com/a/11752084 +export const isMac = () => /Mac/i.test(navigator.platform) diff --git a/packages/nc-gui/utils/columnUtils.ts b/packages/nc-gui/utils/columnUtils.ts index abd746fe42..f1e2f220cc 100644 --- a/packages/nc-gui/utils/columnUtils.ts +++ b/packages/nc-gui/utils/columnUtils.ts @@ -25,7 +25,6 @@ import TimerOutline from '~icons/mdi/timer-outline' import Star from '~icons/mdi/star' import MathIntegral from '~icons/mdi/math-integral' import MovieRoll from '~icons/mdi/movie-roll' -import Counter from '~icons/mdi/counter' import CalendarClock from '~icons/mdi/calendar-clock' import ID from '~icons/mdi/identifier' import RulerSquareCompass from '~icons/mdi/ruler-square-compass' @@ -123,18 +122,10 @@ const uiTypes = [ icon: MovieRoll, virtual: 1, }, - { - name: UITypes.Count, - icon: Counter, - }, { name: UITypes.DateTime, icon: CalendarClock, }, - { - name: UITypes.AutoNumber, - icon: Numeric, - }, { name: UITypes.Geometry, icon: RulerSquareCompass, diff --git a/packages/nc-gui/utils/index.ts b/packages/nc-gui/utils/index.ts index 7a2cf1d395..22bbddc16f 100644 --- a/packages/nc-gui/utils/index.ts +++ b/packages/nc-gui/utils/index.ts @@ -19,3 +19,4 @@ export * from './dataUtils' export * from './userUtils' export * from './stringUtils' export * from './memStorage' +export * from './browserUtils' diff --git a/packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts b/packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts index 1675975865..214609df9a 100644 --- a/packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts +++ b/packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts @@ -1417,8 +1417,6 @@ export class PgUi { case 'internal': case 'interval': return 'string'; - case 'jsonb': - return 'string'; case 'language_handler': case 'line': @@ -1533,6 +1531,7 @@ export class PgUi { case 'multipolygon': return 'string'; case 'json': + case 'jsonb': return 'json'; } } diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index ea468a74d2..115d8eb80a 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -9337,9 +9337,9 @@ } }, "node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -17009,9 +17009,9 @@ } }, "node_modules/webpack-cli/node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -17424,9 +17424,9 @@ } }, "node_modules/webpack/node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -25360,9 +25360,9 @@ "dev": true }, "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -31378,9 +31378,9 @@ } }, "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -31675,9 +31675,9 @@ } }, "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "requires": { "big.js": "^5.2.2", diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts index e9a7f0e57b..3ca0a2b256 100644 --- a/packages/nocodb/src/lib/Noco.ts +++ b/packages/nocodb/src/lib/Noco.ts @@ -101,7 +101,7 @@ export default class Noco { constructor() { process.env.PORT = process.env.PORT || '8080'; // todo: move - process.env.NC_VERSION = '0090000'; + process.env.NC_VERSION = '0098004'; // if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources if (process.env.NC_MINIMAL_DBS) { diff --git a/packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts b/packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts index 7db4f1f7e7..1f6bca8e91 100644 --- a/packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts +++ b/packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts @@ -18,6 +18,7 @@ export default class Backblaze implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -54,11 +55,12 @@ export default class Backblaze implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { @@ -126,7 +128,7 @@ export default class Backblaze implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); diff --git a/packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts b/packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts index c920eff6cf..c2f2b2f4c1 100644 --- a/packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts +++ b/packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts @@ -18,6 +18,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -54,11 +55,12 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { @@ -116,7 +118,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); diff --git a/packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts b/packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts index b2e6ed9162..b2be2ec329 100644 --- a/packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts +++ b/packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts @@ -18,6 +18,7 @@ export default class OvhCloud implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -54,11 +55,12 @@ export default class OvhCloud implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { @@ -116,7 +118,7 @@ export default class OvhCloud implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); diff --git a/packages/nocodb/src/lib/plugins/s3/S3.ts b/packages/nocodb/src/lib/plugins/s3/S3.ts index d1211ff7a1..8e4f1ea21f 100644 --- a/packages/nocodb/src/lib/plugins/s3/S3.ts +++ b/packages/nocodb/src/lib/plugins/s3/S3.ts @@ -18,6 +18,7 @@ export default class S3 implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -53,11 +54,12 @@ export default class S3 implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { @@ -119,7 +121,7 @@ export default class S3 implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); diff --git a/packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts b/packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts index 66034e020e..005c1e774a 100644 --- a/packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts +++ b/packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts @@ -36,7 +36,7 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); @@ -68,6 +68,7 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -104,11 +105,12 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { diff --git a/packages/nocodb/src/lib/plugins/spaces/Spaces.ts b/packages/nocodb/src/lib/plugins/spaces/Spaces.ts index 4880b79cb7..599edcee02 100644 --- a/packages/nocodb/src/lib/plugins/spaces/Spaces.ts +++ b/packages/nocodb/src/lib/plugins/spaces/Spaces.ts @@ -18,6 +18,7 @@ export default class Spaces implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -54,11 +55,12 @@ export default class Spaces implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { @@ -124,7 +126,7 @@ export default class Spaces implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); diff --git a/packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts b/packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts index f549634973..21ffaa4e09 100644 --- a/packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts +++ b/packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts @@ -18,6 +18,7 @@ export default class UpoCloud implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -54,11 +55,12 @@ export default class UpoCloud implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { @@ -114,7 +116,7 @@ export default class UpoCloud implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); diff --git a/packages/nocodb/src/lib/plugins/vultr/Vultr.ts b/packages/nocodb/src/lib/plugins/vultr/Vultr.ts index 2a18f0aeb4..5768e84076 100644 --- a/packages/nocodb/src/lib/plugins/vultr/Vultr.ts +++ b/packages/nocodb/src/lib/plugins/vultr/Vultr.ts @@ -18,6 +18,7 @@ export default class Vultr implements IStorageAdapterV2 { async fileCreate(key: string, file: XcFile): Promise { const uploadParams: any = { ACL: 'public-read', + ContentType: file.mimetype, }; return new Promise((resolve, reject) => { // Configure the file stream and obtain the upload parameters @@ -54,11 +55,12 @@ export default class Vultr implements IStorageAdapterV2 { url: url, encoding: null, }, - (err, _, body) => { + (err, httpResponse, body) => { if (err) return reject(err); uploadParams.Body = body; uploadParams.Key = key; + uploadParams.ContentType = httpResponse.headers['content-type']; // call S3 to retrieve upload file to specified bucket this.s3Client.upload(uploadParams, (err1, data) => { @@ -116,7 +118,7 @@ export default class Vultr implements IStorageAdapterV2 { await waitForStreamClose(createStream); await this.fileCreate('nc-test-file.txt', { path: tempFile, - mimetype: '', + mimetype: 'text/plain', originalname: 'temp.txt', size: '', }); diff --git a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts index 4419f20be6..3fab1baf5e 100644 --- a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts +++ b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts @@ -5,6 +5,7 @@ import NcMetaIO from '../meta/NcMetaIO'; import ncProjectEnvUpgrader from './ncProjectEnvUpgrader'; import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045'; import ncProjectUpgraderV2_0090000 from './ncProjectUpgraderV2_0090000'; +import ncDataTypesUpgrader from './ncDataTypesUpgrader'; const log = debug('nc:version-upgrader'); import { Tele } from 'nc-help'; @@ -31,6 +32,7 @@ export default class NcUpgrader { { name: '0011043', handler: ncProjectEnvUpgrader }, { name: '0011045', handler: ncProjectEnvUpgrader0011045 }, { name: '0090000', handler: ncProjectUpgraderV2_0090000 }, + { name: '0098004', handler: ncDataTypesUpgrader }, ]; if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) { return; diff --git a/packages/nocodb/src/lib/version-upgrader/ncDataTypesUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/ncDataTypesUpgrader.ts new file mode 100644 index 0000000000..4c4f240cee --- /dev/null +++ b/packages/nocodb/src/lib/version-upgrader/ncDataTypesUpgrader.ts @@ -0,0 +1,14 @@ +import { UITypes } from 'nocodb-sdk'; +import { MetaTable } from '../utils/globals'; +import { NcUpgraderCtx } from './NcUpgrader'; + +// The Count and AutoNumber types are removed +// so convert all existing Count and AutoNumber fields to Number type +export default async function (ctx: NcUpgraderCtx) { + // directly update uidt of all existing Count and AutoNumber fields to Number + await ctx.ncMeta.knex + .update({ uidt: UITypes.Number }) + .where({ uidt: UITypes.Count }) + .orWhere({ uidt: UITypes.AutoNumber }) + .table(MetaTable.COLUMNS); +} diff --git a/packages/nocodb/src/run/docker.ts b/packages/nocodb/src/run/docker.ts index b5ddc85520..4702e104d4 100644 --- a/packages/nocodb/src/run/docker.ts +++ b/packages/nocodb/src/run/docker.ts @@ -2,7 +2,6 @@ import cors from 'cors'; import express from 'express'; import Noco from '../lib/Noco'; -process.env.NC_VERSION = '0009044'; const server = express(); server.enable('trust proxy'); diff --git a/packages/nocodb/src/run/dockerRunMysql.ts b/packages/nocodb/src/run/dockerRunMysql.ts index 1478a6d3be..d126a9224d 100644 --- a/packages/nocodb/src/run/dockerRunMysql.ts +++ b/packages/nocodb/src/run/dockerRunMysql.ts @@ -2,7 +2,6 @@ import cors from 'cors'; import express from 'express'; import Noco from '../lib/Noco'; -process.env.NC_VERSION = '0009044'; const server = express(); server.enable('trust proxy'); diff --git a/packages/nocodb/src/run/dockerRunPG.ts b/packages/nocodb/src/run/dockerRunPG.ts index a2c6fdd015..2b0c568bf9 100644 --- a/packages/nocodb/src/run/dockerRunPG.ts +++ b/packages/nocodb/src/run/dockerRunPG.ts @@ -2,7 +2,6 @@ import cors from 'cors'; import express from 'express'; import Noco from '../lib/Noco'; -process.env.NC_VERSION = '0009044'; const server = express(); server.enable('trust proxy'); diff --git a/packages/nocodb/src/run/dockerRunPG_CyQuick.ts b/packages/nocodb/src/run/dockerRunPG_CyQuick.ts index 796522f1af..be74448739 100644 --- a/packages/nocodb/src/run/dockerRunPG_CyQuick.ts +++ b/packages/nocodb/src/run/dockerRunPG_CyQuick.ts @@ -2,7 +2,6 @@ import cors from 'cors'; import express from 'express'; import Noco from '../lib/Noco'; -process.env.NC_VERSION = '0009044'; const server = express(); server.enable('trust proxy'); diff --git a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts index 8c1566717e..bdd4adc18d 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts @@ -25,7 +25,14 @@ export class SelectOptionCellPageObject extends BasePage { option: string; multiSelect?: boolean; }) { - await this.get({ index, columnHeader }).click(); + const selectCell = this.get({ index, columnHeader }); + + // check if cell active + if (!(await selectCell.getAttribute('class')).includes('active')) { + await selectCell.click(); + } + + await selectCell.click(); await this.rootPage.getByTestId(`select-option-${columnHeader}-${index}`).getByText(option).click(); @@ -88,6 +95,13 @@ export class SelectOptionCellPageObject extends BasePage { } async verifyOptions({ index, columnHeader, options }: { index: number; columnHeader: string; options: string[] }) { + const selectCell = this.get({ index, columnHeader }); + + // check if cell active + if (!(await selectCell.getAttribute('class')).includes('active')) { + await selectCell.click(); + } + await this.get({ index, columnHeader }).click(); let counter = 0;