Browse Source

Merge branch 'develop' of https://github.com/nocodb/nocodb into fix/dropdown-ui

pull/7181/head
musharaf-nocodb 9 months ago
parent
commit
6eee8e727f
  1. 9
      packages/nc-gui/assets/style.scss
  2. 1
      packages/nc-gui/components/api-client/Headers.vue
  3. 2
      packages/nc-gui/components/cell/DatePicker.vue
  4. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 2
      packages/nc-gui/components/cell/GeoData.vue
  6. 9
      packages/nc-gui/components/cell/Json.vue
  7. 13
      packages/nc-gui/components/cell/TextArea.vue
  8. 2
      packages/nc-gui/components/cell/TimePicker.vue
  9. 2
      packages/nc-gui/components/cell/YearPicker.vue
  10. 1
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  11. 1
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  12. 5
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  13. 14
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  14. 8
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  15. 6
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  16. 2
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  17. 2
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  18. 2
      packages/nc-gui/components/general/language/index.vue
  19. 2
      packages/nc-gui/components/nc/Dropdown.vue
  20. 14
      packages/nc-gui/components/nc/Modal.vue
  21. 10
      packages/nc-gui/components/nc/Select.vue
  22. 11
      packages/nc-gui/components/nc/Tooltip.vue
  23. 2
      packages/nc-gui/components/project/AccessSettings.vue
  24. 45
      packages/nc-gui/components/project/View.vue
  25. 2
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  26. 4
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  27. 2
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  28. 2
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  29. 2
      packages/nc-gui/components/smartsheet/details/Fields.vue
  30. 4
      packages/nc-gui/components/smartsheet/grid/Table.vue
  31. 5
      packages/nc-gui/components/smartsheet/header/Cell.vue
  32. 2
      packages/nc-gui/components/smartsheet/header/Menu.vue
  33. 2
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  34. 6
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  35. 1
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  36. 8
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  37. 2
      packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue
  38. 2
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  39. 4
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  40. 5
      packages/nc-gui/components/template/Editor.vue
  41. 1
      packages/nc-gui/composables/useAttachment.ts
  42. 8
      packages/nc-gui/composables/useColumnCreateStore.ts
  43. 4
      packages/nc-gui/composables/useViewColumns.ts
  44. 2
      packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts
  45. 4
      packages/nc-gui/lang/en.json
  46. 44
      packages/nc-gui/lang/ja.json
  47. 2
      packages/nc-gui/store/bases.ts
  48. 6
      packages/nocodb/src/controllers/attachments-secure.controller.ts
  49. 18
      packages/nocodb/src/controllers/attachments.controller.ts
  50. 10
      packages/nocodb/src/db/BaseModelSqlv2.ts
  51. 3
      packages/nocodb/src/db/sql-client/lib/mysql/TidbClient.ts
  52. 6
      packages/nocodb/src/db/sql-client/lib/mysql/VitessClient.ts
  53. 4
      packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts
  54. 15
      packages/nocodb/src/db/sql-client/lib/pg/YugabyteClient.ts
  55. 16
      packages/nocodb/src/helpers/apiHelpers.ts
  56. 8
      packages/nocodb/src/services/attachments.service.ts
  57. 4
      tests/playwright/pages/Dashboard/Grid/index.ts

9
packages/nc-gui/assets/style.scss

