Browse Source

Merge branch 'develop' into fix/deprecated-dependencies

pull/6362/head
Wing-Kam Wong 1 year ago
parent
commit
44842bdba7
  1. 4
      packages/nc-gui/assets/style.scss
  2. 34
      packages/nc-gui/components/cell/Checkbox.vue
  3. 9
      packages/nc-gui/components/cell/Currency.vue
  4. 17
      packages/nc-gui/components/cell/DatePicker.vue
  5. 17
      packages/nc-gui/components/cell/DateTimePicker.vue
  6. 9
      packages/nc-gui/components/cell/Decimal.vue
  7. 11
      packages/nc-gui/components/cell/Duration.vue
  8. 18
      packages/nc-gui/components/cell/Email.vue
  9. 9
      packages/nc-gui/components/cell/Float.vue
  10. 9
      packages/nc-gui/components/cell/Integer.vue
  11. 4
      packages/nc-gui/components/cell/MultiSelect.vue
  12. 9
      packages/nc-gui/components/cell/Percent.vue
  13. 83
      packages/nc-gui/components/cell/PhoneNumber.vue
  14. 4
      packages/nc-gui/components/cell/SingleSelect.vue
  15. 12
      packages/nc-gui/components/cell/Text.vue
  16. 20
      packages/nc-gui/components/cell/TextArea.vue
  17. 27
      packages/nc-gui/components/cell/TimePicker.vue
  18. 8
      packages/nc-gui/components/cell/Url.vue
  19. 33
      packages/nc-gui/components/cell/YearPicker.vue
  20. 4
      packages/nc-gui/components/general/ColorPicker.vue
  21. 33
      packages/nc-gui/components/smartsheet/Cell.vue
  22. 25
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  23. 63
      packages/nc-gui/components/smartsheet/column/DefaultValue.vue
  24. 52
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  25. 15
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  26. 1
      packages/nc-gui/composables/useData.ts
  27. 1
      packages/nc-gui/context/index.ts
  28. 2
      packages/noco-docs/docs/050.engineering/040.unit-testing.md
  29. 13
      packages/nocodb-sdk/src/lib/Api.ts
  30. 5
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  31. 6
      packages/nocodb/src/db/sql-client/lib/pg/PgClient.ts
  32. 25
      packages/nocodb/src/schema/swagger.json

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

