diff --git a/packages/nc-gui/app.vue b/packages/nc-gui/app.vue index f59b35c76d..99d4577b53 100644 --- a/packages/nc-gui/app.vue +++ b/packages/nc-gui/app.vue @@ -6,12 +6,37 @@ const route = useRoute() const disableBaseLayout = computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form')) useTheme() + +// TODO: Remove when https://github.com/vuejs/core/issues/5513 fixed +const key = ref(0) + +const messages = [ + `Uncaught NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.`, // chromium based + `NotFoundError: The object can not be found here.`, // safari + "Cannot read properties of null (reading 'parentNode')", +] + +if (typeof window !== 'undefined') { + // @ts-expect-error using arbitrary window key + if (!window.__ncvue) { + window.addEventListener('error', (event) => { + if (messages.includes(event.message)) { + event.preventDefault() + console.warn('Re-rendering layout because of https://github.com/vuejs/core/issues/5513') + key.value++ + } + }) + } + + // @ts-expect-error using arbitrary window key + window.__ncvue = true +} - + diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss index 7e3d0f5cd5..b5551d6076 100644 --- a/packages/nc-gui/assets/style.scss +++ b/packages/nc-gui/assets/style.scss @@ -214,17 +214,13 @@ a { @apply z-1 relative color-transition rounded-md px-4 py-2 text-white; &::after { - @apply ring-opacity-100 ring-[2px] ring-slate-300 rounded absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary bg-opacity-100; + @apply rounded absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary bg-opacity-100; content: ''; z-index: -1; } &:hover::after { - @apply transform scale-110 ring ring-accent; - } - - &:active::after { - @apply ring ring-accent; + @apply transform scale-110; } } @@ -261,7 +257,7 @@ a { } .ant-dropdown-menu-item, .ant-menu-item { - @apply !py-0 active:(ring ring-accent ring-opacity-100); + @apply py-0; } .ant-dropdown-menu-title-content, diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts index 1be206af09..6e9b243c03 100644 --- a/packages/nc-gui/components.d.ts +++ b/packages/nc-gui/components.d.ts @@ -24,6 +24,7 @@ declare module '@vue/runtime-core' { ADivider: typeof import('ant-design-vue/es')['Divider'] ADrawer: typeof import('ant-design-vue/es')['Drawer'] ADropdown: typeof import('ant-design-vue/es')['Dropdown'] + ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton'] AEmpty: typeof import('ant-design-vue/es')['Empty'] AForm: typeof import('ant-design-vue/es')['Form'] AFormItem: typeof import('ant-design-vue/es')['FormItem'] @@ -95,8 +96,6 @@ declare module '@vue/runtime-core' { MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default'] MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default'] MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default'] - MaterialSymbolsKeyboardShift: typeof import('~icons/material-symbols/keyboard-shift')['default'] - MaterialSymbolsLightMode: typeof import('~icons/material-symbols/light-mode')['default'] MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default'] MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default'] MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default'] @@ -126,6 +125,7 @@ declare module '@vue/runtime-core' { MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default'] MdiCalculator: typeof import('~icons/mdi/calculator')['default'] MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default'] + MdiCancel: typeof import('~icons/mdi/cancel')['default'] MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default'] MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default'] MdiChat: typeof import('~icons/mdi/chat')['default'] @@ -143,6 +143,7 @@ declare module '@vue/runtime-core' { MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default'] MdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] MdiContentSave: typeof import('~icons/mdi/content-save')['default'] + MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['default'] MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default'] MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default'] MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default'] @@ -200,6 +201,7 @@ declare module '@vue/runtime-core' { MdiPlus: typeof import('~icons/mdi/plus')['default'] MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default'] MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default'] + MdiPlusThick: typeof import('~icons/mdi/plus-thick')['default'] MdiReddit: typeof import('~icons/mdi/reddit')['default'] MdiRefresh: typeof import('~icons/mdi/refresh')['default'] MdiReload: typeof import('~icons/mdi/reload')['default'] diff --git a/packages/nc-gui/components/cell/MultiSelect.vue b/packages/nc-gui/components/cell/MultiSelect.vue index f3f09fecba..dcfd660be9 100644 --- a/packages/nc-gui/components/cell/MultiSelect.vue +++ b/packages/nc-gui/components/cell/MultiSelect.vue @@ -1,19 +1,23 @@ @@ -152,17 +240,20 @@ useSelectedCellKeyupListener(active, (e) => { mode="multiple" class="w-full" :bordered="false" + clear-icon :show-arrow="!readOnly" - :show-search="false" + :show-search="active || editable" + :open="isOpen && (active || editable)" :disabled="readOnly" :class="{ '!ml-[-8px]': readOnly }" :dropdown-class-name="`nc-dropdown-multi-select-cell ${isOpen ? 'active' : ''}`" - @keydown.enter.stop + @search="search" + @keydown.stop @click="isOpen = (active || editable) && !isOpen" > { + + + + + Create new option named {{ searchVal }} + + + + { @apply "flex overflow-hidden"; } - diff --git a/packages/nc-gui/components/cell/Percent.vue b/packages/nc-gui/components/cell/Percent.vue index 04bbfe2dcf..641f00890f 100644 --- a/packages/nc-gui/components/cell/Percent.vue +++ b/packages/nc-gui/components/cell/Percent.vue @@ -1,4 +1,5 @@ +import { message } from 'ant-design-vue' import tinycolor from 'tinycolor2' import type { Select as AntSelect } from 'ant-design-vue' import type { SelectOptionType } from 'nocodb-sdk' @@ -9,12 +10,13 @@ import { IsKanbanInj, ReadonlyInj, computed, + enumColor, + extractSdkResponseErrorMsg, inject, ref, - useEventListener, + useSelectedCellKeyupListener, watch, } from '#imports' -import { useSelectedCellKeyupListener } from '~/composables/useSelectedCellKeyupListener' interface Props { modelValue?: string | undefined @@ -39,12 +41,19 @@ const isOpen = ref(false) const isKanban = inject(IsKanbanInj, ref(false)) -const vModel = computed({ - get: () => modelValue, - set: (val) => emit('update:modelValue', val || null), -}) +const isPublic = inject(IsPublicInj, ref(false)) + +const { $api } = useNuxtApp() + +const searchVal = ref() + +const { getMeta } = useMetas() + +// a variable to keep newly created option value +// temporary until it's add the option to column meta +const tempSelectedOptState = ref() -const options = computed(() => { +const options = computed<(SelectOptionType & { value: string })[]>(() => { if (column?.value.colOptions) { const opts = column.value.colOptions ? // todo: fix colOptions type, options does not exist as a property @@ -53,19 +62,27 @@ const options = computed(() => { for (const op of opts.filter((el: any) => el.order === null)) { op.title = op.title.replace(/^'/, '').replace(/'$/, '') } - return opts + return opts.map((o: any) => ({ ...o, value: o.title })) } return [] }) -const handleClose = (e: MouseEvent) => { - if (aselect.value && !aselect.value.$el.contains(e.target)) { - isOpen.value = false - aselect.value.blur() - } -} +const isOptionMissing = computed(() => { + return (options.value ?? []).every((op) => op.title !== searchVal.value) +}) -useEventListener(document, 'click', handleClose) +const vModel = computed({ + get: () => tempSelectedOptState.value ?? modelValue, + set: (val) => { + if (isOptionMissing.value && val === searchVal.value) { + tempSelectedOptState.value = val + return addIfMissingAndSave().finally(() => { + tempSelectedOptState.value = undefined + }) + } + emit('update:modelValue', val || null) + }, +}) watch(isOpen, (n, _o) => { if (!n) { @@ -87,6 +104,50 @@ useSelectedCellKeyupListener(active, (e) => { break } }) + +async function addIfMissingAndSave() { + if (!searchVal.value || isPublic.value) return false + + const newOptValue = searchVal.value + searchVal.value = '' + + if (newOptValue && !options.value.some((o) => o.title === newOptValue)) { + try { + options.value.push({ + title: newOptValue, + value: newOptValue, + color: enumColor.light[(options.value.length + 1) % enumColor.light.length], + }) + column.value.colOptions = { options: options.value.map(({ value: _, ...rest }) => rest) } + + await $api.dbTableColumn.update((column.value as { fk_column_id?: string })?.fk_column_id || (column.value?.id as string), { + ...column.value, + }) + vModel.value = newOptValue + await getMeta(column.value.fk_model_id!, true) + } catch (e) { + console.log(e) + message.error(await extractSdkResponseErrorMsg(e)) + } + } +} + +const search = () => { + searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value +} + +const toggleMenu = (e: Event) => { + // todo: refactor + // check clicked element is clear icon + if ( + (e.target as HTMLElement)?.classList.contains('ant-select-clear') || + (e.target as HTMLElement)?.closest('.ant-select-clear') + ) { + vModel.value = '' + return + } + isOpen.value = (active.value || editable.value) && !isOpen.value +} @@ -96,13 +157,15 @@ useSelectedCellKeyupListener(active, (e) => { class="w-full" :allow-clear="!column.rqd && active" :bordered="false" - :open="isOpen" + :open="isOpen && (active || editable)" :disabled="readOnly" :show-arrow="!readOnly && (active || editable || vModel === null)" :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`" + :show-search="active || editable" @select="isOpen = false" - @keydown.enter.stop - @click="isOpen = (active || editable) && !isOpen" + @keydown.stop + @search="search" + @click="toggleMenu" > { + + + + + + Create new option named {{ searchVal }} + + + @@ -141,6 +213,3 @@ useSelectedCellKeyupListener(active, (e) => { opacity: 1; } - diff --git a/packages/nc-gui/components/cell/Text.vue b/packages/nc-gui/components/cell/Text.vue index 465cfd73f9..05745ead2c 100644 --- a/packages/nc-gui/components/cell/Text.vue +++ b/packages/nc-gui/components/cell/Text.vue @@ -16,7 +16,9 @@ const readonly = inject(ReadonlyInj, ref(false)) const vModel = useVModel(props, 'modelValue', emits) -const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() +const focus: VNodeRef = (el) => { + ;(el as HTMLInputElement)?.focus() +} diff --git a/packages/nc-gui/components/dlg/TableCreate.vue b/packages/nc-gui/components/dlg/TableCreate.vue index 7c36fa06e7..46fcb06b87 100644 --- a/packages/nc-gui/components/dlg/TableCreate.vue +++ b/packages/nc-gui/components/dlg/TableCreate.vue @@ -1,5 +1,5 @@ diff --git a/packages/nc-gui/components/dlg/TableRename.vue b/packages/nc-gui/components/dlg/TableRename.vue index e2f6f86bad..3c18db606c 100644 --- a/packages/nc-gui/components/dlg/TableRename.vue +++ b/packages/nc-gui/components/dlg/TableRename.vue @@ -119,6 +119,7 @@ const renameTable = async () => { await $api.dbTable.update(tableMeta.id as string, { project_id: tableMeta.project_id, table_name: formState.title, + title: formState.title, }) dialogShow.value = false diff --git a/packages/nc-gui/components/dlg/ViewCreate.vue b/packages/nc-gui/components/dlg/ViewCreate.vue index eb599963e3..5043c5754c 100644 --- a/packages/nc-gui/components/dlg/ViewCreate.vue +++ b/packages/nc-gui/components/dlg/ViewCreate.vue @@ -184,7 +184,7 @@ async function onSubmit() { - {{ $t(`general.${selectedViewId ? 'duplicate' : 'create'}`) }} {{ typeAlias }} + {{ $t(`general.${selectedViewId ? 'duplicate' : 'create'}`) }} {{ typeAlias }} {{ $t('objects.view') }} diff --git a/packages/nc-gui/components/shared-view/AskPassword.vue b/packages/nc-gui/components/shared-view/AskPassword.vue index ab9680cc88..3edce49ca7 100644 --- a/packages/nc-gui/components/shared-view/AskPassword.vue +++ b/packages/nc-gui/components/shared-view/AskPassword.vue @@ -1,4 +1,6 @@ @@ -42,7 +46,7 @@ const onFinish = async () => { - + Unlock diff --git a/packages/nc-gui/components/smartsheet/Cell.vue b/packages/nc-gui/components/smartsheet/Cell.vue index dfab72b7fa..95f3c682e4 100644 --- a/packages/nc-gui/components/smartsheet/Cell.vue +++ b/packages/nc-gui/components/smartsheet/Cell.vue @@ -111,7 +111,6 @@ const vModel = computed({ }) const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => { - console.log('syncAndNavigate', e.target) if (isJSON(column.value)) return if (currentRow.value.rowMeta.changed || currentRow.value.rowMeta.new) { @@ -136,7 +135,7 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => { > - + diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index fcaa154b32..d007bfb074 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -423,6 +423,7 @@ onClickOutside(smartTable, (e) => { const onNavigate = (dir: NavigateDir) => { if (selected.row === null || selected.col === null) return + editEnabled = false switch (dir) { case NavigateDir.NEXT: if (selected.row < data.value.length - 1) { @@ -435,8 +436,6 @@ const onNavigate = (dir: NavigateDir) => { case NavigateDir.PREV: if (selected.row > 0) { selected.row-- - } else { - editEnabled = false } break } @@ -521,11 +520,14 @@ provide(ReloadRowDataHookInj, reloadViewDataHook) // trigger initial data load in grid // reloadViewDataHook.trigger() +const switchingTab = ref(false) + watch( view, async (next, old) => { try { if (next && next.id !== old?.id) { + switchingTab.value = true // whenever tab changes or view changes save any unsaved data if (old?.id) { const oldMeta = await getMeta(old.fk_model_id!) @@ -541,6 +543,8 @@ watch( } } catch (e) { console.log(e) + } finally { + switchingTab.value = false } }, { immediate: true }, @@ -707,7 +711,7 @@ watch( @mouseover="selectBlock(rowIndex, colIndex)" @contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })" > - + +import { useEventListener } from '@vueuse/core' import { UITypes, isVirtualCol } from 'nocodb-sdk' import { IsFormInj, @@ -116,6 +117,12 @@ onMounted(() => { formState.value.column_name = formState.value?.title } }) + +useEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Escape') { + emit('cancel') + } +}) diff --git a/packages/nc-gui/components/smartsheet/column/SelectOptions.vue b/packages/nc-gui/components/smartsheet/column/SelectOptions.vue index a3295fa84c..f02646718b 100644 --- a/packages/nc-gui/components/smartsheet/column/SelectOptions.vue +++ b/packages/nc-gui/components/smartsheet/column/SelectOptions.vue @@ -163,12 +163,12 @@ watch(inputs, () => { v-model:value="element.title" class="caption" :data-testid="`select-column-option-input-${index}`" + @keydown.enter.prevent="element.title?.trim() && addNewOption()" @change="optionChanged(element.id)" /> diff --git a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue index 8519599e32..5141bc8313 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/Header.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/Header.vue @@ -25,6 +25,8 @@ const { isUIAllowed } = useUIPermission() const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) +const saveRowAndStay = ref(0) + const save = async () => { if (isNew.value) { const data = await _save(state.value) @@ -34,6 +36,9 @@ const save = async () => { await _save() reloadTrigger?.trigger() } + if (!saveRowAndStay.value) { + emit('cancel') + } } // todo: accept as a prop / inject @@ -101,14 +106,39 @@ const copyRecordUrl = () => { - - {{ $t('general.cancel') }} + + + + {{ $t('general.close') }} + - - - {{ $t('activity.saveRow') }} - + + + + + + + {{ $t('activity.saveAndExit') }} + + + + + + {{ $t('activity.saveAndStay') }} + + + + + + + {{ $t('activity.saveAndExit') }} + + + + {{ $t('activity.saveAndStay') }} + + diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index bad7419b47..5a731b2994 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -121,6 +121,12 @@ if (isKanban.value) { } } } + +const cellWrapperEl = (wrapperEl: HTMLElement) => { + nextTick(() => { + ;(wrapperEl?.querySelector('input,select,textarea') as HTMLInputElement)?.focus() + }) +} - + diff --git a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue index 89f97f45b6..07fa87a44d 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue @@ -14,6 +14,7 @@ import { inject, ref, resolveComponent, + useMenuCloseOnEsc, useNuxtApp, useViewColumns, watch, @@ -119,10 +120,14 @@ const getIcon = (c: ColumnType) => h(isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), { columnMeta: c, }) + +const open = ref(false) + +useMenuCloseOnEsc(open) - + diff --git a/packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue b/packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue index cf56ccf3a7..bde75f2511 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue @@ -1,5 +1,5 @@ @@ -248,7 +252,7 @@ watch(passwordProtected, (value) => { Use Survey Mode - + - - Use Theme - - - - - - - - - - - - - - RTL Orientation - + --> @@ -339,20 +304,45 @@ watch(passwordProtected, (value) => { - + - + {{ $t('labels.downloadAllowed') }} + + + + + Use Theme + + + + + + + + + + + + + + RTL Orientation + + diff --git a/packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue index 419588c57f..26f3791611 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue @@ -9,6 +9,7 @@ import { getSortDirectionOptions, inject, ref, + useMenuCloseOnEsc, useViewSorts, watch, } from '#imports' @@ -37,10 +38,14 @@ watch( }, { immediate: true }, ) + +const open = ref(false) + +useMenuCloseOnEsc(open) - + diff --git a/packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue b/packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue index 50440ecb9c..60ce79502f 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue @@ -12,6 +12,7 @@ import { inject, ref, useKanbanViewStoreOrThrow, + useMenuCloseOnEsc, useViewColumns, watch, } from '#imports' @@ -30,7 +31,9 @@ const { fields, loadViewColumns, metaColumnById } = useViewColumns(activeView, m const { kanbanMetaData, loadKanbanMeta, loadKanbanData, updateKanbanMeta, groupingField } = useKanbanViewStoreOrThrow() -const stackedByDropdown = ref(false) +const open = ref(false) + +useMenuCloseOnEsc(open) watch( () => activeView.value?.id, @@ -68,14 +71,14 @@ const singleSelectFieldOptions = computed(() => { }) const handleChange = () => { - stackedByDropdown.value = false + open.value = false } @@ -97,7 +100,7 @@ const handleChange = () => { diff --git a/packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue b/packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue index f525feb263..8289ff6dde 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue @@ -8,6 +8,7 @@ import { message, ref, useI18n, + useMenuCloseOnEsc, useNuxtApp, useProject, useSmartsheetStoreOrThrow, @@ -79,11 +80,15 @@ async function changeLockType(type: LockType) { } const { isSqlView } = useSmartsheetStoreOrThrow() + +const open = ref(false) + +useMenuCloseOnEsc(open) - + - + +import type { Input } from 'ant-design-vue' import { Form, computed, @@ -133,6 +134,10 @@ const clickInviteMore = () => { usersData.role = ProjectRole.Viewer usersData.emails = undefined } + +const emailField = (inputEl: typeof Input) => { + inputEl?.$el?.focus() +} @@ -222,6 +227,7 @@ const clickInviteMore = () => { {{ $t('datatype.Email') }}: { const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined) formState.value = { - ...formState.value, + ...(!isEdit.value && { + // only take title, column_name and uidt when creating a column + // to avoid the extra props from being taken (e.g. SingleLineText -> LTAR -> SingleLineText) + // to mess up the column creation + title: formState.value.title, + column_name: formState.value.column_name, + uidt: formState.value.uidt, + }), + ...(isEdit.value && { + // take the existing formState.value when editing a column + // LTAR is not available in this case + ...formState.value, + }), meta: {}, rqd: false, pk: false, diff --git a/packages/nc-gui/composables/useExpandedFormStore.ts b/packages/nc-gui/composables/useExpandedFormStore.ts index fe528de320..90a9dff500 100644 --- a/packages/nc-gui/composables/useExpandedFormStore.ts +++ b/packages/nc-gui/composables/useExpandedFormStore.ts @@ -46,8 +46,6 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m const activeView = inject(ActiveViewInj, ref()) - const { addOrEditStackRow } = useKanbanViewStoreOrThrow() - const { sharedView } = useSharedView() // getters @@ -197,6 +195,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m } if (activeView.value?.type === ViewTypes.KANBAN) { + const { addOrEditStackRow } = useKanbanViewStoreOrThrow() addOrEditStackRow(row.value, isNewRow) } diff --git a/packages/nc-gui/composables/useMenuCloseOnEsc/index.ts b/packages/nc-gui/composables/useMenuCloseOnEsc/index.ts new file mode 100644 index 0000000000..2365b1ff0d --- /dev/null +++ b/packages/nc-gui/composables/useMenuCloseOnEsc/index.ts @@ -0,0 +1,28 @@ +import { isClient } from '@vueuse/core' +import type { Ref } from 'vue' + +export function useMenuCloseOnEsc(open: Ref) { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + e.stopPropagation() + open.value = false + } + } + if (isClient) { + watch(open, (nextVal, _, cleanup) => { + // bind listener when `open` is truthy + if (nextVal) { + document.addEventListener('keydown', handler, true) + // if `open` 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/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index 96bfdd80a0..9b08d29554 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -41,6 +41,7 @@ export function useMultiSelect( function selectCell(row: number, col: number) { clearRangeRows() + if (selected.row === row && selected.col === col) return editEnabled.value = false selected.row = row selected.col = col @@ -132,7 +133,7 @@ export function useMultiSelect( const onKeyDown = async (e: KeyboardEvent) => { // invoke the keyEventHandler if provided and return if it returns true if (await keyEventHandler?.(e)) { - return + return true } if ( @@ -267,7 +268,7 @@ export function useMultiSelect( } if (unref(editEnabled) || e.ctrlKey || e.altKey || e.metaKey) { - return + return true } /** on letter key press make cell editable and empty */ diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index e37a640ffb..aa3c30391e 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -189,14 +189,21 @@ export function useViewData( : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) formattedData.value = formatData(response.list) paginationData.value = response.pageInfo + + // to cater the case like when querying with a non-zero offset + // the result page may point to the target page where the actual returned data don't display on + const expectedPage = Math.max(1, Math.ceil(paginationData.value.totalRows! / paginationData.value.pageSize!)) + if (expectedPage < paginationData.value.page!) { + await changePage(expectedPage) + } + if (viewMeta.value?.type === ViewTypes.GRID) { await loadAggCommentsCount() } } async function loadGalleryData() { - if (!viewMeta?.value?.id) return - + if (!viewMeta?.value?.id || isPublic.value) return galleryData.value = await $api.dbView.galleryRead(viewMeta.value.id) } diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index 00903d02e2..cf9a7791f9 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -369,6 +369,8 @@ "setPrimary": "Set as Primary value", "addRow": "Add new row", "saveRow": "Save row", + "saveAndExit": "Save & Exit", + "saveAndStay": "Save & Stay", "insertRow": "Insert New Row", "deleteRow": "Delete Row", "deleteSelectedRow": "Delete Selected Rows", diff --git a/packages/nc-gui/nuxt.config.ts b/packages/nc-gui/nuxt.config.ts index 885cbe78d9..afba37c6ba 100644 --- a/packages/nc-gui/nuxt.config.ts +++ b/packages/nc-gui/nuxt.config.ts @@ -141,7 +141,6 @@ export default defineNuxtConfig({ 'process.env.DEBUG': 'false', 'process.nextTick': () => {}, 'process.env.ANT_MESSAGE_DURATION': process.env.ANT_MESSAGE_DURATION, - 'process.env.NC_BACKEND_URL': process.env.NC_BACKEND_URL, }, server: { watch: { diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json index c3c3d22029..47ccc15770 100644 --- a/packages/nc-gui/package-lock.json +++ b/packages/nc-gui/package-lock.json @@ -85,28 +85,6 @@ "windicss": "^3.5.6" } }, - "../nocodb-sdk": { - "version": "0.98.4", - "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", @@ -8405,7 +8383,6 @@ "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", @@ -11761,8 +11738,21 @@ } }, "node_modules/nocodb-sdk": { - "resolved": "../nocodb-sdk", - "link": true + "version": "0.98.4", + "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" + } }, "node_modules/node-abi": { "version": "3.23.0", @@ -23571,8 +23561,7 @@ "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==", - "devOptional": true + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "form-data": { "version": "4.0.0", @@ -26040,22 +26029,20 @@ } }, "nocodb-sdk": { - "version": "file:../nocodb-sdk", + "version": "0.98.4", "requires": { - "@typescript-eslint/eslint-plugin": "^4.0.1", - "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.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", - "jsep": "^1.3.6", - "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "typescript": "^4.0.2" + "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" + } + } } }, "node-abi": { diff --git a/packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue b/packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue index d1d13b09af..771edf786c 100644 --- a/packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue +++ b/packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue @@ -44,7 +44,7 @@ const el = ref() provide(DropZoneRef, el) -const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 250) +const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 50) const steps = computed(() => { if (!formColumns.value) return [] @@ -305,7 +305,7 @@ onMounted(() => { - + ({ title: '', dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) }, inflection: { - inflectionColumn: 'camelize', - inflectionTable: 'camelize', + inflectionColumn: 'none', + inflectionTable: 'none', }, sslUse: SSLUsage.No, extraParameters: [], @@ -57,8 +57,8 @@ const customFormState = ref({ title: '', dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) }, inflection: { - inflectionColumn: 'camelize', - inflectionTable: 'camelize', + inflectionColumn: 'none', + inflectionTable: 'none', }, sslUse: SSLUsage.No, extraParameters: [], diff --git a/packages/nc-gui/pages/index/index/index.vue b/packages/nc-gui/pages/index/index/index.vue index 005c1bc37e..a93ccf366c 100644 --- a/packages/nc-gui/pages/index/index/index.vue +++ b/packages/nc-gui/pages/index/index/index.vue @@ -316,11 +316,7 @@ const copyProjectMeta = async () => { } &:hover::after { - @apply transform scale-110 ring ring-accent; - } - - &:active::after { - @apply ring ring-accent; + @apply transform scale-110; } } diff --git a/packages/nc-gui/utils/cell.ts b/packages/nc-gui/utils/cell.ts index 719071e1d2..9214578bf2 100644 --- a/packages/nc-gui/utils/cell.ts +++ b/packages/nc-gui/utils/cell.ts @@ -2,7 +2,8 @@ import type { ColumnType } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk' export const dataTypeLow = (column: ColumnType) => column.dt?.toLowerCase() -export const isBoolean = (abstractType: any) => abstractType === 'boolean' +export const isBoolean = (column: ColumnType, abstractType?: any) => + column.uidt === UITypes.Checkbox || abstractType === 'boolean' export const isString = (column: ColumnType, abstractType: any) => column.uidt === UITypes.SingleLineText || abstractType === 'string' export const isTextArea = (column: ColumnType) => column.uidt === UITypes.LongText diff --git a/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts b/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts index 01390ecb4e..d053da246f 100644 --- a/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts +++ b/packages/nc-gui/utils/parsers/CSVTemplateAdapter.ts @@ -145,9 +145,6 @@ export default class CSVTemplateAdapter { } // handle numeric case if (len === 2 && UITypes.Number in detectedColTypes && UITypes.Decimal in detectedColTypes) { - if (detectedColTypes[UITypes.Number] > detectedColTypes[UITypes.Decimal]) { - return UITypes.Number - } return UITypes.Decimal } // if there are multiple detected column types @@ -179,8 +176,10 @@ export default class CSVTemplateAdapter { ) { this.tables[tableIdx].columns[columnIdx].uidt = UITypes.Date // take the date format with the max occurrence - this.tables[tableIdx].columns[columnIdx].meta.date_format = - Object.keys(dateFormat).reduce((x, y) => (dateFormat[x] > dateFormat[y] ? x : y)) || 'YYYY/MM/DD' + const objKeys = Object.keys(dateFormat) + this.tables[tableIdx].columns[columnIdx].meta.date_format = objKeys.length + ? objKeys.reduce((x, y) => (dateFormat[x] > dateFormat[y] ? x : y)) + : 'YYYY/MM/DD' } else { // Datetime this.tables[tableIdx].columns[columnIdx].uidt = uidt @@ -238,6 +237,8 @@ export default class CSVTemplateAdapter { reject(e) }, }) + } else { + resolve(true) } }) } diff --git a/packages/noco-docs/content/en/getting-started/installation.md b/packages/noco-docs/content/en/getting-started/installation.md index 107158e4ed..2f341aee2a 100644 --- a/packages/noco-docs/content/en/getting-started/installation.md +++ b/packages/noco-docs/content/en/getting-started/installation.md @@ -478,7 +478,7 @@ It is mandatory to configure `NC_DB` environment variables for production usecas | NC_JWT_EXPIRES_IN | No | JWT token expiry time | `10h` | | | NC_CONNECT_TO_EXTERNAL_DB_DISABLED | No | Disable Project creation with external database | | | | NC_INVITE_ONLY_SIGNUP | No | Allow users to signup only via invite url, value should be any non-empty string. | | | -| NC_BACKEND_URL | No | Custom Backend URL | ``http://localhost:8080`` will be used | | +| NUXT_PUBLIC_NC_BACKEND_URL | No | Custom Backend URL | ``http://localhost:8080`` will be used | | | NC_REQUEST_BODY_SIZE | No | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | `1048576` | | | NC_EXPORT_MAX_TIMEOUT | No | After NC_EXPORT_MAX_TIMEOUT csv gets downloaded in batches | Default value 5000(in millisecond) will be used | | | NC_DISABLE_TELE | No | Disable telemetry | | | diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts index 495d7ca56c..8388bb2f4e 100644 --- a/packages/nocodb-sdk/src/lib/Api.ts +++ b/packages/nocodb-sdk/src/lib/Api.ts @@ -1950,6 +1950,7 @@ export class Api< tableId: string, data: { table_name?: string; + title?: string; project_id?: string; }, params: RequestParams = {} diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index 4c79858965..360535b26c 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -151,28 +151,6 @@ "vuedraggable": "^2.24.3" } }, - "../nocodb-sdk": { - "version": "0.98.4", - "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", @@ -10685,8 +10663,13 @@ "dev": true }, "node_modules/nocodb-sdk": { - "resolved": "../nocodb-sdk", - "link": true + "version": "0.98.4", + "resolved": "file:../nocodb-sdk", + "license": "AGPL-3.0-or-later", + "dependencies": { + "axios": "^0.21.1", + "jsep": "^1.3.6" + } }, "node_modules/node-abort-controller": { "version": "3.0.1", @@ -26108,22 +26091,10 @@ "dev": true }, "nocodb-sdk": { - "version": "file:../nocodb-sdk", + "version": "0.98.4", "requires": { - "@typescript-eslint/eslint-plugin": "^4.0.1", - "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.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", - "jsep": "^1.3.6", - "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "typescript": "^4.0.2" + "jsep": "^1.3.6" } }, "node-abort-controller": { diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts index 91cf8baac9..0dc767c026 100644 --- a/packages/nocodb/src/lib/Noco.ts +++ b/packages/nocodb/src/lib/Noco.ts @@ -213,7 +213,6 @@ export default class Noco { }); // to get ip addresses - this.router.use(requestIp.mw()); this.router.use(cookieParser()); this.router.use( diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts index 0a088f11cd..dfafa59340 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts @@ -176,7 +176,7 @@ class BaseModelSqlv2 { sortArr?: Sort[]; sort?: string | string[]; } = {}, - ignoreFilterSort = false + ignoreViewFilterAndSort = false ): Promise { const { where, ...rest } = this._getListArgs(args as any); @@ -190,7 +190,7 @@ class BaseModelSqlv2 { let sorts = extractSortsObject(rest?.sort, aliasColObjMap); const filterObj = extractFilterFromXwhere(where, aliasColObjMap); // todo: replace with view id - if (!ignoreFilterSort && this.viewId) { + if (!ignoreViewFilterAndSort && this.viewId) { await conditionV2( [ new Filter({ @@ -250,7 +250,7 @@ class BaseModelSqlv2 { qb.orderBy('created_at'); } - if (!ignoreFilterSort) applyPaginate(qb, rest); + if (!ignoreViewFilterAndSort) applyPaginate(qb, rest); const proto = await this.getProto(); const data = await this.extractRawQueryAndExec(qb); @@ -262,7 +262,7 @@ class BaseModelSqlv2 { public async count( args: { where?: string; limit?; filterArr?: Filter[] } = {}, - ignoreFilterSort = false + ignoreViewFilterAndSort = false ): Promise { await this.model.getColumns(); const { where } = this._getListArgs(args); @@ -273,7 +273,7 @@ class BaseModelSqlv2 { const aliasColObjMap = await this.model.getAliasColObjMap(); const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - if (!ignoreFilterSort && this.viewId) { + if (!ignoreViewFilterAndSort && this.viewId) { await conditionV2( [ new Filter({ @@ -2278,7 +2278,7 @@ class BaseModelSqlv2 { for (let i = 0; i < this.model.columns.length; ++i) { const column = this.model.columns[i]; // skip validation if `validate` is undefined or false - if (!column?.meta?.validate && !column?.validate) continue; + if (!column?.meta?.validate || !column?.validate) continue; const validate = column.getValidators(); const cn = column.column_name; @@ -2512,7 +2512,7 @@ class BaseModelSqlv2 { public async groupedList( args: { groupColumnId: string; - ignoreFilterSort?: boolean; + ignoreViewFilterAndSort?: boolean; options?: (string | number | null | boolean)[]; } & Partial ): Promise< @@ -2565,7 +2565,7 @@ class BaseModelSqlv2 { let sorts = extractSortsObject(args?.sort, aliasColObjMap); const filterObj = extractFilterFromXwhere(where, aliasColObjMap); // todo: replace with view id - if (!args.ignoreFilterSort && this.viewId) { + if (!args.ignoreViewFilterAndSort && this.viewId) { await conditionV2( [ new Filter({ @@ -2678,7 +2678,10 @@ class BaseModelSqlv2 { } public async groupedListCount( - args: { groupColumnId: string; ignoreFilterSort?: boolean } & XcFilter + args: { + groupColumnId: string; + ignoreViewFilterAndSort?: boolean; + } & XcFilter ) { const column = await this.model .getColumns() @@ -2697,7 +2700,7 @@ class BaseModelSqlv2 { const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap); // todo: replace with view id - if (!args.ignoreFilterSort && this.viewId) { + if (!args.ignoreViewFilterAndSort && this.viewId) { await conditionV2( [ new Filter({ diff --git a/packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts b/packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts index 024283b72e..2730a1f84d 100644 --- a/packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts +++ b/packages/nocodb/src/lib/meta/api/swagger/redocHtml.ts @@ -17,8 +17,33 @@ export default ` - - + + +