@ -220,6 +220,10 @@ a {
@apply !rounded-md;
}
}
// select dropdown border style
.ant-select-dropdown {
@apply border-1 border-gray-200
}
// menu item styling
.nc-menu-item {
@ -421,7 +425,10 @@ a {
.ant-dropdown-menu-submenu {
@apply !py-0;
&.ant-dropdown-menu-submenu-popup{
@apply border-1 border-gray-200
}
.ant-dropdown-menu,
.ant-menu {
@apply m-0 p-0;

1
packages/nc-gui/components/api-client/Headers.vue

@ -91,6 +91,7 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
:options="headerList"
:placeholder="$t('placeholder.key')"
:filter-option="filterOption"
dropdown-class-name="border-1 border-gray-200"
/>
</a-form-item>
</td>

2
packages/nc-gui/components/cell/DatePicker.vue

@ -226,7 +226,7 @@ const clickHandler = () => {
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''}`"
:dropdown-class-name="`${randomClass} nc-picker-date children:border-1 children:border-gray-200 ${open ? 'active' : ''} `"
:open="isOpen"
@click="clickHandler"
@update:open="updateOpen"

2
packages/nc-gui/components/cell/DateTimePicker.vue

@ -273,7 +273,7 @@ const isColDisabled = computed(() => {
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`"
:dropdown-class-name="`${randomClass} nc-picker-datetime children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
:open="isOpen"
@click="clickHandler"
@ok="open = !open"

2
packages/nc-gui/components/cell/GeoData.vue

@ -100,7 +100,7 @@ const openInOSM = () => {
</div>
<div v-else data-testid="nc-geo-data-lat-long-set">{{ latLongStr }}</div>
<template #overlay>
<a-form :model="formState" class="flex flex-col w-max-64" @finish="handleFinish">
<a-form :model="formState" class="flex flex-col w-max-64 border-1 border-gray-200" @finish="handleFinish">
<a-form-item>
<div class="flex mt-4 items-center mx-2">
<div class="mr-2">{{ $t('labels.lat') }}:</div>

9
packages/nc-gui/components/cell/Json.vue

@ -150,7 +150,14 @@ watch(isExpanded, () => {
</script>
<template>
<component :is="isExpanded ? NcModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null">
<component
:is="isExpanded ? NcModal : 'div'"
v-model:visible="isExpanded"
:closable="false"
centered
:footer="null"
:wrap-class-name="isExpanded ? '!z-1051' : null"
>
<div v-if="editEnabled && !readonly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop>
<div class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop>
<a-button type="text" size="small" @click="isExpanded = !isExpanded">

13
packages/nc-gui/components/cell/TextArea.vue

@ -175,7 +175,7 @@ watch(editEnabled, () => {
:overlay-class-name="isVisible ? 'nc-textarea-dropdown-active' : undefined"
>
<div
class="flex flex-row pt-0.5 w-full"
class="flex flex-row pt-0.5 w-full rich-wrapper"
:class="{
'min-h-10': rowHeight !== 1,
'min-h-6.5': rowHeight === 1,
@ -239,7 +239,7 @@ watch(editEnabled, () => {
<NcTooltip
v-if="!isVisible"
placement="bottom"
class="!absolute right-0 bottom-1 !hidden nc-text-area-expand-btn"
class="!absolute right-0 bottom-1 nc-text-area-expand-btn"
:class="{ 'right-0 bottom-1': editEnabled, '!bottom-0': !isRichMode }"
>
<template #title>{{ $t('title.expand') }}</template>
@ -296,8 +296,13 @@ watch(editEnabled, () => {
textarea:focus {
box-shadow: none;
}
:deep(.nc-text-area-expand-btn) {
@apply !block;
@apply !hidden;
}
.rich-wrapper:hover,
.rich-wrapper:active {
:deep(.nc-text-area-expand-btn) {
@apply !block cursor-pointer;
}
}
</style>

2
packages/nc-gui/components/cell/TimePicker.vue

@ -136,7 +136,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
:open="isOpen"
:popup-class-name="`${randomClass} nc-picker-time ${open ? 'active' : ''}`"
:popup-class-name="`${randomClass} nc-picker-time children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@click="open = (active || editable) && !open"
@ok="open = !open"
>

2
packages/nc-gui/components/cell/YearPicker.vue

@ -121,7 +121,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:allow-clear="(!readOnly && !localState && !isPk) || isEditColumn"
:input-read-only="true"
:open="isOpen"
:dropdown-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''}`"
:dropdown-class-name="`${randomClass} nc-picker-year children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@click="open = (active || editable) && !open"
@change="open = (active || editable) && !open"
@ok="open = !open"

1
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -1,5 +1,4 @@
<script lang="ts" setup>
import GithubButton from 'vue-github-button'
import {
computed,
message,

1
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -429,6 +429,7 @@ const projectDelete = () => {
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
show-on-truncate-only
>
<template #title>{{ base.title }}</template>
<span @click="onProjectClick(base)">

5
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -231,7 +231,10 @@ const isTableOpened = computed(() => {
</div>
</div>
</div>
<NcTooltip class="nc-tbl-title nc-sidebar-node-title text-ellipsis w-full overflow-hidden select-none">
<NcTooltip
class="nc-tbl-title nc-sidebar-node-title text-ellipsis w-full overflow-hidden select-none"
show-on-truncate-only
>
<template #title>{{ table.title }}</template>
<span
:class="{

14
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -128,6 +128,16 @@ onKeyStroke('Enter', (event) => {
}
})
const onRenameMenuClick = () => {
if (isMobileMode.value || !isUIAllowed('viewCreateOrEdit')) return
if (!isEditing.value) {
isEditing.value = true
_title.value = vModel.value.title
$e('c:view:rename', { view: vModel.value?.type })
}
}
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
/** Rename a view */
@ -232,7 +242,7 @@ watch(isDropdownOpen, async () => {
@blur="onRename"
@keydown.stop="onKeyDown($event)"
/>
<NcTooltip v-else class="nc-sidebar-node-title text-ellipsis overflow-hidden select-none w-full">
<NcTooltip v-else class="nc-sidebar-node-title text-ellipsis overflow-hidden select-none w-full" show-on-truncate-only>
<template #title> {{ vModel.alias || vModel.title }}</template>
<div
data-testid="sidebar-view-title"
@ -268,7 +278,7 @@ watch(isDropdownOpen, async () => {
:table="table"
in-sidebar
@close-modal="isDropdownOpen = false"
@rename="onRename"
@rename="onRenameMenuClick"
@delete="onDelete"
/>
</template>

8
packages/nc-gui/components/dashboard/settings/BaseAudit.vue

@ -135,13 +135,14 @@ const columns = [
v-model:page-size="currentLimit"
:total="+totalRows"
show-less-items
class="pagination"
@change="loadAudits"
/>
</div>
</div>
</template>
<style lang="scss">
<style lang="scss" scoped>
.nc-audit-table pre {
display: table;
table-layout: fixed;
@ -150,4 +151,9 @@ const columns = [
font-size: unset;
font-family: unset;
}
.pagination {
.ant-select-dropdown {
@apply !border-1 !border-gray-200;
}
}
</style>

6
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -147,7 +147,7 @@ const toggleSelectAll = (role: Role) => {
<template>
<div class="flex flex-row w-full items-center justify-center">
<div class="flex flex-col w-[900px]">
<NcTooltip class="mb-4 first-letter:capital font-bold max-w-100 truncate">
<NcTooltip class="mb-4 first-letter:capital font-bold max-w-100 truncate" show-on-truncate-only>
<template #title>{{ base.title }}</template>
<span> UI ACL : {{ base.title }} </span>
</NcTooltip>
@ -211,7 +211,7 @@ const toggleSelectAll = (role: Role) => {
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="{ meta: record.table_meta, type: record.ptype }" class="text-gray-500" />
</div>
<NcTooltip class="overflow-ellipsis min-w-0 shrink-1 truncate">
<NcTooltip class="overflow-ellipsis min-w-0 shrink-1 truncate" show-on-truncate-only>
<template #title>{{ record._ptn }}</template>
<span>{{ record._ptn }}</span>
</NcTooltip>
@ -223,7 +223,7 @@ const toggleSelectAll = (role: Role) => {
<div class="min-w-5 flex items-center justify-center">
<GeneralViewIcon :meta="record" class="text-gray-500"></GeneralViewIcon>
</div>
<NcTooltip class="overflow-ellipsis min-w-0 shrink-1 truncate">
<NcTooltip class="overflow-ellipsis min-w-0 shrink-1 truncate" show-on-truncate-only>
<template #title>{{ record.title }}</template>
<span>{{ record.title }}</span>
</NcTooltip>

2
packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue

@ -148,7 +148,7 @@ const onRoleToggle = async () => {
<div class="flex flex-col py-2 px-3 gap-2 w-full" data-testid="nc-share-base-sub-modal">
<div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md">
<div class="flex flex-row w-full justify-between">
<div class="text-black font-medium">{{ $t('activity.enablePublicAccess') }}</div>
<div class="text-gray-900 font-medium">{{ $t('activity.enablePublicAccess') }}</div>
<a-switch
v-e="['c:share:base:enable:toggle']"
:checked="isSharedBaseEnabled"

2
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -277,7 +277,7 @@ const isPublicShared = computed(() => {
<div class="flex flex-col py-2 px-3 mb-1">
<div class="flex flex-col w-full mt-2.5 px-3 py-2.5 border-gray-200 border-1 rounded-md gap-y-2">
<div class="flex flex-row w-full justify-between py-0.5">
<div class="flex" :style="{ fontWeight: 500 }">{{ $t('activity.enabledPublicViewing') }}</div>
<div class="text-gray-900 font-medium">{{ $t('activity.enabledPublicViewing') }}</div>
<a-switch
v-e="['c:share:view:enable:toggle']"
data-testid="share-view-toggle"

2
packages/nc-gui/components/general/language/index.vue

@ -9,7 +9,7 @@
</div>
<template #overlay>
<a-menu class="nc-scrollbar-dark-md min-w-50 max-h-90vh overflow-auto !p-1 m-1 rounded-md">
<a-menu class="nc-scrollbar-dark-md min-w-50 max-h-90vh overflow-auto !p-1 m-1 rounded-md border-1 border-gray-200">
<GeneralLanguageMenu />
</a-menu>
</template>

2
packages/nc-gui/components/nc/Dropdown.vue

@ -25,7 +25,7 @@ const overlayClassName = toRef(props, 'overlayClassName')
const autoClose = computed(() => props.autoClose)
const overlayClassNameComputed = computed(() => {
let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-100 shadow-lg'
let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-200 shadow-lg'
if (overlayClassName.value) {
className += ` ${overlayClassName.value}`
}

14
packages/nc-gui/components/nc/Modal.vue

@ -6,17 +6,19 @@ const props = withDefaults(
size?: 'small' | 'medium' | 'large'
destroyOnClose?: boolean
maskClosable?: boolean
wrapClassName?: string
}>(),
{
size: 'medium',
destroyOnClose: true,
maskClosable: true,
wrapClassName: '',
},
)
const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, maskClosable } = props
const { width: propWidth, destroyOnClose, maskClosable, wrapClassName: _wrapClassName } = props
const { isMobileMode } = useGlobal()
@ -64,6 +66,14 @@ const height = computed(() => {
return 'auto'
})
const newWrapClassName = computed(() => {
let className = 'nc-modal-wrapper'
if (_wrapClassName) {
className += ` ${_wrapClassName}`
}
return className
})
const visible = useVModel(props, 'visible', emits)
const slots = useSlots()
@ -76,7 +86,7 @@ const slots = useSlots()
:width="width"
:centered="true"
:closable="false"
wrap-class-name="nc-modal-wrapper"
:wrap-class-name="newWrapClassName"
:footer="null"
:mask-closable="maskClosable"
:destroy-on-close="destroyOnClose"

10
packages/nc-gui/components/nc/Select.vue

@ -15,7 +15,13 @@ const emits = defineEmits(['update:value', 'change'])
const placeholder = computed(() => props.placeholder)
const dropdownClassName = computed(() => props.dropdownClassName)
const dropdownClassName = computed(() => {
let className = 'nc-select-dropdown'
if (props.dropdownClassName) {
className += ` ${props.dropdownClassName}`
}
return className
})
const showSearch = computed(() => props.showSearch)
@ -37,7 +43,7 @@ const onChange = (value: string) => {
v-model:value="vModel"
:placeholder="placeholder"
class="nc-select"
:dropdown-class-name="dropdownClassName ? `nc-select-dropdown ${dropdownClassName}` : 'nc-select-dropdown'"
:dropdown-class-name="dropdownClassName"
:show-search="showSearch"
:filter-option="filterOption"
:dropdown-match-select-width="dropdownMatchSelectWidth"

11
packages/nc-gui/components/nc/Tooltip.vue

@ -11,6 +11,7 @@ interface Props {
// force disable tooltip
disabled?: boolean
placement?: TooltipPlacement | undefined
showOnTruncateOnly?: boolean
hideOnClick?: boolean
overlayClassName?: string
}
@ -20,6 +21,7 @@ const props = defineProps<Props>()
const modifierKey = computed(() => props.modifierKey)
const tooltipStyle = computed(() => props.tooltipStyle)
const disabled = computed(() => props.disabled)
const showOnTruncateOnly = computed(() => props.showOnTruncateOnly)
const hideOnClick = computed(() => props.hideOnClick)
const placement = computed(() => props.placement ?? 'top')
@ -65,6 +67,15 @@ onKeyStroke(
)
watch([isHovering, () => modifierKey.value, () => disabled.value], ([hovering, key, isDisabled]) => {
if (showOnTruncateOnly?.value) {
const targetElement = el?.value
const isElementTruncated = targetElement && targetElement.scrollWidth > targetElement.clientWidth
if (!isElementTruncated) {
showTooltip.value = false
return
}
}
if (!hovering || isDisabled) {
showTooltip.value = false
return

2
packages/nc-gui/components/project/AccessSettings.vue

@ -177,7 +177,7 @@ onMounted(async () => {
</div>
<div v-else class="nc-collaborators-list mt-6 h-full">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]">
<div class="flex flex-row bg-gray-50 min-h-12 items-center">
<div class="flex flex-row bg-gray-50 min-h-12 items-center border-b-1">
<div class="text-gray-700 users-email-grid">{{ $t('objects.users') }}</div>
<div class="text-gray-700 date-joined-grid">{{ $t('title.dateJoined') }}</div>
<div class="text-gray-700 user-access-grid">{{ $t('general.access') }}</div>

45
packages/nc-gui/components/project/View.vue

@ -1,7 +1,13 @@
<script lang="ts" setup>
import { useTitle } from '@vueuse/core'
import NcLayout from '~icons/nc-icons/layout'
const { openedProject } = storeToRefs(useBases())
import { isEeUI } from '#imports'
const basesStore = useBases()
const { getProjectUsers } = basesStore
const { openedProject, activeProjectId, baseUserCount } = storeToRefs(basesStore)
const { activeTables } = storeToRefs(useTablesStore())
const { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace())
@ -26,6 +32,23 @@ const { isMobileMode } = useGlobal()
const baseSettingsState = ref('')
const userCount = isEeUI ? workspaceUserCount : baseUserCount
const updateBaseUserCount = async () => {
try {
const { totalRows } = await getProjectUsers({
baseId: activeProjectId.value!,
page: 1,
searchText: undefined,
limit: 20,
})
baseUserCount.value = totalRows
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
watch(
() => route.value.query?.page,
(newVal, oldVal) => {
@ -57,6 +80,16 @@ watch(projectPageTab, () => {
}
})
watch(
() => route.value.params.baseId,
(newVal, oldVal) => {
if (newVal && oldVal === undefined) {
updateBaseUserCount()
}
},
{ immediate: true },
)
watch(
() => openedProject.value?.title,
() => {
@ -75,7 +108,7 @@ watch(
<GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2.5">
<GeneralProjectIcon :type="openedProject?.type" />
<NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150">
<NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150" show-on-truncate-only>
<template #title> {{ openedProject?.title }}</template>
<span class="truncate">
{{ openedProject?.title }}
@ -119,14 +152,14 @@ watch(
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
<div>{{ $t('labels.members') }}</div>
<div
v-if="workspaceUserCount"
v-if="userCount"
class="tab-info"
:class="{
'bg-primary-selected': projectPageTab === 'data-source',
'bg-gray-50': projectPageTab !== 'data-source',
'bg-primary-selected': projectPageTab === 'collaborator',
'bg-gray-50': projectPageTab !== 'collaborator',
}"
>
{{ workspaceUserCount }}
{{ userCount }}
</div>
</div>
</template>

2
packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue

@ -73,7 +73,7 @@ vModel.value.au = !!vModel.value.au */
</div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt">
<a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type" @change="onDataTypeChange">
<a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type " @change="onDataTypeChange">
<a-select-option v-for="type in dataTypes" :key="type" :value="type">
{{ type }}
</a-select-option>

4
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -223,7 +223,7 @@ if (props.fromTableExplorer) {
'!w-146': isTextArea(formState) && formState.meta.richMode,
'!w-[600px]': formState.uidt === UITypes.Formula && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee,
'shadow-lg border-1 border-gray-100 shadow-gray-300 rounded-xl p-6': !embedMode,
'shadow-lg border-1 border-gray-200 shadow-gray-300 rounded-xl p-6': !embedMode,
}"
@keydown="handleEscape"
@click.stop
@ -271,7 +271,7 @@ if (props.fromTableExplorer) {
show-search
class="nc-column-type-input !rounded"
:disabled="isKanban || readOnly"
dropdown-class-name="nc-dropdown-column-type "
dropdown-class-name="nc-dropdown-column-type border-1 border-gray-200"
@change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated"
>

2
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -82,7 +82,7 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links)
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="table" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate">
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ table.title }}</template>
<span>{{ table.title }}</span>
</NcTooltip>

2
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -339,7 +339,7 @@ const loadListData = async ($state: any) => {
<a-dropdown
v-model:visible="colorMenus[index]"
:trigger="['click']"
overlay-class-name="nc-dropdown-select-color-options"
overlay-class-name="nc-dropdown-select-color-options rounded-md overflow-hidden border-1 border-gray-200 "
>
<template #overlay>
<LazyGeneralColorPicker

2
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -819,6 +819,7 @@ const onFieldOptionUpdate = () => {
'text-brand-500': compareCols(field, activeField),
}"
class="truncate flex-1"
show-on-truncate-only
>
<template #title> {{ fieldState(field)?.title || field.title }} </template>
<span>
@ -978,6 +979,7 @@ const onFieldOptionUpdate = () => {
:class="{
'text-brand-500': compareCols(displayColumn, activeField),
}"
show-on-truncate-only
>
<template #title> {{ fieldState(displayColumn)?.title || displayColumn.title }} </template>
<span>

4
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -1804,7 +1804,7 @@ onKeyStroke('ArrowDown', onDown)
<template #overlay>
<div class="relative overflow-visible min-h-17 w-10">
<div
class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg justify-start overflow-hidden"
class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg border-1 border-gray-200 justify-start overflow-hidden"
style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)"
:class="{
'-left-44': !isAddNewRecordGridMode,
@ -1847,7 +1847,7 @@ onKeyStroke('ArrowDown', onDown)
</div>
</template>
<template #icon>
<component :is="iconMap.arrowUp" class="text-gray-600 h-4" />
<component :is="iconMap.arrowUp" class="text-gray-600 h-4 w-4" />
</template>
</a-dropdown-button>
</div>

5
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -99,12 +99,13 @@ const onClick = (e: Event) => {
}"
class="name pl-1"
placement="bottom"
show-on-truncate-only
>
<template #title> {{ column.title }} </template>
<div :class="{ truncate: !isForm }" :data-test-id="column.title">
<span :data-test-id="column.title">
{{ column.title }}
</div>
</span>
</NcTooltip>
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>

2
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -291,7 +291,7 @@ const onInsertAfter = () => {
<GeneralIcon icon="arrowDown" class="text-grey h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0 mr-2" />
</div>
<template #overlay>
<a-menu class="shadow bg-white nc-column-options">
<a-menu class="shadow bg-white border-1 border-gray-200 nc-column-options">
<a-menu-item @click="onEditPress">
<div class="nc-column-edit nc-header-menu-item">
<component :is="iconMap.edit" class="text-gray-700 mx-0.65 my-0.75" />

2
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -154,7 +154,7 @@ const openDropDown = (e: Event) => {
>
<LazySmartsheetHeaderVirtualCellIcon v-if="column && !props.hideIcon" />
<NcTooltip placement="bottom" class="truncate name pl-1">
<NcTooltip placement="bottom" class="truncate name pl-1" show-on-truncate-only>
<template #title>
{{ tooltipMsg }}
</template>

6
packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue

@ -104,11 +104,11 @@ const onArrowUp = () => {
@click="onClick(option)"
>
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate">
<NcTooltip class="truncate" show-on-truncate-only>
<template #title> {{ option.title }}</template>
<span>
<template #default>
{{ option.title }}
</span>
</template>
</NcTooltip>
</div>
</div>

1
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -86,6 +86,7 @@ if (!localValue.value && allowEmpty !== true) {
<NcTooltip
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
class="max-w-[15rem] truncate select-none"
show-on-truncate-only
>
<template #title> {{ option.label }}</template>
<span>

8
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -380,11 +380,11 @@ useMenuCloseOnEsc(open)
"
>
<component :is="getIcon(metaColumnById[field.fk_column_id])" />
<NcTooltip :disabled="field.title.length < 30" class="flex-1 px-1 truncate">
<NcTooltip show-on-truncate-only class="flex-1 px-1 truncate">
<template #title>
{{ field.title }}
</template>
<span>{{ field.title }}</span>
<template #default>{{ field.title }}</template>
</NcTooltip>
<NcSwitch v-e="['a:fields:show-hide']" :checked="field.show" :disabled="field.isViewEssentialField" />
@ -406,9 +406,9 @@ useMenuCloseOnEsc(open)
@click.stop
>
<component :is="getIcon(metaColumnById[filteredFieldList[0].fk_column_id as string])" />
<NcTooltip :disabled="filteredFieldList?.[0]?.title?.length < 30" class="px-1 flex-1 truncate">
<NcTooltip show-on-truncate-only class="px-1 flex-1 truncate">
<template #title>{{ filteredFieldList[0].title }}</template>
<span>{{ filteredFieldList[0].title }}</span>
<template #default>{{ filteredFieldList[0].title }}</template>
</NcTooltip>
<NcSwitch v-e="['a:fields:show-hide']" :checked="true" :disabled="true" />

2
packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue

@ -182,7 +182,7 @@ function openDeleteDialog() {
'text-gray-800 font-medium': !activeView?.is_default,
}"
>
<NcTooltip class="truncate xs:pl-1.25 flex-1 text-inherit">
<NcTooltip class="truncate xs:pl-1.25 flex-1 text-inherit" show-on-truncate-only>
<template #title>{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} </template>
<span
:class="{

2
packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue

@ -65,7 +65,7 @@ useMenuCloseOnEsc(open)
</div>
<template #overlay>
<div
class="w-full bg-white shadow-lg menu-filter-dropdown border-1 border-gray-50 rounded-md overflow-hidden"
class="w-full bg-white shadow-lg menu-filter-dropdown border-1 border-gray-200 rounded-md overflow-hidden"
data-testid="nc-height-menu"
>
<div class="flex flex-col w-full text-sm" @click.stop>

4
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -114,9 +114,9 @@ watch(columns, () => {
<a-select-option v-for="op of columns" :key="op.value" v-e="['c:search:field:select']" :value="op.value">
<div class="text-[0.75rem] flex items-center -ml-1 gap-2">
<SmartsheetHeaderIcon class="text-sm" :column="op.column" />
<NcTooltip class="truncate" placement="top">
<NcTooltip class="truncate" placement="top" show-on-truncate-only>
<template #title>{{ op.label }}</template>
<span>{{ op.label }}</span>
<template #default>{{ op.label }}</template>
</NcTooltip>
</div>
</a-select-option>

5
packages/nc-gui/components/template/Editor.vue

@ -131,10 +131,7 @@ const validators = computed(() =>
hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.title`] = [
fieldRequiredValidator(),
fieldLengthValidator(),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.title`] = [fieldRequiredValidator(), fieldLengthValidator()]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true

1
packages/nc-gui/composables/useAttachment.ts

@ -6,6 +6,7 @@ const useAttachment = () => {
const getPossibleAttachmentSrc = (item: Record<string, any>) => {
const res: string[] = []
if (item?.data) res.push(item.data)
if (item?.file) res.push(window.URL.createObjectURL(item.file))
if (item?.signedPath) res.push(`${appInfo.value.ncSiteUrl}/${item.signedPath}`)
if (item?.signedUrl) res.push(item.signedUrl)
if (item?.path) res.push(`${appInfo.value.ncSiteUrl}/${item.path}`)

8
packages/nc-gui/composables/useColumnCreateStore.ts

@ -40,8 +40,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { sqlUis } = storeToRefs(baseStore)
const { bases } = storeToRefs(useBases())
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
@ -66,12 +64,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]),
)
const source = computed(() =>
meta.value && meta.value.source_id && meta.value.base_id
? bases.value.get(meta.value?.base_id as string)?.sources?.find((s) => s.id === meta.value!.source_id)
: undefined,
)
const idType = null
const additionalValidations = ref<ValidationsObj>({})

4
packages/nc-gui/composables/useViewColumns.ts

@ -275,9 +275,9 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
}
// reload view columns when active view changes
// or when columns count changes(delete/add)
// or when columns changes(delete/add)
watch(
[() => view?.value?.id, () => meta.value?.columns?.length],
[() => view?.value?.id, () => meta.value?.columns],
async ([newViewId]) => {
// reload only if view belongs to current table
if (newViewId && view.value?.fk_model_id === meta.value?.id) {

2
packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts

@ -58,7 +58,7 @@ export default class CSVTemplateAdapter {
this.tables[tableIdx] = []
for (const [columnIdx, columnName] of columnNames.entries()) {
let title = ((columnNameRowExist && columnName.toString().trim()) || `Field ${columnIdx + 1}`).trim()
const title = ((columnNameRowExist && columnName.toString().trim()) || `Field ${columnIdx + 1}`).trim()
let cn: string = ((columnNameRowExist && columnName.toString().trim()) || `field_${columnIdx + 1}`)
.replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_')
.trim()

4
packages/nc-gui/lang/en.json

@ -666,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -719,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Disable shared base",
"enable": "Anyone with the link",
"link": "Shared base link"

44
packages/nc-gui/lang/ja.json

@ -4,29 +4,29 @@
"connect_data_sources": "データソースに接続",
"alert": "アラート",
"alert-message": "データベースが接続されていません。DBベースを接続してインターフェースを構成してください。スキップして後でベースのホームページを追加することもできます。",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "このインターフェースに接続するDBベースを選択します。",
"create_interface": "Create interface",
"project_name": "Base Name",
"connect": "Connect",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "このインターフェースに接続するプロジェクトを選択します。",
"create_interface": "インターフェースを作成",
"project_name": "プロジェクト名",
"connect": "接続",
"buttonActionTypes": {
"open_external_url": "Open external link",
"delete_record": "Delete record",
"update_record": "Update record",
"open_layout": "Open layout"
"open_external_url": "外部リンクを開く",
"delete_record": "レコードを削除",
"update_record": "レコードを更新",
"open_layout": "レイアウトを開く"
},
"widgets": {
"static_text": "Text",
"chart": "Chart",
"table": "Table",
"static_text": "テキスト",
"chart": "チャート",
"table": "",
"image": "Image",
"map": "Map",
"button": "Button",
"number": "Number",
"bar_chart": "Bar Chart",
"line_chart": "Line Chart",
"area_chart": "Area Chart",
"pie_chart": "Pie Chart",
"donut_chart": "Donut Chart",
"map": "マップ",
"button": "ボタン",
"number": "ナンバー",
"bar_chart": "棒グラフ",
"line_chart": "折れ線グラフ",
"area_chart": "面グラフ",
"pie_chart": "円グラフ",
"donut_chart": "ドーナツグラフ",
"scatter_plot": "Scatter Plot",
"bubble_chart": "Bubble Chart",
"radar_chart": "Radar Chart",
@ -39,7 +39,7 @@
}
},
"general": {
"quit": "Quit",
"quit": "終了",
"home": "ホーム",
"load": "読み込み",
"open": "開く",
@ -47,7 +47,7 @@
"yes": "はい",
"no": "いいえ",
"ok": "OK",
"back": "Back",
"back": "戻る",
"and": "と",
"or": "または",
"add": "追加",
@ -78,7 +78,7 @@
"quote": "Quote",
"submit": "送信",
"create": "作成",
"createEntity": "Create {entity}",
"createEntity": "{entity}を作成",
"creating": "Creating",
"creatingEntity": "Creating {entity}",
"details": "Details",

2
packages/nc-gui/store/bases.ts

@ -12,6 +12,7 @@ export const useBases = defineStore('basesStore', () => {
const bases = ref<Map<string, NcProject>>(new Map())
const basesList = computed<NcProject[]>(() => Array.from(bases.value.values()).sort((a, b) => a.updated_at - b.updated_at))
const baseUserCount = ref<number | undefined>(undefined)
const router = useRouter()
const route = router.currentRoute
@ -294,6 +295,7 @@ export const useBases = defineStore('basesStore', () => {
return {
bases,
basesList,
baseUserCount,
loadProjects,
loadProject,
getSqlUi,

6
packages/nocodb/src/controllers/attachments-secure.controller.ts

@ -75,7 +75,11 @@ export class AttachmentsSecureController {
path: path.join('nc', 'uploads', fpath),
});
res.sendFile(file.path);
if (this.attachmentsService.previewAvailable(file.type)) {
res.sendFile(file.path);
} else {
res.download(file.path);
}
} catch (e) {
res.status(404).send('Not found');
}

18
packages/nocodb/src/controllers/attachments.controller.ts

@ -68,7 +68,11 @@ export class AttachmentsController {
path: path.join('nc', 'uploads', filename),
});
res.sendFile(file.path);
if (this.attachmentsService.previewAvailable(file.type)) {
res.sendFile(file.path);
} else {
res.download(file.path);
}
} catch (e) {
res.status(404).send('Not found');
}
@ -94,7 +98,11 @@ export class AttachmentsController {
),
});
res.sendFile(file.path);
if (this.attachmentsService.previewAvailable(file.type)) {
res.sendFile(file.path);
} else {
res.download(file.path);
}
} catch (e) {
res.status(404).send('Not found');
}
@ -109,7 +117,11 @@ export class AttachmentsController {
path: path.join('nc', 'uploads', fpath),
});
res.sendFile(file.path);
if (this.attachmentsService.previewAvailable(file.type)) {
res.sendFile(file.path);
} else {
res.download(file.path);
}
} catch (e) {
res.status(404).send('Not found');
}

10
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -1208,10 +1208,8 @@ class BaseModelSqlv2 {
!this.isSqlite,
);
let children = await this.execAndParse(finalQb, childTable);
if (this.isMySQL) {
children = children[0];
}
const children = await this.execAndParse(finalQb, childTable);
const proto = await (
await Model.getBaseModelSQL({
id: rtnId,
@ -3841,7 +3839,7 @@ class BaseModelSqlv2 {
: [columnValue];
for (let j = 0; j < columnValueArr.length; ++j) {
const val = columnValueArr[j];
if (!options.includes(val)) {
if (!options.includes(val) && !options.includes(`'${val}'`)) {
NcError.badRequest(
`Invalid option "${val}" provided for column "${columnTitle}". Valid options are "${options.join(
', ',
@ -4408,7 +4406,7 @@ class BaseModelSqlv2 {
let data =
this.isPg || this.isSnowflake
? (await this.dbDriver.raw(query))?.rows
: query.slice(0, 6) === 'select' && !this.isMssql
: /^(\(|)select/.test(query) && !this.isMssql
? await this.dbDriver.from(
this.dbDriver.raw(query).wrap('(', ') __nc_alias'),
)

3
packages/nocodb/src/db/sql-client/lib/mysql/TidbClient.ts

@ -24,7 +24,8 @@ class Tidb extends MysqlClient {
try {
const response = await this.sqlClient.raw(
`select *, TABLE_NAME as tn from INFORMATION_SCHEMA.KEY_COLUMN_USAGE where CONSTRAINT_SCHEMA='${this.connectionConfig.connection.database}' and TABLE_NAME='${args.tn}'`,
`select *, TABLE_NAME as tn from INFORMATION_SCHEMA.KEY_COLUMN_USAGE where CONSTRAINT_SCHEMA=? and TABLE_NAME=?`,
[this.connectionConfig.connection.database, args.tn],
);
if (response.length === 2) {

6
packages/nocodb/src/db/sql-client/lib/mysql/VitessClient.ts

@ -131,7 +131,8 @@ class Vitess extends MysqlClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`select *, table_name as tn from information_schema.columns where table_name = '${args.tn}' ORDER by ordinal_position`,
`select *, table_name as tn from information_schema.columns where table_name = ? ORDER by ordinal_position`,
[args.tn],
);
if (response.length === 2) {
@ -209,7 +210,8 @@ class Vitess extends MysqlClient {
try {
const response = await this.sqlClient.raw(
`select *, TABLE_NAME as tn from INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = '${args.tn}' ORDER by ordinal_position;`,
`select *, TABLE_NAME as tn from INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = ? ORDER by ordinal_position;`,
[args.tn],
);
if (response.length === 2) {

4
packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts

@ -891,7 +891,9 @@ class PGClient extends KnexClient {
// column['unique'] = response.rows[i]['cst'].indexOf('UNIQUE') === -1 ? false : true;
column.cdf = response.rows[i].cdf
? response.rows[i].cdf.replace(/::[\w ]+$/, '').replace(/^'|'$/g, '')
? response.rows[i].cdf
.replace(/::[\w (),]+$/, '')
.replace(/^'|'$/g, '')
: response.rows[i].cdf;
// todo : need to find column comment

15
packages/nocodb/src/db/sql-client/lib/pg/YugabyteClient.ts

@ -68,7 +68,8 @@ class YBClient extends PGClient {
try {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.raw(`
const response = await this.raw(
`
select c.relname as tn, a.attname as cn, pg_catalog.format_type(a.atttypid, a.atttypmod) as "dt",a.attnotnull as "not_nullable",
pg_catalog.pg_get_expr(ad.adbin, ad.adrelid, true) as cdf, dsc.description as comment,a.attnum as cop,
coalesce(i.indisprimary,false) as pk,
@ -77,8 +78,12 @@ class YBClient extends PGClient {
INNER JOIN pg_catalog.pg_class c ON (a.attrelid=c.oid)
LEFT OUTER JOIN pg_catalog.pg_attrdef ad ON (a.attrelid=ad.adrelid AND a.attnum = ad.adnum)
LEFT OUTER JOIN pg_catalog.pg_description dsc ON (c.oid=dsc.objoid AND a.attnum = dsc.objsubid)
LEFT JOIN pg_index i ON (a.attnum = any(i.indkey) and a.attrelid = i.indrelid and i.indrelid = '${args.tn}'::regclass AND i.indisprimary)
WHERE NOT a.attisdropped AND c.relname = '${args.tn}' and a.attnum > 0 ORDER BY a.attnum`);
LEFT JOIN pg_index i ON (a.attnum = any(i.indkey) and a.attrelid = i.indrelid and i.indrelid = :table::regclass AND i.indisprimary)
WHERE NOT a.attisdropped AND c.relname = :table and a.attnum > 0 ORDER BY a.attnum`,
{
table: args.tn,
},
);
const columns = [];
@ -114,7 +119,9 @@ class YBClient extends PGClient {
// column['unique'] = response.rows[i]['cst'].indexOf('UNIQUE') === -1 ? false : true;
column.cdf = response.rows[i].cdf
? response.rows[i].cdf.split('::')[0].replace(/'/g, '')
? response.rows[i].cdf
.replace(/::[\w (),]+$/, '')
.replace(/^'|'$/g, '')
: response.rows[i].cdf;
// todo : need to find column comment

16
packages/nocodb/src/helpers/apiHelpers.ts

@ -16,13 +16,11 @@ ajv.addSchema(swagger, 'swagger.json');
addFormats(ajv);
// A middleware generator to validate the request body
export const getAjvValidatorMw = (schema) => {
export const getAjvValidatorMw = (schema: string) => {
return (req: Request, res: Response, next: NextFunction) => {
const validate = ajv.getSchema(schema);
// Validate the request body against the schema
const valid = ajv.validate(
typeof schema === 'string' ? { $ref: schema } : schema,
req.body,
);
const valid = validate(req.body);
// If the request body is valid, call the next middleware
if (valid) {
@ -40,12 +38,10 @@ export const getAjvValidatorMw = (schema) => {
};
// a function to validate the payload against the schema
export const validatePayload = (schema, payload) => {
export const validatePayload = (schema: string, payload: any) => {
const validate = ajv.getSchema(schema);
// Validate the request body against the schema
const valid = ajv.validate(
typeof schema === 'string' ? { $ref: schema } : schema,
payload,
);
const valid = validate(payload);
// If the request body is not valid, throw error
if (!valid) {

8
packages/nocodb/src/services/attachments.service.ts

@ -161,6 +161,14 @@ export class AttachmentsService {
return { path: filePath, type };
}
previewAvailable(mimetype: string) {
const available = ['image', 'pdf', 'text/plain'];
if (available.some((type) => mimetype.includes(type))) {
return true;
}
return false;
}
sanitizeUrlPath(paths) {
return paths.map((url) => url.replace(/[/.?#]+/g, '_'));
}

4
tests/playwright/pages/Dashboard/Grid/index.ts

@ -129,7 +129,7 @@ export class GridPage extends BasePage {
await this._fillRow({ index, columnHeader, value: rowValue });
const clickOnColumnHeaderToSave = () =>
this.get().locator(`[data-title="${columnHeader}"]`).locator(`div[data-test-id="${columnHeader}"]`).click();
this.get().locator(`[data-title="${columnHeader}"]`).locator(`span[data-test-id="${columnHeader}"]`).click();
if (networkValidation) {
await this.waitForResponse({
@ -164,7 +164,7 @@ export class GridPage extends BasePage {
await this._fillRow({ index, columnHeader, value });
const clickOnColumnHeaderToSave = () =>
this.get().locator(`[data-title="${columnHeader}"]`).locator(`div[data-test-id="${columnHeader}"]`).click();
this.get().locator(`[data-title="${columnHeader}"]`).locator(`span[data-test-id="${columnHeader}"]`).click();
if (networkValidation) {
await this.waitForResponse({

Loading…
Cancel
Save