@ -534,3 +534,7 @@ a {
.nc-toolbar-dropdown { .nc-toolbar-dropdown {
@apply !rounded-2xl; @apply !rounded-2xl;
} }
input[type='number'] {
@apply !outline-none !ring-0 !border-0;
}

34
packages/nc-gui/components/cell/Checkbox.vue

@ -2,6 +2,7 @@
import { import {
ActiveCellInj, ActiveCellInj,
ColumnInj, ColumnInj,
EditColumnInj,
IsFormInj, IsFormInj,
ReadonlyInj, ReadonlyInj,
getMdiIcon, getMdiIcon,
@ -33,6 +34,8 @@ const column = inject(ColumnInj)
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const isEditColumnMenu = inject(EditColumnInj, ref(false))
const readOnly = inject(ReadonlyInj) const readOnly = inject(ReadonlyInj)
const checkboxMeta = computed(() => { const checkboxMeta = computed(() => {
@ -47,7 +50,7 @@ const checkboxMeta = computed(() => {
}) })
const vModel = computed<boolean | number>({ const vModel = computed<boolean | number>({
get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0, get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0 && props.modelValue !== 'false',
set: (val: any) => emits('update:modelValue', isMssql(column?.value?.base_id) ? +val : val), set: (val: any) => emits('update:modelValue', isMssql(column?.value?.base_id) ? +val : val),
}) })
@ -75,7 +78,7 @@ useSelectedCellKeyupListener(active, (e) => {
<template> <template>
<div <div
class="flex cursor-pointer w-full h-full" class="flex cursor-pointer w-full h-full py-1"
:class="{ :class="{
'justify-center': !isForm, 'justify-center': !isForm,
'w-full': isForm, 'w-full': isForm,
@ -84,17 +87,22 @@ useSelectedCellKeyupListener(active, (e) => {
}" }"
@click="onClick(false, $event)" @click="onClick(false, $event)"
> >
<div class="p-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel, '!ml-[-8px]': readOnly }"> <div
<Transition name="layout" mode="out-in" :duration="100"> class="items-center"
<component :class="{ '!ml-[-8px]': readOnly, 'w-full justify-start': isEditColumnMenu }"
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)" @click="onClick(true)"
class="nc-checkbox" >
:style="{ <div class="p-1" :class="{ 'bg-gray-100 rounded-full ': !vModel }">
color: checkboxMeta.color, <Transition name="layout" mode="out-in" :duration="100">
}" <component
@click="onClick(true)" :is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
/> class="nc-checkbox"
</Transition> :style="{
color: checkboxMeta.color,
}"
/>
</Transition>
</div>
</div> </div>
</div> </div>
</template> </template>

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { ColumnInj, EditModeInj, IsExpandedFormOpenInj, computed, inject, parseProp, useVModel } from '#imports' import { ColumnInj, EditColumnInj, EditModeInj, IsExpandedFormOpenInj, computed, inject, parseProp, useVModel } from '#imports'
interface Props { interface Props {
modelValue: number | null | undefined modelValue: number | null | undefined
@ -16,6 +16,8 @@ const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)! const editEnabled = inject(EditModeInj)!
const isEditColumn = inject(EditColumnInj, ref(false))
const _vModel = useVModel(props, 'modelValue', emit) const _vModel = useVModel(props, 'modelValue', emit)
const vModel = computed({ const vModel = computed({
@ -55,7 +57,7 @@ const currency = computed(() => {
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const submitCurrency = () => { const submitCurrency = () => {
if (lastSaved.value !== vModel.value) { if (lastSaved.value !== vModel.value) {
@ -76,7 +78,8 @@ onMounted(() => {
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
type="number" type="number"
class="w-full h-full border-none outline-none px-2" class="w-full h-full text-sm border-none rounded-md outline-none"
:placeholder="isEditColumn ? '(Optional)' : ''"
@blur="submitCurrency" @blur="submitCurrency"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

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

@ -4,6 +4,7 @@ import {
ActiveCellInj, ActiveCellInj,
CellClickHookInj, CellClickHookInj,
ColumnInj, ColumnInj,
EditColumnInj,
EditModeInj, EditModeInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
@ -32,6 +33,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isLockedMode = inject(IsLockedInj, ref(false)) const isLockedMode = inject(IsLockedInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false)) const editable = inject(EditModeInj, ref(false))
@ -79,7 +82,17 @@ watch(
{ flush: 'post' }, { flush: 'post' },
) )
const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid.value ? 'Invalid date' : '')) const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
} else if (modelValue === null && showNull.value) {
return 'NULL'
} else if (isDateInvalid.value) {
return 'Invalid date'
} else {
return ''
}
})
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
@ -201,7 +214,7 @@ const clickHandler = () => {
<a-date-picker <a-date-picker
v-model:value="localState" v-model:value="localState"
:bordered="false" :bordered="false"
class="!w-full !px-0 !border-none" class="!w-full !px-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }" :class="{ 'nc-null': modelValue === null && showNull }"
:format="dateFormat" :format="dateFormat"
:placeholder="placeholder" :placeholder="placeholder"

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

@ -5,6 +5,7 @@ import {
ActiveCellInj, ActiveCellInj,
CellClickHookInj, CellClickHookInj,
ColumnInj, ColumnInj,
EditColumnInj,
ReadonlyInj, ReadonlyInj,
dateFormats, dateFormats,
inject, inject,
@ -39,6 +40,8 @@ const editable = inject(EditModeInj, ref(false))
const isLockedMode = inject(IsLockedInj, ref(false)) const isLockedMode = inject(IsLockedInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const isDateInvalid = ref(false) const isDateInvalid = ref(false)
@ -132,7 +135,17 @@ watch(
{ flush: 'post' }, { flush: 'post' },
) )
const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isDateInvalid.value ? 'Invalid date' : '')) const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
} else if (modelValue === null && showNull.value) {
return 'NULL'
} else if (isDateInvalid.value) {
return 'Invalid date'
} else {
return ''
}
})
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
@ -249,7 +262,7 @@ const isColDisabled = computed(() => {
:disabled="isColDisabled" :disabled="isColDisabled"
:show-time="true" :show-time="true"
:bordered="false" :bordered="false"
class="!w-full !px-0 !border-none" class="!w-full !px-0 !py-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }" :class="{ 'nc-null': modelValue === null && showNull }"
:format="dateTimeFormat" :format="dateTimeFormat"
:placeholder="placeholder" :placeholder="placeholder"

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports' import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
interface Props { interface Props {
// when we set a number, then it is number type // when we set a number, then it is number type
@ -23,6 +23,8 @@ const editEnabled = inject(EditModeInj)
const column = inject(ColumnInj, null)! const column = inject(ColumnInj, null)!
const isEditColumn = inject(EditColumnInj, ref(false))
const domRef = ref<HTMLElement>() const domRef = ref<HTMLElement>()
const meta = computed(() => { const meta = computed(() => {
@ -78,7 +80,7 @@ const onKeyDown = (e: any) => {
} }
} }
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
watch(isExpandedFormOpen, () => { watch(isExpandedFormOpen, () => {
if (!isExpandedFormOpen.value) { if (!isExpandedFormOpen.value) {
@ -92,9 +94,10 @@ watch(isExpandedFormOpen, () => {
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none !p-0 border-none w-full h-full text-sm" class="outline-none !py-2 !px-1 border-none rounded-md w-full h-full !text-sm"
type="number" type="number"
:step="precision" :step="precision"
:placeholder="isEditColumn ? '(Optional)' : ''"
style="letter-spacing: 0.06rem" style="letter-spacing: 0.06rem"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop="onKeyDown" @keydown.down.stop="onKeyDown"

11
packages/nc-gui/components/cell/Duration.vue

@ -2,6 +2,7 @@
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { import {
ColumnInj, ColumnInj,
EditColumnInj,
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
computed, computed,
@ -34,9 +35,11 @@ const durationInMS = ref(0)
const isEdited = ref(false) const isEdited = ref(false)
const isEditColumn = inject(EditColumnInj, ref(false))
const durationType = computed(() => parseProp(column?.value?.meta)?.duration || 0) const durationType = computed(() => parseProp(column?.value?.meta)?.duration || 0)
const durationPlaceholder = computed(() => durationOptions[durationType.value].title) const durationPlaceholder = computed(() => (isEditColumn.value ? '(Optional)' : durationOptions[durationType.value].title))
const localState = computed({ const localState = computed({
get: () => convertMS2Duration(modelValue, durationType.value), get: () => convertMS2Duration(modelValue, durationType.value),
@ -76,7 +79,7 @@ const submitDuration = () => {
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>
@ -85,8 +88,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="localState" v-model="localState"
class="w-full !border-none p-0" class="w-full !border-none !outline-none p-0"
:class="{ '!px-2': editEnabled }" :class="{ '!px-2 !py-1': editEnabled }"
:placeholder="durationPlaceholder" :placeholder="durationPlaceholder"
@blur="submitDuration" @blur="submitDuration"
@keypress="checkDurationFormat($event)" @keypress="checkDurationFormat($event)"

18
packages/nc-gui/components/cell/Email.vue

@ -1,6 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, IsSurveyFormInj, computed, inject, useI18n, validateEmail } from '#imports' import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsSurveyFormInj,
computed,
inject,
useI18n,
validateEmail,
} from '#imports'
interface Props { interface Props {
modelValue: string | null | undefined modelValue: string | null | undefined
@ -25,6 +34,8 @@ const localState = ref(value)
const isSurveyForm = inject(IsSurveyFormInj, ref(false)) const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const vModel = computed({ const vModel = computed({
get: () => value, get: () => value,
set: (val) => { set: (val) => {
@ -39,7 +50,7 @@ const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
watch( watch(
() => editEnabled.value, () => editEnabled.value,
@ -59,7 +70,8 @@ watch(
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="w-full outline-none text-sm px-2" class="w-full outline-none text-sm px-1 py-2"
:placeholder="isEditColumn ? '(Optional)' : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports' import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
interface Props { interface Props {
// when we set a number, then it is number type // when we set a number, then it is number type
@ -21,6 +21,8 @@ const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
const isEditColumn = inject(EditColumnInj, ref(false))
const _vModel = useVModel(props, 'modelValue', emits) const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({ const vModel = computed({
@ -38,7 +40,7 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>
@ -46,9 +48,10 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none p-0 border-none w-full h-full text-sm" class="outline-none px-1 border-none w-full h-full text-sm"
type="number" type="number"
step="0.1" step="0.1"
:placeholder="isEditColumn ? '(Optional)' : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports' import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
interface Props { interface Props {
// when we set a number, then it is number type // when we set a number, then it is number type
@ -21,6 +21,8 @@ const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
const isEditColumn = inject(EditColumnInj, ref(false))
const _vModel = useVModel(props, 'modelValue', emits) const _vModel = useVModel(props, 'modelValue', emits)
const displayValue = computed(() => { const displayValue = computed(() => {
@ -46,7 +48,7 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
function onKeyDown(e: any) { function onKeyDown(e: any) {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
@ -83,9 +85,10 @@ function onKeyDown(e: any) {
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none p-0 border-none w-full h-full text-sm" class="outline-none py-2 px-1 border-none w-full h-full text-sm"
type="number" type="number"
style="letter-spacing: 0.06rem" style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? '(Optional)' : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown="onKeyDown" @keydown="onKeyDown"
@keydown.down.stop @keydown.down.stop

4
packages/nc-gui/components/cell/MultiSelect.vue

@ -9,6 +9,7 @@ import {
ActiveCellInj, ActiveCellInj,
CellClickHookInj, CellClickHookInj,
ColumnInj, ColumnInj,
EditColumnInj,
EditModeInj, EditModeInj,
IsKanbanInj, IsKanbanInj,
ReadonlyInj, ReadonlyInj,
@ -61,6 +62,8 @@ const isPublic = inject(IsPublicInj, ref(false))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined)) const rowHeight = inject(RowHeightInj, ref(undefined))
const selectedIds = ref<string[]>([]) const selectedIds = ref<string[]>([])
@ -384,6 +387,7 @@ const selectedOpts = computed(() => {
v-model:value="vModel" v-model:value="vModel"
mode="multiple" mode="multiple"
class="w-full overflow-hidden" class="w-full overflow-hidden"
:placeholder="isEditColumn ? '(Optional)' : ''"
:bordered="false" :bordered="false"
clear-icon clear-icon
show-search show-search

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports' import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, inject, useVModel } from '#imports'
interface Props { interface Props {
modelValue?: number | string | null modelValue?: number | string | null
@ -14,6 +14,8 @@ const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
const isEditColumn = inject(EditColumnInj, ref(false))
const _vModel = useVModel(props, 'modelValue', emits) const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({ const vModel = computed({
@ -29,7 +31,7 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>
@ -37,9 +39,10 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="w-full !border-none text-base" class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base p-1"
:class="{ '!px-2': editEnabled }" :class="{ '!px-2': editEnabled }"
type="number" type="number"
:placeholder="isEditColumn ? '(Optional)' : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

83
packages/nc-gui/components/cell/PhoneNumber.vue

@ -1,21 +1,84 @@
<script setup lang="ts"> <script lang="ts" setup>
import { useVModel } from '#imports' import type { VNodeRef } from '@vue/runtime-core'
import isMobilePhone from 'validator/lib/isMobilePhone'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsSurveyFormInj, computed, inject } from '#imports'
interface Props { interface Props {
modelValue: any modelValue: string | null | number | undefined
} }
interface Emits { const { modelValue: value } = defineProps<Props>()
(event: 'update:modelValue', model: string): void
} const emit = defineEmits(['update:modelValue'])
const rowHeight = inject(RowHeightInj, ref(undefined))
const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj)!
const isEditColumn = inject(EditColumnInj, ref(false))
const column = inject(ColumnInj)!
const props = defineProps<Props>() // Used in the logic of when to display error since we are not storing the phone if it's not valid
const localState = ref(value)
const emits = defineEmits<Emits>() const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) const vModel = computed({
get: () => value,
set: (val) => {
localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && isMobilePhone(val)) || !val || isSurveyForm.value) {
emit('update:modelValue', val)
}
},
})
const validEmail = computed(() => vModel.value && isMobilePhone(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
() => {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !isMobilePhone(localState.value)) {
message.error('Invalid Phone Number')
localState.value = undefined
return
}
localState.value = value
},
)
</script> </script>
<template> <template>
<LazyCellText v-model="vModel" /> <input
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="w-full outline-none text-sm px-1 py-2"
:placeholder="isEditColumn ? '(Optional)' : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<a v-else-if="validEmail" class="text-sm underline hover:opacity-75" :href="`tel:${vModel}`" target="_blank">
<LazyCellClampedText :value="vModel" :lines="rowHeight" />
</a>
<LazyCellClampedText v-else :value="vModel" :lines="rowHeight" />
</template> </template>

4
packages/nc-gui/components/cell/SingleSelect.vue

@ -9,6 +9,7 @@ import {
ActiveCellInj, ActiveCellInj,
CellClickHookInj, CellClickHookInj,
ColumnInj, ColumnInj,
EditColumnInj,
EditModeInj, EditModeInj,
IsFormInj, IsFormInj,
IsKanbanInj, IsKanbanInj,
@ -59,6 +60,8 @@ const isKanban = inject(IsKanbanInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -300,6 +303,7 @@ const selectedOpt = computed(() => {
v-model:value="vModel" v-model:value="vModel"
class="w-full overflow-hidden" class="w-full overflow-hidden"
:class="{ 'caret-transparent': !hasEditRoles }" :class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? '(Optional)' : ''"
:allow-clear="!column.rqd && editAllowed" :allow-clear="!column.rqd && editAllowed"
:bordered="false" :bordered="false"
:open="isOpen && editAllowed" :open="isOpen && editAllowed"

12
packages/nc-gui/components/cell/Text.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, IsExpandedFormOpenInj, ReadonlyInj, RowHeightInj, inject, ref, useVModel } from '#imports' import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, ReadonlyInj, RowHeightInj, inject, ref, useVModel } from '#imports'
interface Props { interface Props {
modelValue?: string | null modelValue?: string | null
@ -12,7 +12,9 @@ const emits = defineEmits(['update:modelValue'])
const { showNull } = useGlobal() const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined)) const rowHeight = inject(RowHeightInj, ref(undefined))
@ -22,7 +24,7 @@ const vModel = useVModel(props, 'modelValue', emits)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>
@ -30,8 +32,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
v-if="!readonly && editEnabled" v-if="!readonly && editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="h-full w-full outline-none bg-transparent" class="h-full w-full outline-none p-2 bg-transparent"
:class="{ '!px-2': editEnabled }" :placeholder="isEditColumn ? '(Optional)' : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

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

@ -1,16 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { ActiveCellInj, EditModeInj, IsExpandedFormOpenInj, RowHeightInj, iconMap, inject, useVModel } from '#imports'
import {
ActiveCellInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
RowHeightInj,
iconMap,
inject,
useVModel,
} from '#imports'
const props = defineProps<{ const props = defineProps<{
modelValue?: string | number modelValue?: string | number
isFocus?: boolean
}>() }>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const column = inject(ColumnInj) const column = inject(ColumnInj)
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined)) const rowHeight = inject(RowHeightInj, ref(undefined))
@ -20,7 +33,7 @@ const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLTextAreaElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLTextAreaElement)?.focus()
const height = computed(() => { const height = computed(() => {
if (!rowHeight.value) return 60 if (!rowHeight.value) return 60
@ -68,6 +81,7 @@ onClickOutside(inputWrapperRef, (e) => {
:style="{ :style="{
minHeight: `${height}px`, minHeight: `${height}px`,
}" }"
:placeholder="isEditColumn ? '(Optional)' : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.alt.enter.stop @keydown.alt.enter.stop
@keydown.shift.enter.stop @keydown.shift.enter.stop

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

@ -1,6 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ActiveCellInj, ReadonlyInj, inject, onClickOutside, useProject, useSelectedCellKeyupListener, watch } from '#imports' import {
ActiveCellInj,
EditColumnInj,
ReadonlyInj,
inject,
onClickOutside,
useProject,
useSelectedCellKeyupListener,
watch,
} from '#imports'
interface Props { interface Props {
modelValue?: string | null | undefined modelValue?: string | null | undefined
@ -21,6 +30,8 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false)) const editable = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const isTimeInvalid = ref(false) const isTimeInvalid = ref(false)
@ -76,7 +87,17 @@ watch(
{ flush: 'post' }, { flush: 'post' },
) )
const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isTimeInvalid.value ? 'Invalid time' : '')) const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
} else if (modelValue === null && showNull.value) {
return 'NULL'
} else if (isTimeInvalid.value) {
return 'Invalid time'
} else {
return ''
}
})
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
@ -101,7 +122,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:bordered="false" :bordered="false"
use12-hours use12-hours
format="HH:mm" format="HH:mm"
class="!w-full !px-0 !border-none" class="!w-full !px-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }" :class="{ 'nc-null': modelValue === null && showNull }"
:placeholder="placeholder" :placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"

8
packages/nc-gui/components/cell/Url.vue

@ -3,6 +3,7 @@ import type { VNodeRef } from '@vue/runtime-core'
import { import {
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
ColumnInj, ColumnInj,
EditColumnInj,
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsSurveyFormInj, IsSurveyFormInj,
@ -33,6 +34,8 @@ const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)! const editEnabled = inject(EditModeInj)!
const isEditColumn = inject(EditColumnInj, ref(false))
const disableOverlay = inject(CellUrlDisableOverlayInj, ref(false)) const disableOverlay = inject(CellUrlDisableOverlayInj, ref(false))
// Used in the logic of when to display error since we are not storing the url if it's not valid // Used in the logic of when to display error since we are not storing the url if it's not valid
@ -67,7 +70,7 @@ const { cellUrlOptions } = useCellUrlConfig(url)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
watch( watch(
() => editEnabled.value, () => editEnabled.value,
@ -88,7 +91,8 @@ watch(
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none text-sm w-full px-2 bg-transparent h-full" :placeholder="isEditColumn ? 'Enter default URL (Optional)' : ''"
class="outline-none text-sm w-full px-2 py-2 bg-transparent h-full"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

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

@ -1,6 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ActiveCellInj, ReadonlyInj, computed, inject, onClickOutside, ref, useSelectedCellKeyupListener, watch } from '#imports' import {
ActiveCellInj,
EditColumnInj,
ReadonlyInj,
computed,
inject,
onClickOutside,
ref,
useSelectedCellKeyupListener,
watch,
} from '#imports'
interface Props { interface Props {
modelValue?: number | string | null modelValue?: number | string | null
@ -19,6 +29,8 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false)) const editable = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isYearInvalid = ref(false) const isYearInvalid = ref(false)
const localState = computed({ const localState = computed({
@ -42,7 +54,7 @@ const localState = computed({
} }
if (val?.isValid()) { if (val?.isValid()) {
emit('update:modelValue', Number(val.format('YYYY'))) emit('update:modelValue', val.format('YYYY'))
} }
}, },
}) })
@ -62,7 +74,17 @@ watch(
{ flush: 'post' }, { flush: 'post' },
) )
const placeholder = computed(() => (modelValue === null && showNull.value ? 'NULL' : isYearInvalid.value ? 'Invalid year' : '')) const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
} else if (modelValue === null && showNull.value) {
return 'NULL'
} else if (isYearInvalid.value) {
return 'Invalid year'
} else {
return ''
}
})
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
@ -85,15 +107,16 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
v-model:value="localState" v-model:value="localState"
picker="year" picker="year"
:bordered="false" :bordered="false"
class="!w-full !px-0 !border-none" class="!w-full !px-1 !border-none"
:class="{ 'nc-null': modelValue === null && showNull }" :class="{ 'nc-null': modelValue === null && showNull }"
:placeholder="placeholder" :placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="(!readOnly && !localState && !isPk) || isEditColumn"
:input-read-only="true" :input-read-only="true"
:open="(readOnly || (localState && isPk)) && !active && !editable ? false : open" :open="(readOnly || (localState && isPk)) && !active && !editable ? false : open"
:dropdown-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''}`" :dropdown-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''}`"
@click="open = (active || editable) && !open" @click="open = (active || editable) && !open"
@change="open = (active || editable) && !open" @change="open = (active || editable) && !open"
@ok="open = !open"
> >
<template #suffixIcon></template> <template #suffixIcon></template>
</a-date-picker> </a-date-picker>

4
packages/nc-gui/components/general/ColorPicker.vue

@ -59,12 +59,12 @@ watch(picked, (n, _o) => {
{{ compare(picked, color) ? '&#10003;' : '' }} {{ compare(picked, color) ? '&#10003;' : '' }}
</button> </button>
<button <button
class="h-6 w-6 mt-2.5 ml-1 border-1 border-[grey] rounded-md flex items-center justify-center" class="h-6 w-6 mt-2.7 ml-1 border-1 border-[grey] rounded-md flex items-center justify-center"
@click="isPickerOn = !isPickerOn" @click="isPickerOn = !isPickerOn"
> >
<GeneralTooltip> <GeneralTooltip>
<template #title>More colors</template> <template #title>More colors</template>
<GeneralIcon :icon="isPickerOn ? 'minus' : 'plus'" /> <GeneralIcon class="mt-1.5" :icon="isPickerOn ? 'minus' : 'plus'" />
</GeneralTooltip> </GeneralTooltip>
</button> </button>
</div> </div>

33
packages/nc-gui/components/smartsheet/Cell.vue

@ -4,6 +4,7 @@ import { isSystemColumn } from 'nocodb-sdk'
import { import {
ActiveCellInj, ActiveCellInj,
ColumnInj, ColumnInj,
EditColumnInj,
EditModeInj, EditModeInj,
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
@ -61,7 +62,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'save', 'navigate', 'update:editEnabled']) const emit = defineEmits(['update:modelValue', 'save', 'navigate', 'update:editEnabled', 'update:value'])
const column = toRef(props, 'column') const column = toRef(props, 'column')
@ -87,6 +88,8 @@ const isLocked = inject(IsLockedInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false)) const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isEditColumnMenu = inject(EditColumnInj, ref(false))
const { currentRow } = useSmartsheetRowStoreOrThrow() const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUis } = storeToRefs(useProject()) const { sqlUis } = storeToRefs(useProject())
@ -109,7 +112,10 @@ const vModel = computed({
return props.modelValue return props.modelValue
}, },
set: (val) => { set: (val) => {
if (val !== props.modelValue) { if (isEditColumnMenu.value) {
column.value.cdf = val
emit('update:value', val)
} else if (val !== props.modelValue) {
currentRow.value.rowMeta.changed = true currentRow.value.rowMeta.changed = true
emit('update:modelValue', val) emit('update:modelValue', val)
if (isAutoSaved(column.value)) { if (isAutoSaved(column.value)) {
@ -173,6 +179,9 @@ function initIntersectionObserver() {
}) })
} }
const numberInputAlignment = computed(() => {
return isEditColumnMenu.value ? 'left' : 'right'
})
// observe the cell when it is mounted // observe the cell when it is mounted
onMounted(() => { onMounted(() => {
initIntersectionObserver() initIntersectionObserver()
@ -205,8 +214,18 @@ onUnmounted(() => {
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" /> <LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" /> <LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" /> <LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellSingleSelect v-else-if="isSingleSelect(column)" v-model="vModel" :row-index="props.rowIndex" /> <LazyCellSingleSelect
<LazyCellMultiSelect v-else-if="isMultiSelect(column)" v-model="vModel" :row-index="props.rowIndex" /> v-else-if="isSingleSelect(column)"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellMultiSelect
v-else-if="isMultiSelect(column)"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" /> <LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" /> <LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker <LazyCellDateTimePicker
@ -224,9 +243,9 @@ onUnmounted(() => {
<LazyCellPercent v-else-if="isPercent(column)" v-model="vModel" /> <LazyCellPercent v-else-if="isPercent(column)" v-model="vModel" />
<LazyCellCurrency v-else-if="isCurrency(column)" v-model="vModel" @save="emit('save')" /> <LazyCellCurrency v-else-if="isCurrency(column)" v-model="vModel" @save="emit('save')" />
<LazyCellDecimal v-else-if="isDecimal(column)" v-model="vModel" /> <LazyCellDecimal v-else-if="isDecimal(column)" v-model="vModel" />
<LazyCellInteger v-else-if="isInt(column, abstractType)" v-model="vModel" />
<LazyCellFloat v-else-if="isFloat(column, abstractType)" v-model="vModel" /> <LazyCellFloat v-else-if="isFloat(column, abstractType)" v-model="vModel" />
<LazyCellText v-else-if="isString(column, abstractType)" v-model="vModel" /> <LazyCellText v-else-if="isString(column, abstractType)" v-model="vModel" />
<LazyCellInteger v-else-if="isInt(column, abstractType)" v-model="vModel" />
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" /> <LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" /> <LazyCellText v-else v-model="vModel" />
<div <div
@ -240,9 +259,9 @@ onUnmounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.nc-grid-numeric-cell { .nc-grid-numeric-cell {
@apply text-right; text-align: v-bind(numberInputAlignment);
:deep(input) { :deep(input) {
@apply text-right; text-align: v-bind(numberInputAlignment);
} }
} }
</style> </style>

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

@ -20,17 +20,6 @@ const { isPg } = useProject()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const sampleValue = computed(() => {
switch (vModel.value.uidt) {
case UITypes.SingleSelect:
return 'eg : a'
case UITypes.MultiSelect:
return 'eg : a,b,c'
default:
return sqlUi.value.getDefaultValueForDatatype(vModel.value.dt)
}
})
const hideLength = computed(() => { const hideLength = computed(() => {
return [UITypes.SingleSelect, UITypes.MultiSelect].includes(vModel.value.uidt) return [UITypes.SingleSelect, UITypes.MultiSelect].includes(vModel.value.uidt)
}) })
@ -44,7 +33,7 @@ vModel.value.au = !!vModel.value.au */
</script> </script>
<template> <template>
<div class="p-4 border-[0.1px] radius-1 border-grey w-full flex flex-col gap-2"> <div class="p-4 border-[0.1px] radius-1 rounded-md border-grey w-full flex flex-col gap-2">
<template v-if="props.advancedDbOptions"> <template v-if="props.advancedDbOptions">
<div class="flex justify-between w-full gap-1"> <div class="flex justify-between w-full gap-1">
<a-form-item label="NN"> <a-form-item label="NN">
@ -91,10 +80,9 @@ vModel.value.au = !!vModel.value.au */
</a-select> </a-select>
</a-form-item> </a-form-item>
<LazySmartsheetColumnPgBinaryOptions v-if="isPg(meta.base_id) && vModel.dt === 'bytea'" v-model:value="vModel" />
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')"> <a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input <a-input
class="!rounded-md"
v-model:value="vModel.dtxp" v-model:value="vModel.dtxp"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)" :disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)"
@input="onAlter" @input="onAlter"
@ -102,13 +90,10 @@ vModel.value.au = !!vModel.value.au */
</a-form-item> </a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale"> <a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input v-model:value="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" /> <a-input v-model:value="vModel.dtxs" class="!rounded-md" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item> </a-form-item>
</template>
<a-form-item :label="$t('placeholder.defaultValue')"> <LazySmartsheetColumnPgBinaryOptions v-if="isPg(meta?.base_id) && vModel.dt === 'bytea'" v-model:value="vModel" />
<a-textarea v-model:value="vModel.cdf" auto-size @input="onAlter(2, true)" /> </template>
<span class="text-gray-400 text-xs">{{ sampleValue }}</span>
</a-form-item>
</div> </div>
</template> </template>

63
packages/nc-gui/components/smartsheet/column/DefaultValue.vue

@ -0,0 +1,63 @@
<script lang="ts" setup>
import { UITypes } from 'nocodb-sdk'
import { iconMap } from '#imports'
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
provide(EditColumnInj, ref(true))
const vModel = useVModel(props, 'value', emit)
const rowRef = ref({
row: {},
oldRow: {},
rowMeta: {
isUpdatedFromCopyNPaste: [vModel?.value.title],
},
})
const cdfValue = computed({
get: () => {
if (vModel.value.uidt === UITypes.MultiSelect || vModel.value.uidt === UITypes.SingleSelect) {
return (vModel.value.cdf ?? '').replaceAll("'", '')
} else if (
vModel.value.uidt === UITypes.SingleLineText ||
vModel.value.uidt === UITypes.LongText ||
vModel.value.uidt === UITypes.Email ||
vModel.value.uidt === UITypes.URL ||
vModel.value.uidt === UITypes.JSON ||
vModel.value.uidt === UITypes.DateTime ||
vModel.value.uidt === UITypes.Time ||
vModel.value.uidt === UITypes.Year ||
vModel.value.uidt === UITypes.Date
) {
return (vModel.value.cdf ?? '').replace(/^'/, '').replace(/'$/, '')
}
return vModel.value.cdf
},
set: (value) => {
vModel.value.cdf = value
},
})
useProvideSmartsheetRowStore(vModel, rowRef)
</script>
<template>
<div class="!my-3 text-xs">Default Value</div>
<div class="flex flex-row gap-2">
<div class="border-1 flex items-center w-full px-1 my-[-4px] border-gray-300 rounded-md">
<LazySmartsheetCell :column="vModel" :model-value="cdfValue" :edit-enabled="true" />
<component
:is="iconMap.close"
v-if="vModel.uidt !== UITypes.Year"
class="w-4 h-4 cursor-pointer rounded-full !text-black-500 text-gray-500 cursor-pointer hover:bg-gray-50"
@click="cdfValue = null"
/>
</div>
</div>
</template>
<style scoped></style>

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

@ -8,7 +8,8 @@ import {
ReloadViewDataHookInj, ReloadViewDataHookInj,
computed, computed,
inject, inject,
isEeUI, isJSON,
isTextArea,
message, message,
onMounted, onMounted,
ref, ref,
@ -18,9 +19,9 @@ import {
useI18n, useI18n,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useProject,
watchEffect, watchEffect,
} from '#imports' } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier' import MdiIdentifierIcon from '~icons/mdi/identifier'
@ -63,12 +64,12 @@ const isForm = inject(IsFormInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false)) const isKanban = inject(IsKanbanInj, ref(false))
const { isMysql, isMssql } = useProject()
const reloadDataTrigger = inject(ReloadViewDataHookInj) const reloadDataTrigger = inject(ReloadViewDataHookInj)
const advancedOptions = ref(false) const advancedOptions = ref(false)
const advancedDbOptions = ref(false)
const mounted = ref(false) const mounted = ref(false)
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber] const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
@ -290,12 +291,34 @@ if (props.fromTableExplorer) {
v-model:value="formState" v-model:value="formState"
/> />
</div> </div>
<a-checkbox
v-if="formState.meta && columnToValidate.includes(formState.uidt)"
v-model:checked="formState.meta.validate"
class="ml-1 mb-1"
>
<span class="text-[10px] text-gray-600">
{{ `Accept only valid ${formState.uidt}` }}
</span>
</a-checkbox>
<div class="!my-3">
<!--
Default Value for JSON & LongText is not supported in MySQL
Default Value is Disabled for MSSQL -->
<LazySmartsheetColumnDefaultValue
v-if="
!isVirtualCol(formState) &&
!isAttachment(formState) &&
!isMssql(meta!.base_id) &&
!(isMysql(meta!.base_id) && (isJSON(formState) || isTextArea(formState)))
"
v-model:value="formState"
/>
</div>
<div <div
v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt)" v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt) && !appInfo.ee"
class="text-xs cursor-pointer text-gray-400 nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end" class="text-xs cursor-pointer text-gray-400 nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions" @click="advancedOptions = !advancedOptions"
@dblclick="advancedDbOptions = !advancedDbOptions"
> >
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }} {{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" /> <component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
@ -303,25 +326,12 @@ if (props.fromTableExplorer) {
<Transition name="layout" mode="out-in"> <Transition name="layout" mode="out-in">
<div v-if="advancedOptions" class="overflow-hidden"> <div v-if="advancedOptions" class="overflow-hidden">
<a-checkbox <LazySmartsheetColumnAttachmentOptions v-if="appInfo.ee && isAttachment(formState)" v-model:value="formState" />
v-if="formState.meta && columnToValidate.includes(formState.uidt)"
v-model:checked="formState.meta.validate"
class="ml-1 mb-1"
>
<span class="text-[10px] text-gray-600">
{{ `Accept only valid ${formState.uidt}` }}
</span>
</a-checkbox>
<LazySmartsheetColumnAttachmentOptions
v-if="appInfo.ee && formState.uidt === UITypes.Attachment"
v-model:value="formState"
/>
<LazySmartsheetColumnAdvancedOptions <LazySmartsheetColumnAdvancedOptions
v-if="formState.uidt !== UITypes.Attachment" v-if="formState.uidt !== UITypes.Attachment"
v-model:value="formState" v-model:value="formState"
:advanced-db-options="advancedDbOptions || formState.uidt === UITypes.SpecificDBType" :advanced-db-options="advancedOptions || formState.uidt === UITypes.SpecificDBType"
/> />
</div> </div>
</Transition> </Transition>

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

@ -101,11 +101,6 @@ onMounted(() => {
} }
if (vModel.value.cdf) { if (vModel.value.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg.value) {
vModel.value.cdf = vModel.value.cdf.substring(vModel.value.cdf.indexOf(`'`) + 1, vModel.value.cdf.lastIndexOf(`'`))
}
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped // Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql.value) { if (!isMysql.value) {
vModel.value.cdf = vModel.value.cdf.replace(/''/g, "'") vModel.value.cdf = vModel.value.cdf.replace(/''/g, "'")
@ -184,6 +179,16 @@ watch(inputs, () => {
inputs.value.$el.focus() inputs.value.$el.focus()
} }
}) })
// Removes the Select Option from cdf if the option is removed
watch(vModel.value, (next) => {
const cdfs = (next.cdf ?? '').split(',')
const values = (next.colOptions.options ?? []).map((col) => {
return col.title.replace(/^'/, '').replace(/'$/, '')
})
const newCdf = cdfs.filter((c: string) => values.includes(c)).join(',')
next.cdf = newCdf.length === 0 ? null : newCdf
})
</script> </script>
<template> <template>

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

@ -243,6 +243,7 @@ export function useData(args: {
col.uidt === UITypes.QrCode || col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode || col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup || col.uidt === UITypes.Rollup ||
col.uidt === UITypes.Checkbox ||
col.au || col.au ||
col.cdf?.includes(' on update ') col.cdf?.includes(' on update ')
) )

1
packages/nc-gui/context/index.ts

@ -41,6 +41,7 @@ export const DocsLocalPageInj: InjectionKey<Ref<PageSidebarNode | undefined>> =
export const ProjectRoleInj: InjectionKey<Ref<string | string[]>> = Symbol('project-roles-injection') export const ProjectRoleInj: InjectionKey<Ref<string | string[]>> = Symbol('project-roles-injection')
export const ProjectInj: InjectionKey<Ref<NcProject>> = Symbol('project-injection') export const ProjectInj: InjectionKey<Ref<NcProject>> = Symbol('project-injection')
export const ProjectIdInj: InjectionKey<Ref<string>> = Symbol('project-id-injection') export const ProjectIdInj: InjectionKey<Ref<string>> = Symbol('project-id-injection')
export const EditColumnInj: InjectionKey<Ref<boolean>> = Symbol('edit-column-injection')
export const TreeViewInj: InjectionKey<{ export const TreeViewInj: InjectionKey<{
setMenuContext: (type: 'project' | 'base' | 'table' | 'main' | 'layout', value?: any) => void setMenuContext: (type: 'project' | 'base' | 'table' | 'main' | 'layout', value?: any) => void
duplicateTable: (table: TableType) => void duplicateTable: (table: TableType) => void

2
packages/noco-docs/docs/050.engineering/040.unit-testing.md

@ -16,7 +16,7 @@ description: "Overview to Unit Testing"
### Setup ### Setup
```bash ```bash
pnpm --filter=-nocodb install pnpm --filter=nocodb install
# add a .env file # add a .env file
cp tests/unit/.env.sample tests/unit/.env cp tests/unit/.env.sample tests/unit/.env

13
packages/nocodb-sdk/src/lib/Api.ts

@ -321,7 +321,7 @@ export interface ColumnType {
/** Column Comment */ /** Column Comment */
cc?: string; cc?: string;
/** Column Default */ /** Column Default */
cdf?: StringOrNullType; cdf?: StringOrNullOrBooleanOrNumberType;
/** Character Maximum Length */ /** Character Maximum Length */
clen?: number | null | string; clen?: number | null | string;
/** Column Options */ /** Column Options */
@ -1690,7 +1690,7 @@ export interface NormalColumnRequestType {
/** Column Comment */ /** Column Comment */
cc?: StringOrNullType; cc?: StringOrNullType;
/** Column Default Value */ /** Column Default Value */
cdf?: StringOrNullType; cdf?: StringOrNullOrBooleanOrNumberType;
/** Column Name */ /** Column Name */
column_name: string; column_name: string;
/** Model for StringOrNull */ /** Model for StringOrNull */
@ -2280,6 +2280,15 @@ export interface SortReqType {
*/ */
export type StringOrNullType = string | null; export type StringOrNullType = string | null;
/**
* Model for StringOrNullOrBooleanOrNumber
*/
export type StringOrNullOrBooleanOrNumberType =
| string
| null
| boolean
| number;
/** /**
* Model for Table * Model for Table
*/ */

5
packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts

@ -261,13 +261,12 @@ export class SqliteUi {
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i; /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i;
const match = query.match(reg); const match = query.match(reg);
return match && match[1]; return match && match[1];
} }*/
static columnEditable(_colObj) { static columnEditable(_colObj) {
return true; // colObj.altered === 1; return true; // colObj.altered === 1;
} }
static handleRawOutput(result, headers) { /*static handleRawOutput(result, headers) {
console.log(result); console.log(result);
if (Array.isArray(result) && result[0]) { if (Array.isArray(result) && result[0]) {
const keys = Object.keys(result[0]); const keys = Object.keys(result[0]);

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

@ -876,9 +876,9 @@ class PGClient extends KnexClient {
// todo : need to find if column is unique or not // todo : need to find if column is unique or not
// column['unique'] = response.rows[i]['cst'].indexOf('UNIQUE') === -1 ? false : true; // column['unique'] = response.rows[i]['cst'].indexOf('UNIQUE') === -1 ? false : true;
column.cdf = response.rows[i].cdf; 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; : response.rows[i].cdf;
// todo : need to find column comment // todo : need to find column comment
column.cc = response.rows[i].cc; column.cc = response.rows[i].cc;

25
packages/nocodb/src/schema/swagger.json

@ -16018,7 +16018,7 @@
"type": "string" "type": "string"
}, },
"cdf": { "cdf": {
"$ref": "#/components/schemas/StringOrNull", "$ref": "#/components/schemas/StringOrNullOrBooleanOrNumber",
"description": "Column Default" "description": "Column Default"
}, },
"clen": { "clen": {
@ -19319,7 +19319,7 @@
"description": "Column Comment" "description": "Column Comment"
}, },
"cdf": { "cdf": {
"$ref": "#/components/schemas/StringOrNull", "$ref": "#/components/schemas/StringOrNullOrBooleanOrNumber",
"description": "Column Default Value" "description": "Column Default Value"
}, },
"column_name": { "column_name": {
@ -20855,6 +20855,27 @@
"id": "p1g7xrgdsn540" "id": "p1g7xrgdsn540"
} }
}, },
"StringOrNullOrBooleanOrNumber": {
"description": "Model for StringOrNullOrBooleanOrNumber",
"examples": [
"string"
],
"oneOf": [
{
"type": "string"
},
{
"type": "null"
},
{
"type": "boolean"
},
{
"type": "number"
}
],
"title": "StringOrNullOrBooleanOrNumber Model"
},
"Table": { "Table": {
"description": "Model for Table", "description": "Model for Table",
"examples": [ "examples": [

Loading…
Cancel
Save