Browse Source

Nc feat/new field modal (#8578)

* fix(nc-gui): remove field modal title & type selector label

* fix(nc-gui): hide default value input initially

* fix(nc-gui): remove clear icon from default value input

* fix(nc-gui): update email, phone, url column validation settings ui

* fix(nc-gui): add missing validate field condition

* fix(nc-gui): update long text field modal ui

* fix(nc-gui): update default field modal width & enable rich text option

* fix(nc-gui): small changes

* fix(nc-gui): hide default value input only if user clicks on delete icon

* fix(nc-gui): decimal field option ui

* fix(nc-gui): update percent option ui

* fix(nc-gui): small changes

* fix(nc-gui): update field modal switch option alignment

* fix(nc-gui): update date & dateTime field modal options

* feat(nc-gui): add 12, 24 hrs time format option in field modal

* feat(nc-gui): add 12hr time cell format support

* fix(nc-gui): update time cell placeholder according to time format

* fix(nc-gui): field modal default value option visibility issue

* fix(nc-gui): update barcode, qr code field modal option

* fix(nc-gui): field modal expanded json input modal overlay click issue

* fix(nc-gui): currency code option from field modal

* fix(nc-gui): udpate duration option

* fix(nc-gui): date time cell clear icon visibility issue in link record dropdown

* fix(nc-gui): update field modal lookup options

* fix(nc-gui): update user option from field modal

* fix(nc-gui): update rollup option from field modal

* fix(nc-gui): update select field type ui for create column

* fix(nc-gui): update field modal cancel & save btn alignment

* fix(nc-gui): update formula option margin

* fic(nc-gui): update select type option

* fix(nc-gui): small changes

* fix(nc-gui): update links field options

* fix(nc-gui): small changes

* fix(nc-gui): select option border issue

* fix(nc-gui): add new color picker

* fix(nc-gui): update rating field options

* fix(nc-gui): update geodata field options

* fix(nc-gui): geodata option small changes

* fix(nc-gui): add new color picker for select type options

* fix(nc-gui): show only title & field type list if uidt is null

* feat(nc-gui): add 12hrs time support in dateTime cell

* fix(nc-gui): formula suggestion list visibility issue

* fix(nc-gui): reduce formaula suggestion fields icon size

* fix(nc-gui): rich text default value visibility issue in field modal

* fix(nc-gui): update rich text default value bubble menu option

* fix(nc-gui): some pr review changes

* fix(nc-gui): remove example from duration format

* feat(nc-gui): add keyboard navigation support for field list

* fix(nc-gui): update email, url, phone validate text

* fix(nc-gui): update qr & barcode value select input

* fix(nc-gui): pr review changes

* test: update create column test cases

* fix(nc-gui): remove required symbol from field modal inputs

* fix(nc-gui): remove delete default value icon and add cross icon in default input itself

* test: update duration field type test

* fix(nc-gui): update column name & type input shadow

* fix(nc-gui): add hover effect on selected type

* fix(nc-gui): enabel rich text case update

* fix(nc-gui): update select options

* fix(nc-gui): show full time format in edit modal default value

* fix(nc-gui): remove optional placeholder of default value

* fix(nc-gui): instead of removing field type option disable it if it is onlyNameUpdateOnEditColumns

* fix(nc-gui): update links field icons from field modal

* fix(nc-gui): add links options in edit modal

* fix(nc-gui): show links config and disable if it is not editable in edit column mode

* fix(nc-gui): add support to configure date time format for create & last modified type field

* fix(nc-gui): virtual field icon visibility issue if it is edit mode

* fix(nc-gui): disabled edit option from field context menu if column is pk or system column

* fix(nc-gui): update field modal submit btn text

* fix(nc-gui): add shdow on input field in field modal

* fix(nc-gui): disable submit btn if field modal has some warnings

* test: update field add/edit save test case

* test: update links column add/edit test cases

* test: uncomment code

* test: update user field default value update test cases

* test: update select type option default value

* test: update multi field editor test cases

* test: update kanban view add option test cases

* test: update multifield editor test cases

* test: update create column keyboard shortcut test case

* chore(nc-gui): lint

* fix(nc-gui): field modal redio option shadow issue

* fix(nc-gui): update field modal select option color picker btn border radius

* fix(nc-gui): checkbox & rating icon alignment issue

* fix(nc-gui): update field modal formula field

* fix(nc-gui): field modal padding and gap issue

* fix(nc-gui): update set default value font case & font color

* fix(nc-gui): update field modal formula suggestion list ui

* fix(nc-gui): removecolumn create field search list from multifield editor

* fix(nc-gui): add placeholder for lookup & rollup options

* fix: label

* fix(nc-gui): remove placeholder from select type

* fix(nc-gui): remove link type from link field select option

* fix(nc-gui): qr, barcode value field icon issue

* fix(nc-gui): set color picker tab according to active color

* fix(nc-gui): json editor save btn ui changes in edit modal

* fix(nc-gui): disable editing primary key col

* chore(nc-gui): lint

---------

Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>
pull/8628/head
Ramesh Mane 5 months ago committed by GitHub
parent
commit
2640656ddd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/nc-gui/assets/style.scss
  2. 2
      packages/nc-gui/components/cell/Currency.vue
  3. 17
      packages/nc-gui/components/cell/DatePicker.vue
  4. 100
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 2
      packages/nc-gui/components/cell/Decimal.vue
  6. 6
      packages/nc-gui/components/cell/Duration.vue
  7. 1
      packages/nc-gui/components/cell/Email.vue
  8. 1
      packages/nc-gui/components/cell/Float.vue
  9. 35
      packages/nc-gui/components/cell/GeoData.vue
  10. 1
      packages/nc-gui/components/cell/Integer.vue
  11. 48
      packages/nc-gui/components/cell/Json.vue
  12. 1
      packages/nc-gui/components/cell/MultiSelect.vue
  13. 2
      packages/nc-gui/components/cell/Percent.vue
  14. 1
      packages/nc-gui/components/cell/PhoneNumber.vue
  15. 4
      packages/nc-gui/components/cell/RichText.vue
  16. 10
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  17. 1
      packages/nc-gui/components/cell/SingleSelect.vue
  18. 1
      packages/nc-gui/components/cell/Text.vue
  19. 1
      packages/nc-gui/components/cell/TextArea.vue
  20. 52
      packages/nc-gui/components/cell/TimePicker.vue
  21. 1
      packages/nc-gui/components/cell/Url.vue
  22. 20
      packages/nc-gui/components/cell/YearPicker.vue
  23. 197
      packages/nc-gui/components/general/AdvanceColorPicker.vue
  24. 21
      packages/nc-gui/components/nc/Switch.vue
  25. 4
      packages/nc-gui/components/nc/TimeSelector.vue
  26. 17
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  27. 133
      packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue
  28. 79
      packages/nc-gui/components/smartsheet/column/CheckboxOptions.vue
  29. 4
      packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue
  30. 7
      packages/nc-gui/components/smartsheet/column/DateOptions.vue
  31. 95
      packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue
  32. 3
      packages/nc-gui/components/smartsheet/column/DecimalOptions.vue
  33. 71
      packages/nc-gui/components/smartsheet/column/DefaultValue.vue
  34. 12
      packages/nc-gui/components/smartsheet/column/DurationOptions.vue
  35. 520
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  36. 8
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  37. 98
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  38. 2
      packages/nc-gui/components/smartsheet/column/LinkOptions.vue
  39. 175
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  40. 8
      packages/nc-gui/components/smartsheet/column/LongTextOptions.vue
  41. 140
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  42. 12
      packages/nc-gui/components/smartsheet/column/PercentOptions.vue
  43. 94
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  44. 85
      packages/nc-gui/components/smartsheet/column/RatingOptions.vue
  45. 55
      packages/nc-gui/components/smartsheet/column/RichLongTextDefaultValue.vue
  46. 82
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  47. 96
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  48. 2
      packages/nc-gui/components/smartsheet/column/SpecificDBTypeOptions.vue
  49. 41
      packages/nc-gui/components/smartsheet/column/TimeOptions.vue
  50. 115
      packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue
  51. 44
      packages/nc-gui/components/smartsheet/column/UserOptions.vue
  52. 6
      packages/nc-gui/components/smartsheet/details/Fields.vue
  53. 2
      packages/nc-gui/components/smartsheet/header/Cell.vue
  54. 4
      packages/nc-gui/components/smartsheet/header/Menu.vue
  55. 2
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  56. 13
      packages/nc-gui/composables/useColumnCreateStore.ts
  57. 2
      packages/nc-gui/composables/useData.ts
  58. 21
      packages/nc-gui/lang/en.json
  59. 112
      packages/nc-gui/utils/colorsUtils.ts
  60. 113
      packages/nc-gui/windi.config.ts
  61. 9
      packages/nocodb/src/services/columns.service.ts
  62. 52
      tests/playwright/pages/Dashboard/Details/FieldsPage.ts
  63. 15
      tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts
  64. 2
      tests/playwright/pages/Dashboard/Grid/Column/UserOptionColumn.ts
  65. 74
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  66. 2
      tests/playwright/pages/Dashboard/common/Toolbar/AddEditKanbanStack.ts
  67. 10
      tests/playwright/tests/db/columns/columnDuration.spec.ts
  68. 2
      tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts
  69. 1
      tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

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

@ -251,7 +251,7 @@ a {
}
// select dropdown border style
.ant-select-dropdown {
@apply border-1 border-gray-200;
@apply border-1 border-gray-200 rounded-lg;
.rc-virtual-list-scrollbar {
@apply !w-1;

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

@ -108,7 +108,7 @@ onMounted(() => {
type="number"
class="nc-cell-field h-full border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder"
:disabled="readOnly"
@blur="onBlur"
@keydown.enter="onKeydownEnter"

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

@ -144,11 +144,10 @@ watch(editable, (nextValue) => {
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isDateInvalid.value) ||
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(columnMeta.value) && active.value)
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(columnMeta.value) && active.value) ||
isEditColumn.value
) {
return dateFormat.value
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isDateInvalid.value) {
@ -301,7 +300,7 @@ function handleSelectDate(value?: dayjs.Dayjs) {
>
<div
:title="localState?.format(dateFormat)"
class="nc-date-picker h-full flex items-center justify-between ant-picker-input relative group"
class="nc-date-picker h-full flex items-center justify-between ant-picker-input relative"
>
<input
ref="datePickerRef"
@ -320,9 +319,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
/>
<GeneralIcon
v-if="localState"
v-if="localState && !readOnly"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-date-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
@ -354,7 +353,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
.nc-cell-field {
&:hover .nc-clear-date-icon {
@apply visible;
}
}
</style>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { dateFormats, isSystemColumn, isValidTimeFormat, timeFormats } from 'nocodb-sdk'
import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk'
interface Props {
modelValue?: string | null
@ -9,8 +9,15 @@ interface Props {
}
const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const timeFormatsObj = {
[timeFormats[0]]: 'hh:mm A',
[timeFormats[1]]: 'hh:mm:ss A',
[timeFormats[2]]: 'hh:mm:ss.SSS A',
}
const { isMssql, isXcdbBase } = useBase()
const { showNull, isMobileMode } = useGlobal()
@ -183,11 +190,14 @@ watch(
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isDateInvalid.value) ||
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(column.value) && active.value)
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(column.value) && active.value) ||
isEditColumn.value
) {
return { dateTime: dateTimeFormat.value, date: dateFormat.value, time: timeFormat.value }
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
return {
dateTime: dateTimeFormat.value,
date: dateFormat.value,
time: parseProp(column.value.meta).is12hrFormat ? `${timeFormat.value} AM` : timeFormat.value,
}
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isDateInvalid.value) {
@ -346,12 +356,21 @@ const handleUpdateValue = (e: Event, _isDatePicker: boolean) => {
return
}
if (timeFormat.value === 'HH:mm' && targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
if (isValidTimeFormat(targetValue, timeFormat.value)) {
tempDate.value = dayjs(`${(tempDate.value ?? dayjs()).format('YYYY-MM-DD')} ${targetValue}`)
targetValue = parseProp(column.value.meta).is12hrFormat
? targetValue
.trim()
.toUpperCase()
.replace(/(AM|PM)$/, ' $1')
.replace(/\s+/g, ' ')
: targetValue.trim()
const parsedDate = dayjs(
targetValue,
parseProp(column.value.meta).is12hrFormat ? timeFormatsObj[timeFormat.value] : timeFormat.value,
)
if (parsedDate.isValid()) {
tempDate.value = dayjs(`${(tempDate.value ?? dayjs()).format('YYYY-MM-DD')} ${parsedDate.format(timeFormat.value)}`)
}
}
}
@ -384,35 +403,32 @@ function handleSelectTime(value: dayjs.Dayjs) {
open.value = false
}
const selectedTime = computed(() => {
const result = {
value: '',
label: '',
}
if (localState.value) {
const time = localState.value.format(timeFormat.value)
const [hours, minutes] = time.split(':')
result.value = `${hours}:${minutes}`
result.label = time
}
return result
})
const timeCellMaxWidth = computed(() => {
return {
[timeFormats[0]]: 'max-w-[65px]',
[timeFormats[1]]: 'max-w-[80px]',
[timeFormats[2]]: 'max-w-[110px]',
}[timeFormat.value]
[timeFormats[0]]: {
12: 'max-w-[85px]',
24: 'max-w-[65px]',
},
[timeFormats[1]]: {
12: 'max-w-[100px]',
24: 'max-w-[80px]',
},
[timeFormats[2]]: {
12: 'max-w-[130px]',
24: 'max-w-[110px]',
},
}[timeFormat.value][parseProp(column.value.meta).is12hrFormat ? 12 : 24]
})
const cellValue = computed(
() =>
localState.value?.format(parseProp(column.value.meta).is12hrFormat ? timeFormatsObj[timeFormat.value] : timeFormat.value) ??
'',
)
</script>
<template>
<div class="nc-cell-field group relative">
<div class="nc-cell-field relative">
<NcDropdown
:visible="isOpen"
:placement="isDatePicker ? 'bottomLeft' : 'bottomRight'"
@ -424,7 +440,7 @@ const timeCellMaxWidth = computed(() => {
>
<div
:title="localState?.format(dateTimeFormat)"
class="nc-date-picker ant-picker-input flex justify-between gap-2 relative group !w-auto"
class="nc-date-picker ant-picker-input flex justify-between gap-2 relative !w-auto"
>
<div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border w-[60%] max-w-[110px]"
@ -461,7 +477,7 @@ const timeCellMaxWidth = computed(() => {
>
<input
ref="timePickerRef"
:value="selectedTime.value ? `${selectedTime.label}` : ''"
:value="cellValue"
:placeholder="typeof placeholder === 'string' ? placeholder : placeholder?.time"
class="nc-time-input w-full !truncate border-transparent outline-none !text-current !bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="!!isMobileMode || isColDisabled"
@ -497,6 +513,7 @@ const timeCellMaxWidth = computed(() => {
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is12hr-format="!!parseProp(column.meta).is12hrFormat"
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
@ -506,17 +523,20 @@ const timeCellMaxWidth = computed(() => {
</NcDropdown>
<GeneralIcon
v-if="localState && (isExpandedForm || isForm || !isGrid)"
v-if="localState && (isExpandedForm || isForm || !isGrid || isEditColumn) && !readOnly"
icon="closeCircle"
class="h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-date-time-icon h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
.nc-cell-field {
&:hover .nc-clear-date-time-icon {
@apply visible;
}
}
</style>

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

@ -102,7 +102,7 @@ watch(isExpandedFormOpen, () => {
class="nc-cell-field outline-none py-1 border-none rounded-md w-full h-full"
type="number"
:step="precision"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder"
style="letter-spacing: 0.06rem"
@blur="editEnabled = false"
@keydown.down.stop="onKeyDown"

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

@ -10,8 +10,6 @@ const { modelValue, showValidationError = true } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const { showNull } = useGlobal()
const column = inject(ColumnInj)
@ -30,9 +28,7 @@ const isEdited = ref(false)
const durationType = computed(() => parseProp(column?.value?.meta)?.duration || 0)
const durationPlaceholder = computed(() =>
isEditColumn.value ? `(${t('labels.optional')})` : durationOptions[durationType.value].title,
)
const durationPlaceholder = computed(() => durationOptions[durationType.value].title)
const localState = computed({
get: () => convertMS2Duration(modelValue, durationType.value),

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

@ -81,7 +81,6 @@ watch(
:ref="focus"
v-model="vModel"
class="nc-cell-field w-full outline-none py-1"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

@ -53,7 +53,6 @@ const focus: VNodeRef = (el) =>
class="nc-cell-field outline-none px-1 border-none w-full h-full"
type="number"
step="0.1"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

@ -151,25 +151,33 @@ const openInOSM = () => {
v-if="isLoading"
:class="{ 'animate-infinite animate-spin text-gray-500': isLoading }"
/>
<a-button class="ml-2" @click="onClickSetCurrentLocation"
><component :is="iconMap.currentLocation" class="mr-2" />{{ $t('labels.currentLocation') }}</a-button
>
<a-button class="ml-2 !rounded-lg" @click="onClickSetCurrentLocation">
<div class="flex items-center gap-1">
<component :is="iconMap.currentLocation" />{{ $t('labels.currentLocation') }}
</div>
</a-button>
</div>
</a-form-item>
<a-form-item v-if="vModel">
<div class="mr-2 flex flex-row items-end gap-1 text-left">
<a-button @click="openInOSM"
><component :is="iconMap.openInNew" class="mr-2" />{{ $t('activity.map.openInOpenStreetMap') }}</a-button
>
<a-button @click="openInGoogleMaps"
><component :is="iconMap.openInNew" class="mr-2" />{{ $t('activity.map.openInGoogleMaps') }}</a-button
>
<a-button class="!rounded-lg" @click="openInOSM">
<div class="flex items-center gap-1">
<component :is="iconMap.openInNew" />{{ $t('activity.map.openInOpenStreetMap') }}
</div>
</a-button>
<a-button class="!rounded-lg" @click="openInGoogleMaps">
<div class="flex items-center gap-1">
<component :is="iconMap.openInNew" />{{ $t('activity.map.openInGoogleMaps') }}
</div>
</a-button>
</div>
</a-form-item>
<a-form-item>
<div class="ml-auto mr-2 w-auto">
<a-button type="text" @click="clear">{{ $t('general.cancel') }}</a-button>
<a-button type="primary" html-type="submit" data-testid="nc-geo-data-save">{{ $t('general.submit') }}</a-button>
<a-button type="text" class="!rounded-lg" @click="clear">{{ $t('general.cancel') }}</a-button>
<a-button type="primary" html-type="submit" data-testid="nc-geo-data-save" class="!rounded-lg">{{
$t('general.submit')
}}</a-button>
</div>
</a-form-item>
</a-form>
@ -179,7 +187,10 @@ const openInOSM = () => {
<style scoped lang="scss">
input[type='number']:focus {
@apply ring-transparent;
@apply ring-transparent shadow-selected;
}
input[type='number'] {
@apply !border-1 !pr-1 rounded-lg;
}
input[type='number'] {

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

@ -96,7 +96,6 @@ function onKeyDown(e: any) {
class="nc-cell-field outline-none py-1 border-none w-full h-full"
:type="inputType"
style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown="onKeyDown"
@keydown.down.stop

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

@ -19,6 +19,8 @@ const editEnabled = inject(EditModeInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false))
@ -133,6 +135,27 @@ onClickOutside(inputWrapperRef, (e) => {
watch(isExpanded, () => {
_isExpanded.value = isExpanded.value
})
const stopPropagation = (event: MouseEvent) => {
event.stopPropagation()
}
watch(inputWrapperRef, () => {
if (!isEditColumn.value) return
// stop event propogation in edit to prevent close edit modal on clicking expanded modal overlay
const modal = document.querySelector('.nc-json-expanded-modal') as HTMLElement
if (isExpanded.value && modal?.parentElement) {
modal.parentElement.addEventListener('click', stopPropagation)
modal.parentElement.addEventListener('mousedown', stopPropagation)
modal.parentElement.addEventListener('mouseup', stopPropagation)
} else if (modal?.parentElement) {
modal.parentElement.removeEventListener('click', stopPropagation)
modal.parentElement.removeEventListener('mousedown', stopPropagation)
modal.parentElement.removeEventListener('mouseup', stopPropagation)
}
})
</script>
<template>
@ -142,7 +165,7 @@ watch(isExpanded, () => {
:closable="false"
centered
:footer="null"
:wrap-class-name="isExpanded ? '!z-1051' : null"
:wrap-class-name="isExpanded ? '!z-1051 nc-json-expanded-modal' : 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>
@ -152,12 +175,21 @@ watch(isExpanded, () => {
<CilFullscreen v-else class="h-2.5" />
</a-button>
<div v-if="!isForm || isExpanded" class="flex flex-row my-1">
<a-button type="text" size="small" :onclick="clear"
<div v-if="!isForm || isExpanded" class="flex flex-row my-1 space-x-1">
<a-button type="text" size="small" class="!rounded-lg" @click="clear"
><div class="text-xs">{{ $t('general.cancel') }}</div></a-button
>
<a-button type="primary" size="small" :disabled="!!error || localValue === vModel" @click="onSave">
<a-button
:type="isEditColumn && !isExpanded ? 'text' : 'primary'"
size="small"
class="nc-save-json-value-btn !rounded-lg"
:class="{
'nc-edit-modal': isEditColumn && !isExpanded,
}"
:disabled="!!error || localValue === vModel"
@click="onSave"
>
<div class="text-xs">{{ $t('general.save') }}</div>
</a-button>
</div>
@ -170,7 +202,7 @@ watch(isExpanded, () => {
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
:hide-minimap="true"
:disable-deep-compare="true"
:auto-focus="!isForm"
:auto-focus="!isForm && !isEditColumn"
@update:model-value="localValue = $event"
@keydown.enter.stop
/>
@ -194,4 +226,10 @@ watch(isExpanded, () => {
.editor {
min-height: min(200px, 10vh);
}
.nc-save-json-value-btn {
&.nc-edit-modal:not(:disabled) {
@apply !text-brand-500 !hover:text-brand-600;
}
}
</style>

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

@ -469,7 +469,6 @@ const onFocus = () => {
v-model:value="vModel"
mode="multiple"
class="w-full overflow-hidden"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:bordered="false"
clear-icon
:show-search="!isMobileMode"

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

@ -145,7 +145,7 @@ const onTabPress = (e: KeyboardEvent) => {
v-model="vModel"
class="nc-cell-field w-full !border-none !outline-none focus:ring-0 py-1"
:type="inputType"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder"
@blur="onBlur"
@focus="onFocus"
@keydown.down.stop

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

@ -65,7 +65,6 @@ watch(
:ref="focus"
v-model="vModel"
class="nc-cell-field w-full outline-none py-1"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

@ -40,6 +40,8 @@ const rowHeight = inject(RowHeightInj, ref(1 as const))
const readOnlyCell = inject(ReadonlyInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false))
@ -220,7 +222,7 @@ if (isFormField.value) {
}
onMounted(() => {
if (fullMode.value || isFormField.value || isForm.value) {
if (fullMode.value || isFormField.value || isForm.value || isEditColumn.value) {
setEditorContent(vModel.value, true)
if (fullMode.value || isSurveyForm.value) {

10
packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue

@ -27,6 +27,8 @@ const props = withDefaults(defineProps<Props>(), {
const { editor, embedMode, isFormField, hiddenOptions } = toRefs(props)
const isEditColumn = inject(EditColumnInj, ref(false))
const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL'
})
@ -108,6 +110,7 @@ const showDivider = (options: RichTextBubbleMenuOptions[]) => {
'flex bg-gray-100 px-1 py-1': !isFormField,
'embed-mode': embedMode,
'full-mode': !embedMode,
'edit-column-mode': isEditColumn,
}"
>
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
@ -172,7 +175,7 @@ const showDivider = (options: RichTextBubbleMenuOptions[]) => {
<MdiFormatUnderline />
</NcButton>
</NcTooltip>
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<NcTooltip v-if="embedMode && !isEditColumn" :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -283,7 +286,10 @@ const showDivider = (options: RichTextBubbleMenuOptions[]) => {
<div class="divider"></div>
</template>
<NcTooltip v-if="embedMode && isOptionVisible(RichTextBubbleMenuOptions.blockQuote)" :placement="tooltipPlacement">
<NcTooltip
v-if="embedMode && !isEditColumn && isOptionVisible(RichTextBubbleMenuOptions.blockQuote)"
:placement="tooltipPlacement"
>
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton
size="small"

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

@ -387,7 +387,6 @@ const onFocus = () => {
v-model:value="vModel"
class="w-full overflow-hidden xs:min-h-12"
:class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:allow-clear="!column.rqd && editAllowed"
:bordered="false"
:open="isOpen && editAllowed"

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

@ -35,7 +35,6 @@ const focus: VNodeRef = (el) =>
:ref="focus"
v-model="vModel"
class="nc-cell-field h-full w-full outline-none py-1 bg-transparent"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

@ -247,7 +247,6 @@ watch(inputWrapperRef, () => {
:style="{
minHeight: isForm ? '117px' : `${height}px`,
}"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:disabled="readOnly"
@blur="editEnabled = false"
@keydown.alt.enter.stop

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn, isValidTimeFormat } from 'nocodb-sdk'
import { isSystemColumn } from 'nocodb-sdk'
interface Props {
modelValue?: string | null | undefined
@ -140,11 +140,10 @@ watch(editable, (nextValue) => {
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isTimeInvalid.value) ||
(isGrid.value && !showNull.value && !isTimeInvalid.value && !isSystemColumn(column.value) && active.value)
(isGrid.value && !showNull.value && !isTimeInvalid.value && !isSystemColumn(column.value) && active.value) ||
isEditColumn.value
) {
return 'HH:mm'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
return parseProp(column.value.meta).is12hrFormat ? 'hh:mm AM' : 'HH:mm'
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isTimeInvalid.value) {
@ -212,6 +211,12 @@ const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
default:
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
const targetEl = e.target as HTMLInputElement
const value = targetEl.value
nextTick(() => {
targetEl.value = value
})
}
}
}
@ -256,12 +261,18 @@ const handleUpdateValue = (e: Event) => {
return
}
if (targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
targetValue = parseProp(column.value.meta).is12hrFormat
? targetValue
.trim()
.toUpperCase()
.replace(/(AM|PM)$/, ' $1')
.replace(/\s+/g, ' ')
: targetValue.trim()
if (isValidTimeFormat(targetValue, 'HH:mm')) {
tempDate.value = dayjs(`${dayjs().format('YYYY-MM-DD')} ${targetValue}`)
const parsedDate = dayjs(targetValue, parseProp(column.value.meta).is12hrFormat ? 'hh:mm A' : 'HH:mm')
if (parsedDate.isValid()) {
tempDate.value = dayjs(`${dayjs().format('YYYY-MM-DD')} ${parsedDate.format('HH:mm')}`)
}
}
@ -284,6 +295,8 @@ function handleSelectTime(value?: dayjs.Dayjs) {
open.value = false
}
const cellValue = computed(() => localState.value?.format(parseProp(column.value.meta).is12hrFormat ? 'hh:mm A' : 'HH:mm') ?? '')
</script>
<template>
@ -297,12 +310,12 @@ function handleSelectTime(value?: dayjs.Dayjs) {
>
<div
:title="localState?.format('HH:mm')"
class="nc-time-picker h-full flex items-center justify-between ant-picker-input relative group"
class="nc-time-picker h-full flex items-center justify-between ant-picker-input relative"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format('HH:mm') ?? ''"
:value="cellValue"
:placeholder="placeholder"
class="nc-time-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@ -316,19 +329,20 @@ function handleSelectTime(value?: dayjs.Dayjs) {
/>
<GeneralIcon
v-if="localState"
v-if="localState && !readOnly"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-time-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectTime()"
/>
</div>
<template #overlay>
<div class="w-[72px]">
<div class="min-w-[72px]">
<NcTimeSelector
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is12hr-format="!!parseProp(column.meta).is12hrFormat"
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
@ -337,3 +351,11 @@ function handleSelectTime(value?: dayjs.Dayjs) {
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>
<style scoped>
.nc-cell-field {
&:hover .nc-clear-time-icon {
@apply visible;
}
}
</style>

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

@ -85,7 +85,6 @@ watch(
v-if="!readOnly && editEnabled"
:ref="focus"
v-model="vModel"
:placeholder="isEditColumn ? $t('labels.enterDefaultUrlOptional') : ''"
class="nc-cell-field outline-none w-full py-1 bg-transparent h-full"
@blur="editEnabled = false"
@keydown.down.stop

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

@ -127,11 +127,10 @@ watch(editable, (nextValue) => {
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isYearInvalid.value) ||
(isGrid.value && !showNull.value && !isYearInvalid.value && !isSystemColumn(column.value) && active.value)
(isGrid.value && !showNull.value && !isYearInvalid.value && !isSystemColumn(column.value) && active.value) ||
isEditColumn.value
) {
return 'YYYY'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isYearInvalid.value) {
@ -265,10 +264,7 @@ function handleSelectDate(value?: dayjs.Dayjs) {
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:overlay-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''} !min-w-[260px]`"
>
<div
:title="localState?.format('YYYY')"
class="nc-year-picker flex items-center justify-between ant-picker-input relative group"
>
<div :title="localState?.format('YYYY')" class="nc-year-picker flex items-center justify-between ant-picker-input relative">
<input
ref="datePickerRef"
type="text"
@ -285,9 +281,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
/>
<GeneralIcon
v-if="localState"
v-if="localState && !readOnly"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-year-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
@ -309,7 +305,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
.nc-cell-field {
&:hover .nc-clear-year-icon {
@apply visible;
}
}
</style>

197
packages/nc-gui/components/general/AdvanceColorPicker.vue

@ -0,0 +1,197 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import windiColors from 'windicss/colors'
import { themeV3Colors } from '../../utils/colorsUtils'
interface Props {
modelValue?: string | any
isOpen?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isOpen: false,
})
const emit = defineEmits(['input', 'closeModal'])
const { isOpen } = toRefs(props)
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
emit('input', val || null)
},
})
const showActiveColorTab = ref<boolean>(false)
const picked = ref<string>(props.modelValue || enumColor.light[0])
const defaultColors = computed<string[][]>(() => {
const colors = [
'gray',
'red',
'green',
'yellow',
'orange',
'pink',
'maroon',
'purple',
'blue',
] as (keyof typeof themeV3Colors)[] & (keyof typeof windiColors)[]
const allColors = []
for (const color of colors) {
if (themeV3Colors[color]) {
allColors.push(color === 'gray' ? Object.values(themeV3Colors[color]).slice(1) : Object.values(themeV3Colors[color]))
} else if (windiColors[color]) {
allColors.push(Object.values(windiColors[color]))
}
}
return allColors
})
const localIsDefaultColorTab = ref(true)
const isDefaultColorTab = computed({
get: () => {
if (showActiveColorTab.value && vModel.value) {
for (const colorGrp of defaultColors.value) {
if (colorGrp.includes(vModel.value)) {
return true
}
}
return false
}
return localIsDefaultColorTab.value
},
set: (val: boolean) => {
localIsDefaultColorTab.value = val
if (showActiveColorTab.value) {
showActiveColorTab.value = false
}
},
})
const selectColor = (color: string, closeModal = false) => {
picked.value = color
if (closeModal) {
emit('closeModal')
}
}
const compare = (colorA: string, colorB: string) => {
if (!colorA || !colorB) return false
return colorA.toLowerCase() === colorB.toLowerCase() || colorA.toLowerCase() === tinycolor(colorB).toHex8String().toLowerCase()
}
watch(picked, (n, _o) => {
vModel.value = n
})
watch(
isOpen,
(newValue) => {
if (newValue) {
showActiveColorTab.value = true
}
},
{
immediate: true,
},
)
</script>
<template>
<div class="nc-advance-color-picker w-[336px] pt-2" click.stop>
<NcTabs v-model:activeKey="isDefaultColorTab" class="nc-advance-color-picker-tab w-full">
<a-tab-pane :key="true">
<template #tab>
<div class="tab" data-testid="nc-default-colors-tab">Default colors</div>
</template>
<div class="h-full p-2">
<div class="flex flex-col gap-1">
<div v-for="(colorGroup, i) of defaultColors" :key="i" class="flex">
<div v-for="(color, j) of colorGroup" :key="`color-${i}-${j}`" class="p-1 rounded-md flex h-8 hover:bg-gray-200">
<button
class="color-selector"
:class="{ selected: compare(picked, color) }"
:style="{
backgroundColor: `${color}`,
}"
@click="selectColor(color, true)"
></button>
</div>
</div>
</div>
</div>
</a-tab-pane>
<a-tab-pane :key="false">
<template #tab>
<div class="tab" data-testid="nc-custom-colors-tab">
<div>Custom colours</div>
</div>
</template>
<div class="h-full p-2">
<LazyGeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
</div>
</a-tab-pane>
</NcTabs>
</div>
</template>
<style lang="scss" scoped>
.color-picker {
@apply flex flex-col items-center justify-center bg-white p-2.5;
}
.color-picker-row {
@apply flex flex-row space-x-1;
}
.color-selector {
@apply h-6 w-6 rounded;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
}
.color-selector:hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector:focus,
.color-selector.selected,
.nc-more-colors-trigger:focus {
outline: none;
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
:deep(.vc-chrome-toggle-icon) {
@apply !ml-3;
}
:deep(.ant-tabs) {
@apply !overflow-visible;
.ant-tabs-nav {
@apply px-1;
.ant-tabs-nav-list {
@apply w-[99%] mx-auto gap-6;
.ant-tabs-tab {
@apply flex-1 flex items-center justify-center pt-2 pb-2 text-xs font-semibold;
& + .ant-tabs-tab {
@apply !ml-0;
}
}
}
}
.ant-tabs-content-holder {
.ant-tabs-content {
@apply h-full;
}
}
}
</style>

21
packages/nc-gui/components/nc/Switch.vue

@ -1,7 +1,11 @@
<script lang="ts" setup>
const props = withDefaults(defineProps<{ checked: boolean; disabled?: boolean; size?: 'default' | 'small' | 'xsmall' }>(), {
size: 'small',
})
const props = withDefaults(
defineProps<{ checked: boolean; disabled?: boolean; size?: 'default' | 'small' | 'xsmall'; placement: 'left' | 'right' }>(),
{
size: 'small',
placement: 'left',
},
)
const emit = defineEmits(['change', 'update:checked'])
@ -9,12 +13,19 @@ const checked = useVModel(props, 'checked', emit)
const switchSize = computed(() => (['default', 'small'].includes(props.size) ? props.size : undefined))
const onChange = (e: boolean) => {
const onChange = (e: boolean, updateValue = false) => {
if (updateValue) {
checked.value = e
}
emit('change', e)
}
</script>
<template>
<span v-if="placement === 'right' && $slots.default" class="cursor-pointer pr-2" @click="onChange(!checked, true)">
<slot />
</span>
<a-switch
v-model:checked="checked"
:disabled="disabled"
@ -27,7 +38,7 @@ const onChange = (e: boolean) => {
@change="onChange"
>
</a-switch>
<span v-if="$slots.default" class="cursor-pointer pl-2" @click="checked = !checked">
<span v-if="placement === 'left' && $slots.default" class="cursor-pointer pl-2" @click="onChange(!checked, true)">
<slot />
</span>
</template>

4
packages/nc-gui/components/nc/TimeSelector.vue

@ -42,7 +42,7 @@ const handleSelectTime = (time: dayjs.Dayjs) => {
// TODO: 12hr time format & regular time picker
const timeOptions = computed(() => {
return Array.from({ length: is12hrFormat.value ? 12 : 24 }).flatMap((_, h) => {
return Array.from({ length: 24 }).flatMap((_, h) => {
return (isMinGranularityPicker.value ? [0, minGranularity.value] : Array.from({ length: 60 })).map((_m, m) => {
const time = dayjs()
.set('hour', h)
@ -89,7 +89,7 @@ onMounted(() => {
:data-testid="`time-option-${time.format('HH:mm')}`"
@click="handleSelectTime(time)"
>
{{ time.format('HH:mm') }}
{{ time.format(is12hrFormat ? 'hh:mm A' : 'HH:mm') }}
</div>
</div>
<div v-else></div>

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

@ -32,7 +32,7 @@ vModel.value.au = !!vModel.value.au */
</script>
<template>
<div class="p-4 border-[0.1px] radius-1 rounded-md border-grey w-full flex flex-col gap-2">
<div class="p-4 border-[0.1px] radius-1 rounded-lg border-grey w-full flex flex-col gap-2">
<template v-if="props.advancedDbOptions">
<div class="flex justify-between w-full gap-1">
<a-form-item label="NN">
@ -72,7 +72,11 @@ 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 " class="!mt-0.5" @change="onDataTypeChange">
<a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type" @change="onDataTypeChange">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="type in dataTypes" :key="type" :value="type">
<div class="flex gap-2 items-center justify-between">
{{ type }}
@ -85,19 +89,14 @@ vModel.value.au = !!vModel.value.au */
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input
v-model:value="vModel.dtxp"
class="!rounded-md !mt-0.5"
class="!rounded-lg"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)"
@input="onAlter"
/>
</a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input
v-model:value="vModel.dtxs"
class="!rounded-md !mt-0.5"
:disabled="!sqlUi.columnEditable(vModel)"
@input="onAlter"
/>
<a-input v-model:value="vModel.dtxs" class="!rounded-lg" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item>
<LazySmartsheetColumnPgBinaryOptions v-if="isPg(meta?.source_id) && vModel.dt === 'bytea'" v-model:value="vModel" />

133
packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes, isVirtualCol } from 'nocodb-sdk'
const props = defineProps<{
modelValue: any
@ -17,18 +16,17 @@ const { setAdditionalValidations, validateInfos, column } = useColumnCreateStore
const { t } = useI18n()
const columnsAllowedAsBarcodeValue = computed<SelectProps['options']>(() => {
return fields.value
?.filter(
(el) =>
el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
})
const columnsAllowedAsBarcodeValue = computed<ColumnType[]>(() => {
return (
fields.value
?.filter(
(el) =>
el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return metaColumnById.value[field.fk_column_id!]
}) || []
)
})
const supportedBarcodeFormats = [
@ -41,8 +39,8 @@ const supportedBarcodeFormats = [
{ value: 'CODE39', label: 'CODE39' },
{ value: 'ITF14', label: 'ITF-14' },
{ value: 'MSI', label: 'MSI' },
{ value: 'PHARMACODE', label: 'pharmacode' },
{ value: 'CODABAR', label: 'codabar' },
{ value: 'PHARMACODE', label: 'PHARMACODE' },
{ value: 'CODABAR', label: 'CODABAR' },
]
onMounted(() => {
@ -52,13 +50,12 @@ onMounted(() => {
...vModel.value.meta,
}
vModel.value.fk_barcode_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id ||
columnsAllowedAsBarcodeValue.value?.[0]?.value
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id || columnsAllowedAsBarcodeValue.value?.[0]?.id
})
watch(columnsAllowedAsBarcodeValue, (newColumnsAllowedAsBarcodeValue) => {
if (vModel.value.fk_barcode_value_column_id === null) {
vModel.value.fk_barcode_value_column_id = newColumnsAllowedAsBarcodeValue?.[0]?.value
vModel.value.fk_barcode_value_column_id = newColumnsAllowedAsBarcodeValue?.[0]?.id
}
})
@ -68,48 +65,66 @@ setAdditionalValidations({
})
const showBarcodeValueColumnInfoIcon = computed(() => !columnsAllowedAsBarcodeValue.value?.length)
const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column,
})
</script>
<template>
<a-row>
<a-col :span="24">
<a-form-item
class="flex pb-2 nc-barcode-value-column-select flex-row"
:label="$t('labels.barcodeValueColumn')"
v-bind="validateInfos.fk_barcode_value_column_id"
>
<div class="flex w-1/2 flex-row items-center">
<a-select
v-model:value="vModel.fk_barcode_value_column_id"
:options="columnsAllowedAsBarcodeValue"
:placeholder="$t('placeholder.barcodeColumn')"
:not-found-content="$t('placeholder.notFoundContent')"
@click.stop
/>
<div v-if="showBarcodeValueColumnInfoIcon" class="pl-2">
<a-tooltip placement="bottom">
<template #title>
<span>
{{ $t('msg.validColumnsForBarCode') }}
</span>
</template>
<component :is="iconMap.info" class="cursor-pointer" />
</a-tooltip>
</div>
</div>
</a-form-item>
<a-form-item
class="flex w-1/2 pb-2 nc-barcode-format-select"
:label="$t('labels.barcodeFormat')"
v-bind="validateInfos.barcode_format"
>
<div class="flex flex-col gap-4">
<a-form-item
class="flex pb-2 nc-barcode-value-column-select flex-row"
:label="`${$t('placeholder.value')} ${t('objects.field').toLowerCase()}`"
v-bind="validateInfos.fk_barcode_value_column_id"
>
<div class="flex flex-row items-center">
<a-select
v-model:value="vModel.meta.barcodeFormat"
:options="supportedBarcodeFormats"
:placeholder="$t('placeholder.selectBarcodeFormat')"
v-model:value="vModel.fk_barcode_value_column_id"
:placeholder="$t('placeholder.barcodeColumn')"
:not-found-content="$t('placeholder.notFoundContent')"
@click.stop
/>
</a-form-item>
</a-col>
</a-row>
>
<template #suffixIcon> <GeneralIcon icon="arrowDown" class="text-gray-700" /> </template>
<a-select-option v-for="(option, index) of columnsAllowedAsBarcodeValue" :key="index" :value="option.id">
<div class="w-full flex gap-2 truncate items-center justify-between" :data-testid="`nc-barcode-${option.title}`">
<div class="inline-flex items-center gap-2 flex-1 truncate">
<component :is="cellIcon(option)" :column-meta="option" class="!mx-0" />
<div class="truncate flex-1">{{ option.title }}</div>
</div>
<component
:is="iconMap.check"
v-if="vModel.fk_barcode_value_column_id === option.id"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
<div v-if="showBarcodeValueColumnInfoIcon" class="pl-2">
<a-tooltip placement="bottom">
<template #title>
<span>
{{ $t('msg.validColumnsForBarCode') }}
</span>
</template>
<component :is="iconMap.info" class="cursor-pointer" />
</a-tooltip>
</div>
</div>
</a-form-item>
<a-form-item class="flexp nc-barcode-format-select" :label="$t('general.format')" v-bind="validateInfos.barcode_format">
<a-select
v-model:value="vModel.meta.barcodeFormat"
:options="supportedBarcodeFormats"
:placeholder="$t('placeholder.selectBarcodeFormat')"
@click.stop
>
<template #suffixIcon> <GeneralIcon icon="arrowDown" class="text-gray-700" /> </template
></a-select>
</a-form-item>
</div>
</template>

79
packages/nc-gui/components/smartsheet/column/CheckboxOptions.vue

@ -46,6 +46,8 @@ const picked = computed({
},
})
const isOpenColorPicker = ref(false)
// set default value
vModel.value.meta = {
icon: {
@ -73,26 +75,19 @@ watch(
</script>
<template>
<a-row>
<a-col :span="24">
<a-form-item label="Icon">
<a-row :gutter="8">
<a-col :span="12">
<a-form-item :label="$t('labels.icon')">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52" dropdown-class-name="nc-dropdown-checkbox-icon">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<div class="flex gap-2 w-full truncate items-center">
<div class="flex-1 truncate">
<component
:is="getMdiIcon(icon.checked)"
class="mx-1"
:style="{
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.unchecked)"
:style="{
color: vModel.meta.color,
}"
/>
<div class="flex-1 flex items-center text-gray-700 gap-2 children:(h-4 w-4)">
<component :is="getMdiIcon(icon.checked)" />
<component :is="getMdiIcon(icon.unchecked)" />
</div>
<component
@ -106,15 +101,49 @@ watch(
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-col :span="12">
<a-form-item :label="$t('general.colour')">
<NcDropdown
v-model:visible="isOpenColorPicker"
placement="bottomRight"
:auto-close="false"
class="nc-color-picker-dropdown-trigger"
>
<div
class="flex-1 border-1 border-gray-300 rounded-lg h-8 px-[11px] flex items-center justify-between transition-all cursor-pointer"
:class="{
'border-brand-500 shadow-selected': isOpenColorPicker,
}"
>
<div class="flex-1 flex items-center gap-2 children:(h-4 w-4)">
<component
:is="getMdiIcon(iconList[vModel.meta.iconIdx].checked)"
:style="{
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(iconList[vModel.meta.iconIdx].unchecked)"
:style="{
color: vModel.meta.color,
}"
/>
</div>
<a-row class="w-full justify-center">
<LazyGeneralColorPicker
v-model="picked"
:row-size="8"
:colors="['#FF94B6', '#6A8D9D', '#6DAE42', '#4AC0BF', '#905FB3', '#FF8320', '#6BCC72', '#FF4138']"
@input="(el:string)=>vModel.meta.color=el"
/>
<GeneralIcon icon="arrowDown" class="text-gray-700 h-4 w-4" />
</div>
<template #overlay>
<div>
<LazyGeneralAdvanceColorPicker
v-model="picked"
:is-open="isOpenColorPicker"
@input="(el:string)=>vModel.meta.color=el"
></LazyGeneralAdvanceColorPicker>
</div>
</template>
</NcDropdown>
</a-form-item>
</a-col>
</a-row>
</template>

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

@ -86,6 +86,8 @@ currencyLocales().then((locales) => {
:disabled="isMoney && isPg"
dropdown-class-name="nc-dropdown-currency-cell-locale"
>
<template #suffixIcon> <GeneralIcon icon="arrowDown" class="text-gray-700" /> </template>
<a-select-option v-for="(currencyLocale, i) of currencyLocaleList" :key="i" :value="currencyLocale.value">
<div class="flex gap-2 w-full truncate items-center">
<NcTooltip show-on-truncate-only class="flex-1 truncate">
@ -115,6 +117,8 @@ currencyLocales().then((locales) => {
:disabled="isMoney && isPg"
dropdown-class-name="nc-dropdown-currency-cell-code"
>
<template #suffixIcon> <GeneralIcon icon="arrowDown" class="text-gray-700" /> </template>
<a-select-option v-for="(currencyCode, i) of currencyList" :key="i" :value="currencyCode">
<div class="flex gap-2 w-full justify-between items-center">
{{ currencyCode }}

7
packages/nc-gui/components/smartsheet/column/DateOptions.vue

@ -16,15 +16,18 @@ if (!vModel.value.meta?.date_format) {
</script>
<template>
<a-form-item :label="$t('labels.dateFormat')">
<a-form-item>
<a-select
v-model:value="vModel.meta.date_format"
show-search
class="nc-date-select"
dropdown-class-name="nc-dropdown-date-format"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(format, i) of [...dateFormats, ...dateMonthFormats]" :key="i" :value="format">
<div class="flex gap-2 justify-between items-center">
<div class="w-full flex gap-2 justify-between items-center">
{{ format }}
<component
:is="iconMap.check"

95
packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue

@ -18,37 +18,72 @@ if (!vModel.value.meta?.time_format) {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.time_format = timeFormats[0]
}
if (vModel.value.meta?.is12hrFormat === undefined) {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.is12hrFormat = false
}
</script>
<template>
<a-form-item :label="$t('labels.dateFormat')">
<a-select v-model:value="vModel.meta.date_format" class="nc-date-select" dropdown-class-name="nc-dropdown-date-format">
<a-select-option v-for="(format, i) of dateFormats" :key="i" :value="format">
<div class="flex gap-2 w-full justify-between items-center">
{{ format }}
<component
:is="iconMap.check"
v-if="vModel.meta.date_format === format"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.timeFormat')">
<a-select v-model:value="vModel.meta.time_format" class="nc-time-select" dropdown-class-name="nc-dropdown-time-format">
<a-select-option v-for="(format, i) of timeFormats" :key="i" :value="format">
<div class="flex gap-2 w-full justify-between items-center" :data-testid="`nc-time-${format}`">
{{ format }}
<component
:is="iconMap.check"
v-if="vModel.meta.time_format === format"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2 children:flex-1">
<a-form-item>
<a-select v-model:value="vModel.meta.date_format" class="nc-date-select" dropdown-class-name="nc-dropdown-date-format">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(format, i) of dateFormats" :key="i" :value="format">
<div class="flex gap-2 w-full justify-between items-center">
{{ format }}
<component
:is="iconMap.check"
v-if="vModel.meta.date_format === format"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-select v-model:value="vModel.meta.time_format" class="nc-time-select" dropdown-class-name="nc-dropdown-time-format">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(format, i) of timeFormats" :key="i" :value="format">
<div class="flex gap-2 w-full justify-between items-center" :data-testid="`nc-time-${format}`">
{{ format }}
<component
:is="iconMap.check"
v-if="vModel.meta.time_format === format"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
<a-form-item>
<a-radio-group v-if="vModel.meta" v-model:value="vModel.meta.is12hrFormat" class="nc-time-form-layout">
<a-radio :value="true">12 Hrs</a-radio>
<a-radio :value="false">24 Hrs</a-radio>
</a-radio-group>
</a-form-item>
</div>
</template>
<style lang="scss" scoped>
:deep(.nc-time-form-layout) {
@apply flex justify-between gap-2 children:(flex-1 m-0 px-2 py-1 border-1 border-gray-200 rounded-lg);
.ant-radio-wrapper {
@apply transition-all;
&.ant-radio-wrapper-checked {
@apply border-brand-500;
}
}
}
</style>

3
packages/nc-gui/components/smartsheet/column/DecimalOptions.vue

@ -45,6 +45,9 @@ const onPrecisionChange = (value: number) => {
dropdown-class-name="nc-dropdown-decimal-format"
@change="onPrecisionChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(format, i) of precisionFormats" :key="i" :value="format">
<div class="flex gap-2 w-full justify-between items-center">
{{ (precisionFormatsDisplay as any)[format] }}

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

@ -1,14 +1,17 @@
<script lang="ts" setup>
import { UITypes } from 'nocodb-sdk'
const props = defineProps<{
value: any
isVisibleDefaultValueInput: boolean
}>()
const emits = defineEmits(['update:value'])
const emits = defineEmits(['update:value', 'update:isVisibleDefaultValueInput'])
provide(EditColumnInj, ref(true))
const vModel = useVModel(props, 'value', emits)
const isVisibleDefaultValueInput = useVModel(props, 'isVisibleDefaultValueInput', emits)
const rowRef = ref({
row: {},
oldRow: {},
@ -41,29 +44,49 @@ watch(
</script>
<template>
<div class="!my-3 text-xs">{{ $t('placeholder.defaultValue') }}</div>
<div class="flex flex-row gap-2">
<div
class="border-1 flex items-center w-full px-3 my-[-4px] border-gray-300 rounded-md sm:min-h-[32px] xs:min-h-13 flex items-center focus-within:(border-brand-500 shadow-none ring-0)"
:class="{
'!border-brand-500': editEnabled,
}"
<div v-if="!isVisibleDefaultValueInput">
<NcButton
size="small"
type="text"
class="!text-gray-500 !hover:text-gray-700"
data-testid="nc-show-default-value-btn"
@click.stop="isVisibleDefaultValueInput = true"
>
<LazySmartsheetCell
:edit-enabled="true"
:model-value="cdfValue"
:column="vModel"
class="!border-none h-auto my-auto"
@update:cdf="updateCdfValue"
@update:edit-enabled="editEnabled = $event"
@click="editEnabled = true"
/>
<component
:is="iconMap.close"
v-if="![UITypes.Year, UITypes.SingleSelect, UITypes.MultiSelect].includes(vModel.uidt)"
class="w-4 h-4 cursor-pointer rounded-full !text-black-500 text-gray-500 cursor-pointer hover:bg-gray-50"
@click="updateCdfValue(null)"
/>
<div class="flex items-center gap-2">
<span>{{ $t('general.set') }} {{ $t('placeholder.defaultValue').toLowerCase() }}</span>
<GeneralIcon icon="plus" class="flex-none h-4 w-4" />
</div>
</NcButton>
</div>
<div v-else>
<div class="w-full flex items-center gap-2 mb-2">
<div class="text-small leading-[18px] flex-1 text-gray-700">{{ $t('placeholder.defaultValue') }}</div>
</div>
<div class="flex flex-row gap-2">
<div
class="nc-default-value-wrapper border-1 flex items-center w-full px-3 border-gray-300 rounded-lg sm:min-h-[32px] xs:min-h-13 flex items-center focus-within:(border-brand-500 shadow-selected ring-0) transition-all duration-0.3s"
>
<LazySmartsheetCell
:edit-enabled="true"
:model-value="cdfValue"
:column="vModel"
class="!border-none h-auto my-auto"
@update:cdf="updateCdfValue"
@update:edit-enabled="editEnabled = $event"
@click="editEnabled = true"
/>
<component
:is="iconMap.close"
v-if="
![UITypes.Year, UITypes.Date, UITypes.Time, UITypes.DateTime, UITypes.SingleSelect, UITypes.MultiSelect].includes(
vModel.uidt,
)
"
class="w-4 h-4 cursor-pointer rounded-full !text-black-500 text-gray-500 cursor-pointer hover:bg-gray-50"
@click="updateCdfValue(null)"
/>
</div>
</div>
</div>
</template>

12
packages/nc-gui/components/smartsheet/column/DurationOptions.vue

@ -11,7 +11,7 @@ const durationOptionList =
durationOptions.map((o) => ({
...o,
// h:mm:ss (e.g. 3:45, 1:23:40)
title: `${o.title} ${o.example}`,
title: `${o.title}`,
})) || []
// set default value
@ -24,14 +24,12 @@ vModel.value.meta = {
<template>
<a-row>
<a-col :span="24">
<span class="prose-sm mt-2">{{ $t('labels.durationInfo') }}</span>
</a-col>
<a-col :span="24">
<a-form-item :label="$t('labels.durationFormat')">
<a-form-item :label="$t('general.format')">
<a-select v-model:value="vModel.meta.duration" class="w-52" dropdown-class-name="nc-dropdown-duration-option">
<template #suffixIcon> <GeneralIcon icon="arrowDown" class="text-gray-700" /> </template>
<a-select-option v-for="(duration, i) of durationOptionList" :key="i" :value="duration.id">
<div class="flex gap-2 w-full truncate items-center">
<div class="flex gap-2 w-full truncate items-center" :data-testid="duration.title">
<NcTooltip show-on-truncate-only class="flex-1 truncate">
<template #title> {{ duration.title }}</template>
{{ duration.title }}

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

@ -1,7 +1,8 @@
<script lang="ts" setup>
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSelfReferencingTableColumn, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { UITypes, UITypesName, isLinksOrLTAR, isSelfReferencingTableColumn, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier'
@ -22,7 +23,7 @@ const props = defineProps<{
const emit = defineEmits(['submit', 'cancel', 'mounted', 'add', 'update'])
const { formState, generateNewColumnMeta, addOrUpdate, onAlter, onUidtOrIdTypeChange, validateInfos, isEdit } =
const { formState, generateNewColumnMeta, addOrUpdate, onAlter, onUidtOrIdTypeChange, validateInfos, isEdit, disableSubmitBtn } =
useColumnCreateStoreOrThrow()
const { getMeta } = useMetas()
@ -57,6 +58,23 @@ const advancedOptions = ref(false)
const mounted = ref(false)
const showDefaultValueInput = ref(false)
const showHoverEffectOnSelectedType = ref(true)
const isVisibleDefaultValueInput = computed({
get: () => {
if (formState.value.cdf && !showDefaultValueInput.value) {
showDefaultValueInput.value = true
}
return formState.value.cdf !== null || showDefaultValueInput.value
},
set: (value: boolean) => {
showDefaultValueInput.value = value
},
})
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [
@ -85,7 +103,12 @@ const showDeprecated = ref(false)
const uiTypesOptions = computed<typeof uiTypes>(() => {
return [
...uiTypes
.filter((t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual) && (!t.deprecated || showDeprecated.value))
.filter(
(t) =>
geoDataToggleCondition(t) &&
(!isEdit.value || !t.virtual || t.name === formState.value.uidt) &&
(!t.deprecated || showDeprecated.value),
)
.filter((t) => !(t.name === UITypes.SpecificDBType && isXcdbBase(meta.value?.source_id))),
...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk)
? [
@ -99,6 +122,11 @@ const uiTypesOptions = computed<typeof uiTypes>(() => {
]
})
const onSelectType = (uidt: UITypes) => {
formState.value.uidt = uidt
onUidtOrIdTypeChange()
}
const reloadMetaAndData = async () => {
await getMeta(meta.value?.id as string, true)
@ -147,7 +175,7 @@ watchEffect(() => {
onMounted(() => {
if (!isEdit.value) {
generateNewColumnMeta()
generateNewColumnMeta(true)
} else {
if (formState.value.pk) {
message.info(t('msg.info.editingPKnotSupported'))
@ -212,12 +240,19 @@ const onDropdownChange = (value: boolean) => {
if (value) {
isColumnTypeOpen.value = value
} else {
showHoverEffectOnSelectedType.value = true
setTimeout(() => {
isColumnTypeOpen.value = value
}, 300)
}
}
const handleResetHoverEffect = () => {
if (!showHoverEffectOnSelectedType.value) return
showHoverEffectOnSelectedType.value = false
}
if (props.fromTableExplorer) {
watch(
formState,
@ -227,6 +262,13 @@ if (props.fromTableExplorer) {
{ deep: true },
)
}
const submitBtnLabel = computed(() => {
return {
label: `${isEdit.value && !props.columnLabel ? t('general.update') : t('general.save')} ${columnLabel.value}`,
loadingLabel: `${isEdit.value && !props.columnLabel ? t('general.updating') : t('general.saving')} ${columnLabel.value}`,
}
})
</script>
<template>
@ -234,229 +276,318 @@ if (props.fromTableExplorer) {
class="overflow-auto"
:class="{
'bg-white': !props.fromTableExplorer,
'w-[400px]': !props.embedMode,
'!w-146': isTextArea(formState) && formState.meta?.richMode,
'w-[384px]': !props.embedMode,
'!w-116 overflow-visible': formState.uidt === UITypes.Formula && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee,
'shadow-lg border-1 border-gray-200 shadow-gray-300 rounded-xl p-6': !embedMode,
'!w-[600px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'shadow-lg border-1 border-gray-200 shadow-gray-300 rounded-xl p-5': !embedMode,
}"
@keydown="handleEscape"
@click.stop
>
<a-form v-model="formState" no-style name="column-create-or-edit" layout="vertical" data-testid="add-or-edit-column">
<div class="flex flex-col gap-2">
<a-form-item v-if="isFieldsTab" v-bind="validateInfos.title" class="flex flex-grow">
<div
class="flex flex-grow px-2 py-1 items-center rounded-md bg-gray-100 focus:bg-gray-100 outline-none"
style="outline-style: solid; outline-width: thin"
>
<input
ref="antInput"
v-model="formState.title"
:disabled="readOnly"
class="flex flex-grow nc-fields-input text-lg font-bold outline-none bg-inherit"
:contenteditable="true"
/>
</div>
</a-form-item>
<a-form-item
v-if="!props.hideTitle && !isFieldsTab"
:label="`${columnLabel} ${$t('general.name')}`"
v-bind="validateInfos.title"
:required="false"
<a-form
v-model="formState"
no-style
name="column-create-or-edit"
layout="vertical"
data-testid="add-or-edit-column"
class="flex flex-col gap-4"
>
<a-form-item v-if="isFieldsTab" v-bind="validateInfos.title" class="flex flex-grow">
<div
class="flex flex-grow px-2 py-1 items-center rounded-md bg-gray-100 focus:bg-gray-100 outline-none"
style="outline-style: solid; outline-width: thin"
>
<a-input
<input
ref="antInput"
v-model:value="formState.title"
class="nc-column-name-input !rounded-md !mt-1"
:disabled="isKanban || readOnly"
@input="onAlter(8)"
v-model="formState.title"
:disabled="readOnly"
class="flex flex-grow nc-fields-input text-lg font-bold outline-none bg-inherit"
:contenteditable="true"
/>
</a-form-item>
</div>
</a-form-item>
<a-form-item v-if="!props.hideTitle && !isFieldsTab" v-bind="validateInfos.title" :required="false" class="!mb-0">
<a-input
ref="antInput"
v-model:value="formState.title"
class="nc-column-name-input !rounded-lg"
:disabled="isKanban || readOnly"
@input="onAlter(8)"
/>
</a-form-item>
<div class="flex items-center gap-1">
<template v-if="!props.hideType && !formState.uidt">
<SmartsheetColumnUITypesOptionsWithSearch :options="uiTypesOptions" @selected="onSelectType" />
</template>
<div class="flex items-center gap-1">
<a-form-item
v-if="!props.hideType && !(isEdit && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
class="flex-1"
:label="`${columnLabel} ${$t('general.type')}`"
<a-form-item v-else-if="!props.hideType" class="flex-1">
<a-select
v-model:value="formState.uidt"
show-search
class="nc-column-type-input !rounded-lg"
:disabled="isKanban || readOnly || (isEdit && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
dropdown-class-name="nc-dropdown-column-type border-1 !rounded-lg border-gray-200"
@dropdown-visible-change="onDropdownChange"
@change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated"
>
<div class="h-1 w-full"></div>
<a-select
v-model:value="formState.uidt"
show-search
class="nc-column-type-input !rounded-md"
:disabled="isKanban || readOnly"
dropdown-class-name="nc-dropdown-column-type border-1 !rounded-md border-gray-200"
@dropdown-visible-change="onDropdownChange"
@change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated"
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option
v-for="opt of uiTypesOptions"
:key="opt.name"
:value="opt.name"
v-bind="validateInfos.uidt"
:class="{
'ant-select-item-option-active-selected': showHoverEffectOnSelectedType && formState.uidt === opt.name,
}"
@mouseover="handleResetHoverEffect"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="w-full flex gap-2 items-center justify-between" :data-testid="opt.name">
<div class="flex gap-2 items-center">
<component :is="opt.icon" class="text-gray-700 w-4 h-4" />
<div class="flex-1">{{ opt.name }}</div>
<div class="flex-1">{{ UITypesName[opt.name] }}</div>
<span v-if="opt.deprecated" class="!text-xs !text-gray-300">({{ $t('general.deprecated') }})</span>
<component
:is="iconMap.check"
v-if="formState.uidt === opt.name"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
<!-- <div v-if="isEeUI && !props.hideType" class="mt-2 cursor-pointer" @click="predictColumnType()">
<component
:is="iconMap.check"
v-if="formState.uidt === opt.name"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
<!-- <div v-if="isEeUI && !props.hideType" class="mt-2 cursor-pointer" @click="predictColumnType()">
<GeneralIcon icon="magic" :class="{ 'nc-animation-pulse': loadMagic }" class="w-full flex mt-2 text-orange-400" />
</div> -->
</div>
<template v-if="!readOnly && formState.uidt">
<LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnLongTextOptions v-if="formState.uidt === UITypes.LongText" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnTimeOptions v-if="formState.uidt === UITypes.Time" v-model:value="formState" />
<LazySmartsheetColumnDecimalOptions v-if="formState.uidt === UITypes.Decimal" v-model:value="formState" />
<LazySmartsheetColumnDateTimeOptions
v-if="[UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(formState.uidt)"
v-model:value="formState"
/>
<LazySmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links"
:key="`${formState.uidt}-${formState.id || formState.title}`"
v-model:value="formState"
:is-edit="isEdit"
/>
<LazySmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" v-model:value="formState" />
<LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<LazySmartsheetColumnUserOptions v-if="formState.uidt === UITypes.User" v-model:value="formState" :is-edit="isEdit" />
<SmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState"
:from-table-explorer="props.fromTableExplorer || false"
/>
</template>
<template v-if="formState.uidt">
<div v-if="formState.meta && columnToValidate.includes(formState.uidt)" class="flex items-center gap-1">
<NcSwitch v-model:checked="formState.meta.validate" size="small" class="nc-switch">
<div class="text-sm text-gray-800">
{{
`${$t('msg.acceptOnlyValid', {
type:
formState.uidt === UITypes.URL
? `${UITypesName[formState.uidt as UITypes]}s`
: `${UITypesName[formState.uidt as UITypes]}s`.toLowerCase(),
})}`
}}
</div>
</NcSwitch>
</div>
<template v-if="!readOnly">
<LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnLongTextOptions v-if="formState.uidt === UITypes.LongText" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnDecimalOptions v-if="formState.uidt === UITypes.Decimal" v-model:value="formState" />
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && (formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links)"
:key="formState.uidt"
v-model:value="formState"
/>
<LazySmartsheetColumnLinkOptions v-if="isEdit && formState.uidt === UITypes.Links" v-model:value="formState" />
<LazySmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" v-model:value="formState" />
<LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<LazySmartsheetColumnUserOptions v-if="formState.uidt === UITypes.User" v-model:value="formState" :is-edit="isEdit" />
<SmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState"
:from-table-explorer="props.fromTableExplorer || false"
/>
</template>
</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">
{{ `${$t('msg.acceptOnlyValid')} ${formState.uidt}` }}
</span>
</a-checkbox>
<template v-if="!readOnly">
<div class="!my-3">
<!--
<div class="nc-column-options-wrapper flex flex-col gap-4">
<!--
Default Value for JSON & LongText is not supported in MySQL
Default Value is Disabled for MSSQL -->
<LazySmartsheetColumnRichLongTextDefaultValue
v-if="isTextArea(formState) && formState.meta?.richMode"
v-model:value="formState"
/>
<LazySmartsheetColumnDefaultValue
v-else-if="
<LazySmartsheetColumnRichLongTextDefaultValue
v-if="isTextArea(formState) && formState.meta?.richMode"
v-model:value="formState"
v-model:is-visible-default-value-input="isVisibleDefaultValueInput"
/>
<LazySmartsheetColumnDefaultValue
v-else-if="
!isVirtualCol(formState) &&
!isAttachment(formState) &&
!isMssql(meta!.source_id) &&
!(isMysql(meta!.source_id) && (isJSON(formState) || isTextArea(formState))) &&
!(isDatabricks(meta!.source_id) && formState.unique)
"
v-model:value="formState"
/>
<div
v-if="isDatabricks(meta!.source_id) && !formState.cdf && ![UITypes.MultiSelect, UITypes.Checkbox, UITypes.Rating, UITypes.Attachment, UITypes.Lookup, UITypes.Rollup, UITypes.Formula, UITypes.Barcode, UITypes.QrCode, UITypes.CreatedTime, UITypes.LastModifiedTime, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(formState.uidt)"
class="mt-3"
>
<a-checkbox v-model:checked="formState.unique"> Set as Unique </a-checkbox>
</div>
</div>
<div
v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt)&&!(!appInfo.ee && isAttachment(formState)) && (!appInfo.ee || (appInfo.ee && !isXcdbBase(meta!.source_id) && formState.uidt === UITypes.SpecificDBType))"
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"
>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<Transition name="layout" mode="out-in">
<div v-if="advancedOptions" class="overflow-hidden">
<LazySmartsheetColumnAttachmentOptions v-if="appInfo.ee && isAttachment(formState)" v-model:value="formState" />
<LazySmartsheetColumnAdvancedOptions
v-if="formState.uidt !== UITypes.Attachment"
v-model:value="formState"
:advanced-db-options="advancedOptions || formState.uidt === UITypes.SpecificDBType"
v-model:is-visible-default-value-input="isVisibleDefaultValueInput"
/>
<div
v-if="isDatabricks(meta!.source_id) && !formState.cdf && ![UITypes.MultiSelect, UITypes.Checkbox, UITypes.Rating, UITypes.Attachment, UITypes.Lookup, UITypes.Rollup, UITypes.Formula, UITypes.Barcode, UITypes.QrCode, UITypes.CreatedTime, UITypes.LastModifiedTime, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(formState.uidt)"
class="flex gap-1"
>
<NcSwitch v-model:checked="formState.unique" size="small" class="nc-switch">
<div class="text-sm text-gray-800">Set as Unique</div>
</NcSwitch>
</div>
</div>
</Transition>
</template>
<template v-if="props.fromTableExplorer">
<a-form-item></a-form-item>
</template>
<template v-else>
<a-form-item>
<div
class="flex gap-x-2 justify-between"
:class="{
'mt-6': props.hideAdditionalOptions,
'mt-2': !props.hideAdditionalOptions,
'justify-end': !props.embedMode,
}"
v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt)&&!(!appInfo.ee && isAttachment(formState)) && (!appInfo.ee || (appInfo.ee && !isXcdbBase(meta!.source_id) && formState.uidt === UITypes.SpecificDBType))"
class="text-xs text-gray-400 flex items-center justify-end"
>
<!-- Cancel -->
<NcButton size="small" class="w-full" html-type="button" type="secondary" @click="emit('cancel')">
{{ $t('general.cancel') }}
</NcButton>
<!-- Save -->
<NcButton
class="w-full"
html-type="submit"
type="primary"
:loading="saving"
size="small"
:label="`${$t('general.save')} ${columnLabel}`"
:loading-label="`${$t('general.saving')} ${columnLabel}`"
@click.prevent="onSubmit"
<div
class="nc-more-options flex items-center gap-1 cursor-pointer select-none"
@click="advancedOptions = !advancedOptions"
>
{{ $t('general.save') }} {{ columnLabel }}
<template #loading> {{ $t('general.saving') }} {{ columnLabel }} </template>
</NcButton>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
</div>
</a-form-item>
<Transition name="layout" mode="out-in">
<div v-if="advancedOptions" class="overflow-hidden">
<LazySmartsheetColumnAttachmentOptions v-if="appInfo.ee && isAttachment(formState)" v-model:value="formState" />
<LazySmartsheetColumnAdvancedOptions
v-if="formState.uidt !== UITypes.Attachment"
v-model:value="formState"
:advanced-db-options="advancedOptions || formState.uidt === UITypes.SpecificDBType"
/>
</div>
</Transition>
</template>
<template v-if="props.fromTableExplorer">
<a-form-item></a-form-item>
</template>
<template v-else>
<a-form-item>
<div
class="flex gap-x-2 justify-end"
:class="{
'justify-end': !props.embedMode,
}"
>
<!-- Cancel -->
<NcButton size="small" html-type="button" type="secondary" @click="emit('cancel')">
{{ $t('general.cancel') }}
</NcButton>
<!-- Save -->
<NcButton
html-type="submit"
type="primary"
:loading="saving"
:disabled="!formState.uidt || disableSubmitBtn"
size="small"
:label="submitBtnLabel.label"
:loading-label="submitBtnLabel.loadingLabel"
data-testid="nc-field-modal-submit-btn"
@click.prevent="onSubmit"
>
{{ submitBtnLabel.label }}
<template #loading>
{{ submitBtnLabel.loadingLabel }}
</template>
</NcButton>
</div>
</a-form-item>
</template>
</template>
</a-form>
</div>
</template>
<style lang="scss">
.nc-column-type-input {
.ant-select-selector {
@apply !rounded-md;
.nc-dropdown-column-type {
.ant-select-item-option-active-selected {
@apply !bg-gray-100;
}
}
</style>
<style scoped>
<style lang="scss" scoped>
.nc-column-name-input,
:deep(.nc-formula-input),
:deep(.ant-form-item-control-input-content > input.ant-input) {
&:not(:hover):not(:focus) {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
}
&:hover:not(:focus) {
@apply border-gray-300;
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.24);
}
}
:deep(.nc-color-picker-dropdown-trigger),
:deep(.nc-default-value-wrapper) {
@apply transition-all duration-0.3s;
&:not(:hover):not(:focus-within):not(.shadow-selected) {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
}
&:hover:not(:focus-within):not(.shadow-selected) {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.24);
}
}
:deep(.ant-radio-group .ant-radio-wrapper) {
@apply transition-all duration-0.3s;
&.ant-radio-wrapper-checked:not(.ant-radio-wrapper-disabled):focus-within {
@apply shadow-selected;
}
.ant-radio-wrapper-disabled {
@apply pointer-events-none;
}
&:not(:hover):not(:focus-within):not(.shadow-selected),
&.ant-radio-wrapper-disabled:hover {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
}
&:hover:not(:focus-within):not(.ant-radio-wrapper-disabled) {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.24);
}
}
:deep(.ant-select) {
&:not(:hover):not(.ant-select-focused) .ant-select-selector,
&:hover.ant-select-disabled .ant-select-selector {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
}
&:hover:not(.ant-select-focused):not(.ant-select-disabled) .ant-select-selector {
@apply border-gray-300;
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.24);
}
}
:deep(.ant-form-item-label > label) {
@apply !text-xs;
@apply !text-small !leading-[18px] mb-2 text-gray-700 flex;
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
@apply content-[''] m-0;
}
}
:deep(.ant-form-item-label) {
@apply !pb-0;
@apply !pb-0 text-small leading-[18px] text-gray-700;
}
:deep(.ant-form-item-control-input) {
@ -464,18 +595,49 @@ if (props.fromTableExplorer) {
}
:deep(.ant-form-item) {
@apply !mb-1;
@apply !mb-0;
}
:deep(.ant-select-selection-item) {
@apply flex items-center;
}
:deep(.ant-form-item-explain-error) {
@apply !text-[10px];
:deep(.ant-form-item-explain) {
@apply !text-[10px] leading-normal;
& > div:first-child {
@apply mt-0.5;
}
}
:deep(.ant-form-item-explain) {
@apply !min-h-[15px];
}
:deep(.ant-alert) {
@apply !rounded-lg !bg-transparent !border-none !p-0;
.ant-alert-message {
@apply text-sm text-gray-800 font-weight-600;
}
.ant-alert-description {
@apply text-small text-gray-500 font-weight-500;
}
}
:deep(.ant-select) {
.ant-select-selector {
@apply !rounded-lg;
}
}
:deep(input::placeholder),
:deep(textarea::placeholder) {
@apply text-gray-500;
}
.nc-column-options-wrapper {
&:empty {
@apply hidden;
}
}
</style>

8
packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue

@ -16,13 +16,9 @@ const emit = defineEmits(['submit', 'cancel', 'mounted'])
const meta = inject(MetaInj, ref())
const column = toRef(props, 'column')
const { column, preload, tableExplorerColumns, fromTableExplorer } = toRefs(props)
const preload = toRef(props, 'preload')
const tableExplorerColumns = toRef(props, 'tableExplorerColumns')
useProvideColumnCreateStore(meta, column, tableExplorerColumns)
useProvideColumnCreateStore(meta, column, tableExplorerColumns, fromTableExplorer)
</script>
<template>

98
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -283,27 +283,42 @@ setAdditionalValidations({
onMounted(() => {
jsep.plugins.register(jsepCurlyHook)
})
const suggestionPreviewLeft = ref('-left-85')
watch(sugListRef, () => {
nextTick(() => {
setTimeout(() => {
const fieldModal = document.querySelector('.nc-dropdown-edit-column.active') as HTMLDivElement
if (fieldModal && fieldModal.getBoundingClientRect().left < 364) {
suggestionPreviewLeft.value = '-right-85'
}
}, 500)
})
})
</script>
<template>
<div class="formula-wrapper relative">
<div
v-if="suggestionPreviewed && !suggestionPreviewed.unsupported && suggestionPreviewed.type === 'function'"
class="absolute -left-91 w-84 top-0 bg-white z-10 pl-3 pt-3 border-1 shadow-md rounded-xl"
class="absolute w-84 top-0 bg-white z-10 pl-3 pt-3 border-1 shadow-md rounded-xl"
:class="suggestionPreviewLeft"
>
<div class="pr-3">
<div class="flex flex-row w-full justify-between pb-1 border-b-1">
<div class="flex items-center gap-x-1 font-semibold text-base">
<div class="flex flex-row w-full justify-between pb-2 border-b-1">
<div class="flex items-center gap-x-1 font-semibold text-base text-gray-600">
<component :is="iconMap.function" class="text-lg" />
{{ suggestionPreviewed.text }}
</div>
<NcButton type="text" size="small" @click="suggestionPreviewed = undefined">
<NcButton type="text" size="small" class="!h-7 !w-7 !min-w-0" @click="suggestionPreviewed = undefined">
<GeneralIcon icon="close" />
</NcButton>
</div>
</div>
<div class="flex flex-col max-h-120 nc-scrollbar-md pr-2">
<div class="flex mt-3">{{ suggestionPreviewed.description }}</div>
<div class="flex flex-col max-h-120 nc-scrollbar-thin pr-2">
<div class="flex mt-3 text-sm">{{ suggestionPreviewed.description }}</div>
<div class="text-gray-500 uppercase text-xs mt-3 mb-2">Syntax</div>
<div class="bg-white rounded-md py-1 px-2 border-1">{{ suggestionPreviewed.syntax }}</div>
@ -322,16 +337,16 @@ onMounted(() => {
{{ example }}
</div>
</div>
<div class="flex flex-row mt-1 mb-3 justify-end pr-3">
<div class="flex flex-row mt-3 mb-3 justify-end pr-3">
<a v-if="suggestionPreviewed.docsUrl" target="_blank" rel="noopener noreferrer" :href="suggestionPreviewed.docsUrl">
<NcButton type="text" class="!text-gray-400 !hover:text-gray-800 !text-xs"
<NcButton type="text" size="small" class="!text-gray-400 !hover:text-gray-700 !text-xs"
>View in Docs
<GeneralIcon icon="openInNew" class="ml-1" />
</NcButton>
</a>
</div>
</div>
<a-form-item v-bind="validateInfos.formula_raw" class="!pb-1" :label="$t('datatype.Formula')">
<a-form-item v-bind="validateInfos.formula_raw" :label="$t('datatype.Formula')">
<!-- <GeneralIcon
v-if="isEeUI"
icon="magic"
@ -342,7 +357,7 @@ onMounted(() => {
<a-textarea
ref="formulaRef"
v-model:value="vModel.formula_raw"
class="nc-formula-input !rounded-md !my-1"
class="nc-formula-input !rounded-md"
@keydown.down.prevent="suggestionListDown"
@keydown.up.prevent="suggestionListUp"
@keydown.enter.prevent="selectText"
@ -350,15 +365,13 @@ onMounted(() => {
/>
</a-form-item>
<div ref="sugListRef" class="h-[250px] overflow-auto nc-scrollbar-md">
<div ref="sugListRef" class="h-[250px] overflow-auto nc-scrollbar-thin border-1 border-gray-200 rounded-lg mt-4">
<template v-if="suggestedFormulas.length > 0">
<div class="rounded-t-lg border-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs">Formulas</div>
<div class="border-b-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs font-semibold sticky top-0 z-10">
Formulas
</div>
<a-list
:data-source="suggestedFormulas"
:locale="{ emptyText: $t('msg.formula.noSuggestedFormulaFound') }"
class="border-1 border-t-0 rounded-b-lg !mb-4"
>
<a-list :data-source="suggestedFormulas" :locale="{ emptyText: $t('msg.formula.noSuggestedFormulaFound') }">
<template #renderItem="{ item, index }">
<a-list-item
:ref="
@ -377,12 +390,12 @@ onMounted(() => {
<a-list-item-meta>
<template #title>
<div class="flex items-center gap-x-1" :class="{ 'text-gray-400': item.unsupported }">
<component :is="iconMap.function" v-if="item.type === 'function'" class="text-lg" />
<component :is="iconMap.function" v-if="item.type === 'function'" class="w-4 h-4 !text-gray-600" />
<component :is="iconMap.calculator" v-if="item.type === 'op'" class="text-lg" />
<component :is="iconMap.calculator" v-if="item.type === 'op'" class="w-4 h-4 !text-gray-600" />
<component :is="item.icon" v-if="item.type === 'column'" class="text-lg" />
<span class="prose-sm" :class="{ 'text-gray-600': !item.unsupported }">{{ item.text }}</span>
<component :is="item.icon" v-if="item.type === 'column'" class="w-4 h-4 !text-gray-600" />
<span class="text-small leading-[18px]" :class="{ 'text-gray-800': !item.unsupported }">{{ item.text }}</span>
</div>
<div v-if="item.unsupported" class="ml-5 text-gray-400 text-xs">{{ $t('msg.formulaNotSupported') }}</div>
</template>
@ -393,13 +406,13 @@ onMounted(() => {
</template>
<template v-if="variableList.length > 0">
<div class="rounded-t-lg border-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs">Fields</div>
<div class="border-b-1 bg-gray-50 px-3 py-1 uppercase text-gray-600 text-xs font-semibold sticky top-0 z-10">Fields</div>
<a-list
ref="variableListRef"
:data-source="variableList"
:locale="{ emptyText: $t('msg.formula.noSuggestedFormulaFound') }"
class="border-1 border-t-0 rounded-b-lg !overflow-hidden"
class="!overflow-hidden"
>
<template #renderItem="{ item, index }">
<a-list-item
@ -414,12 +427,22 @@ onMounted(() => {
class="cursor-pointer hover:bg-gray-50"
@click.prevent.stop="appendText(item)"
>
<a-list-item-meta>
<a-list-item-meta class="nc-variable-list-item">
<template #title>
<div class="flex items-center gap-x-1">
<component :is="item.icon" class="text-lg" />
<span class="prose-sm text-gray-600">{{ item.text }}</span>
<div class="flex items-center gap-x-1 justify-between">
<div class="flex items-center gap-x-1 rounded-md bg-gray-200 px-1 h-5">
<component :is="item.icon" class="w-4 h-4 !text-gray-600" />
<span class="text-small leading-[18px] text-gray-800 font-weight-500">{{ item.text }}</span>
</div>
<NcButton
size="small"
type="text"
class="nc-variable-list-item-use-field-btn !h-7 px-3 !text-small invisible"
>
{{ $t('general.use') }} {{ $t('objects.field').toLowerCase() }}
</NcButton>
</div>
</template>
</a-list-item-meta>
@ -436,6 +459,23 @@ onMounted(() => {
<style lang="scss" scoped>
:deep(.ant-list-item) {
@apply !pt-1.75 pb-0.75 !px-2;
@apply !py-0 !px-2;
&:not(:has(.nc-variable-list-item)) {
@apply !py-[7px] !px-2;
}
.nc-variable-list-item {
@apply min-h-8 flex items-center;
}
.ant-list-item-meta-title {
@apply m-0;
}
&.ant-list-item,
&.ant-list-item:last-child {
@apply !border-b-1 border-gray-200 border-solid;
}
&:hover .nc-variable-list-item-use-field-btn {
@apply visible;
}
}
</style>

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

@ -47,7 +47,7 @@ vModel.value.meta = {
</script>
<template>
<a-row class="my-2" :gutter="8">
<a-row :gutter="8">
<a-col :span="12">
<a-form-item v-bind="validateInfos['meta.singular']" :label="$t('labels.singularLabel')">
<a-input

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

@ -1,16 +1,17 @@
<script setup lang="ts">
import { ModelTypes, MssqlUi, RelationTypes, SqliteUi, UITypes } from 'nocodb-sdk'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
const props = defineProps<{
value: any
isEdit: boolean
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const isEdit = toRef(props, 'isEdit')
const meta = inject(MetaInj, ref())
const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi, isXcdbBase } = useColumnCreateStoreOrThrow()
@ -20,25 +21,28 @@ const { tables } = storeToRefs(baseStore)
const { t } = useI18n()
setAdditionalValidations({
childId: [{ required: true, message: t('general.required') }],
})
if (!isEdit.value) {
setAdditionalValidations({
childId: [{ required: true, message: t('general.required') }],
})
}
const onUpdateDeleteOptions = sqlUi === MssqlUi ? ['NO ACTION'] : ['NO ACTION', 'CASCADE', 'RESTRICT', 'SET NULL', 'SET DEFAULT']
if (!vModel.value.parentId) vModel.value.parentId = meta.value?.id
if (!vModel.value.childId) vModel.value.childId = null
if (!vModel.value.childColumn) vModel.value.childColumn = `${meta.value?.table_name}_id`
if (!vModel.value.childTable) vModel.value.childTable = meta.value?.table_name
if (!vModel.value.parentTable) vModel.value.parentTable = vModel.value.rtn || ''
if (!vModel.value.parentColumn) vModel.value.parentColumn = vModel.value.rcn || ''
if (!vModel.value.type) vModel.value.type = 'mm'
if (!vModel.value.onUpdate) vModel.value.onUpdate = onUpdateDeleteOptions[0]
if (!vModel.value.onDelete) vModel.value.onDelete = onUpdateDeleteOptions[0]
if (!vModel.value.virtual) vModel.value.virtual = sqlUi === SqliteUi // appInfo.isCloud || sqlUi === SqliteUi
if (!vModel.value.alias) vModel.value.alias = vModel.value.column_name
if (!isEdit.value) {
if (!vModel.value.parentId) vModel.value.parentId = meta.value?.id
if (!vModel.value.childId) vModel.value.childId = null
if (!vModel.value.childColumn) vModel.value.childColumn = `${meta.value?.table_name}_id`
if (!vModel.value.childTable) vModel.value.childTable = meta.value?.table_name
if (!vModel.value.parentTable) vModel.value.parentTable = vModel.value.rtn || ''
if (!vModel.value.parentColumn) vModel.value.parentColumn = vModel.value.rcn || ''
if (!vModel.value.type) vModel.value.type = 'mm'
if (!vModel.value.onUpdate) vModel.value.onUpdate = onUpdateDeleteOptions[0]
if (!vModel.value.onDelete) vModel.value.onDelete = onUpdateDeleteOptions[0]
if (!vModel.value.virtual) vModel.value.virtual = sqlUi === SqliteUi // appInfo.isCloud || sqlUi === SqliteUi
if (!vModel.value.alias) vModel.value.alias = vModel.value.column_name
}
const advancedOptions = ref(false)
const refTables = computed(() => {
@ -52,31 +56,65 @@ const refTables = computed(() => {
const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase())
const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.value.type !== RelationTypes.ONE_TO_ONE)
const referenceTableChildId = computed({
get: () => (isEdit.value ? vModel.value?.colOptions?.fk_related_model_id : vModel.value?.childId) ?? null,
set: (value) => {
if (!isEdit.value && value) {
vModel.value.childId = value
}
},
})
const linkType = computed({
get: () => (isEdit.value ? vModel.value?.colOptions?.type : vModel.value?.type) ?? null,
set: (value) => {
if (!isEdit.value && value) {
vModel.value.type = value
}
},
})
</script>
<template>
<div class="w-full flex flex-col mb-2 mt-4">
<div class="border-2 p-6">
<a-form-item v-bind="validateInfos.type" class="nc-ltar-relation-type">
<a-radio-group v-model:value="vModel.type" name="type" v-bind="validateInfos.type" class="!flex flex-col gap-2">
<a-radio value="oo">{{ $t('title.oneToOne') }}</a-radio>
<a-radio value="hm">{{ $t('title.hasMany') }}</a-radio>
<a-radio value="mm">{{ $t('title.manyToMany') }}</a-radio>
<div class="w-full flex flex-col gap-4">
<div class="flex flex-col gap-4">
<a-form-item :label="$t('labels.relationType')" v-bind="validateInfos.type" class="nc-ltar-relation-type">
<a-radio-group v-model:value="linkType" name="type" v-bind="validateInfos.type" :disabled="isEdit">
<a-radio value="mm" data-testid="Many to Many">
<span class="nc-ltar-icon nc-mm-icon">
<GeneralIcon icon="mm_solid" />
</span>
{{ $t('title.manyToMany') }}
</a-radio>
<a-radio value="hm" data-testid="Has Many">
<span class="nc-ltar-icon nc-hm-icon">
<GeneralIcon icon="hm_solid" />
</span>
{{ $t('title.hasMany') }}
</a-radio>
<a-radio value="oo" data-testid="One to One">
<span class="nc-ltar-icon nc-oo-icon">
<GeneralIcon icon="oneToOneSolid" />
</span>
{{ $t('title.oneToOne') }}
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
:label="$t('labels.childTable')"
v-bind="validateInfos.childId"
>
<a-form-item class="flex w-full nc-ltar-child-table" v-bind="validateInfos.childId">
<a-select
v-model:value="vModel.childId"
v-model:value="referenceTableChildId"
show-search
:disabled="isEdit"
:filter-option="filterOption"
placeholder="select table to link"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
@ -91,19 +129,27 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.val
</a-select>
</a-form-item>
</div>
<template v-if="!isXcdbBase || isLinks">
<div
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions"
>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
<template v-if="(!isXcdbBase && !isEdit) || isLinks">
<div>
<NcButton
size="small"
type="text"
class="!text-gray-500 !hover:text-gray-700"
@click.stop="advancedOptions = !advancedOptions"
>
<div class="flex items-center gap-2">
<span class="first-letter:capitalize">
{{ $t('title.advancedSettings').toLowerCase() }}
</span>
<GeneralIcon :icon="advancedOptions ? 'arrowUp' : 'arrowDown'" class="h-4 w-4" />
</div>
</NcButton>
</div>
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<LazySmartsheetColumnLinkOptions v-if="isLinks" v-model:value="vModel" class="-my-2" />
<template v-if="!isXcdbBase">
<div v-if="advancedOptions" class="flex flex-col gap-4">
<LazySmartsheetColumnLinkOptions v-if="isLinks" v-model:value="vModel" />
<template v-if="!isXcdbBase && !isEdit">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select
@ -113,6 +159,9 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.val
dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
<template v-if="option === 'NO ACTION'">{{ $t('title.links.noAction') }}</template>
<template v-else-if="option === 'CASCADE'">{{ $t('title.links.cascade') }}</template>
@ -134,6 +183,9 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.val
dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
<template v-if="option === 'NO ACTION'">{{ $t('title.links.noAction') }}</template>
<template v-else-if="option === 'CASCADE'">{{ $t('title.links.cascade') }}</template>
@ -150,9 +202,13 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.val
<div class="flex flex-row">
<a-form-item>
<a-checkbox v-model:checked="vModel.virtual" name="virtual" @change="onDataTypeChange">{{
$t('title.virtualRelation')
}}</a-checkbox>
<div class="flex items-center gap-1">
<NcSwitch v-model:checked="vModel.virtual" @change="onDataTypeChange">
<div class="text-sm text-gray-800 select-none">
{{ $t('title.virtualRelation') }}
</div>
</NcSwitch>
</div>
</a-form-item>
</div>
</template>
@ -160,3 +216,32 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.val
</template>
</div>
</template>
<style lang="scss" scoped>
:deep(.nc-ltar-relation-type .ant-radio-group) {
@apply flex justify-between gap-2 children:(flex-1 m-0 px-2 py-1 border-1 border-gray-200 rounded-lg);
.ant-radio-wrapper {
@apply transition-all flex-row-reverse justify-between items-center py-1 pl-1 pr-3;
&.ant-radio-wrapper-checked:not(.ant-radio-wrapper-disabled) {
@apply border-brand-500;
}
span:not(.ant-radio):not(.nc-ltar-icon) {
@apply flex-1 pl-0 flex items-center gap-2;
}
.ant-radio {
@apply top-0;
}
.nc-ltar-icon {
@apply inline-flex items-center p-1 rounded-md;
}
}
}
:deep(.nc-ltar-relation-type .ant-col.ant-form-item-control) {
@apply h-8.5;
}
</style>

8
packages/nc-gui/components/smartsheet/column/LongTextOptions.vue

@ -24,11 +24,13 @@ watch(richMode, () => {
</script>
<template>
<div class="flex flex-col mt-2 gap-2">
<div class="flex flex-col gap-2">
<a-form-item>
<div class="flex flex-row items-center">
<div class="flex items-center gap-1">
<NcSwitch v-model:checked="richMode">
<div class="text-xs">{{ $t('labels.enableRichText') }}</div>
<div class="text-sm text-gray-800 select-none">
{{ $t('labels.enableRichText') }}
</div>
</NcSwitch>
</div>
</a-form-item>

140
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -15,7 +15,7 @@ const meta = inject(MetaInj, ref())
const { t } = useI18n()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit, disableSubmitBtn } = useColumnCreateStoreOrThrow()
const baseStore = useBase()
@ -76,6 +76,14 @@ const onRelationColChange = async () => {
onDataTypeChange()
}
watchEffect(() => {
if (!refTables.value.length) {
disableSubmitBtn.value = true
} else if (refTables.value.length && disableSubmitBtn.value) {
disableSubmitBtn.value = false
}
})
const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column,
@ -83,64 +91,96 @@ const cellIcon = (column: ColumnType) =>
</script>
<template>
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div v-if="refTables.length" class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id">
<a-select
v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 !rounded-md nc-dropdown-relation-table"
@change="onRelationColChange"
>
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex gap-2 w-full justify-between truncate items-center">
<NcTooltip class="font-semibold truncate min-w-1/2" show-on-truncate-only>
<div v-if="refTables.length" class="w-full flex flex-row space-x-2">
<a-form-item
class="flex w-1/2 !max-w-[calc(50%_-_4px)]"
:label="`${$t('general.link')} ${$t('objects.field')}`"
v-bind="validateInfos.fk_relation_column_id"
>
<a-select
v-model:value="vModel.fk_relation_column_id"
placeholder="-select-"
dropdown-class-name="!w-64 !rounded-md nc-dropdown-relation-table"
@change="onRelationColChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex gap-2 w-full justify-between truncate items-center">
<div class="min-w-1/2 flex items-center gap-2">
<component :is="cellIcon(table.column)" :column-meta="table.column" class="!mx-0" />
<NcTooltip class="truncate min-w-[calc(100%_-_24px)]" show-on-truncate-only>
<template #title>{{ table.column.title }}</template>
{{ table.column.title }}</NcTooltip
>
<div class="inline-flex items-center truncate gap-2">
<div class="text-[0.65rem] flex-1 truncate text-gray-600 nc-relation-details">
<span class="uppercase">{{ table.col.type }}</span>
<span class="truncate">{{ table.title || table.table_name }}</span>
</div>
<component
:is="iconMap.check"
v-if="vModel.fk_relation_column_id === table.col.fk_column_id"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
{{ table.column.title }}
</NcTooltip>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childField')" v-bind="validateInfos.fk_lookup_column_id">
<a-select
v-model:value="vModel.fk_lookup_column_id"
name="fk_lookup_column_id"
dropdown-class-name="nc-dropdown-relation-column !rounded-md"
@change="onDataTypeChange"
>
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
<div class="flex gap-2 truncate items-center">
<div class="inline-flex items-center flex-1 truncate font-semibold">
<component :is="cellIcon(column)" :column-meta="column" />
<div class="truncate flex-1">{{ column.title }}</div>
<div class="inline-flex items-center truncate gap-2">
<div class="text-[0.65rem] leading-4 flex-1 truncate text-gray-600 nc-relation-details">
<NcTooltip class="truncate" show-on-truncate-only>
<template #title>{{ table.title || table.table_name }}</template>
{{ table.title || table.table_name }}
</NcTooltip>
</div>
<component
:is="iconMap.check"
v-if="vModel.fk_lookup_column_id === column.id"
v-if="vModel.fk_relation_column_id === table.col.fk_column_id"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
<div v-else>{{ $t('msg.linkColumnClearNotSupportedYet') }}</div>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
class="flex w-1/2"
:label="`${$t('datatype.Lookup')} ${$t('objects.field')}`"
v-bind="vModel.fk_relation_column_id ? validateInfos.fk_lookup_column_id : undefined"
>
<a-select
v-model:value="vModel.fk_lookup_column_id"
name="fk_lookup_column_id"
placeholder="-select-"
:disabled="!vModel.fk_relation_column_id"
dropdown-class-name="nc-dropdown-relation-column !rounded-md"
@change="onDataTypeChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
<div class="w-full flex gap-2 truncate items-center justify-between">
<div class="inline-flex items-center gap-2 flex-1 truncate">
<component :is="cellIcon(column)" :column-meta="column" class="!mx-0" />
<div class="truncate flex-1">{{ column.title }}</div>
</div>
<component
:is="iconMap.check"
v-if="vModel.fk_lookup_column_id === column.id"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
<div v-else>
<a-alert type="warning" show-icon>
<template #icon><GeneralIcon icon="alertTriangle" class="h-6 w-6" width="24" height="24" /></template>
<template #message> Alert </template>
<template #description>
{{
$t('msg.linkColumnClearNotSupportedYet', {
type: 'Lookup',
})
}}
</template>
</a-alert>
</div>
</template>

12
packages/nc-gui/components/smartsheet/column/PercentOptions.vue

@ -25,10 +25,12 @@ vModel.value.meta = {
<template>
<div class="flex flex-col">
<div>
<a-checkbox v-if="vModel.meta" v-model:checked="vModel.meta.is_progress" class="ml-1 mb-1">
<span class="text-[10px] text-gray-600">Display as progress</span>
</a-checkbox>
</div>
<a-form-item>
<div class="flex items-center gap-1">
<NcSwitch v-if="vModel.meta" v-model:checked="vModel.meta.is_progress">
<div class="text-sm text-gray-800 select-none">{{ $t('labels.displayAsProgress') }}</div>
</NcSwitch>
</div>
</a-form-item>
</div>
</template>

94
packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes, isVirtualCol } from 'nocodb-sdk'
const props = defineProps<{
modelValue: any
@ -17,61 +16,70 @@ const vModel = useVModel(props, 'modelValue', emit)
const { setAdditionalValidations, validateInfos, column } = useColumnCreateStoreOrThrow()
const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
return fields.value
?.filter(
(el) =>
el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
})
const columnsAllowedAsQrValue = computed<ColumnType[]>(() => {
return (
fields.value
?.filter(
(el) =>
el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return metaColumnById.value[field.fk_column_id!]
}) || []
)
})
onMounted(() => {
// set default value
vModel.value.fk_qr_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || columnsAllowedAsQrValue.value?.[0]?.value
(column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || columnsAllowedAsQrValue.value?.[0]?.id
})
setAdditionalValidations({
fk_qr_value_column_id: [{ required: true, message: t('general.required') }],
})
const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column,
})
</script>
<template>
<a-row>
<a-col :span="24">
<a-form-item
class="flex w-1/2 pb-2 nc-qr-code-value-column-select"
:label="$t('labels.qrCodeValueColumn')"
v-bind="validateInfos.fk_qr_value_column_id"
<div class="flex flex-col gap-2">
<a-form-item
class="flex nc-qr-code-value-column-select"
:label="`${$t('placeholder.value')} ${t('objects.field').toLowerCase()}`"
v-bind="validateInfos.fk_qr_value_column_id"
>
<a-select
v-model:value="vModel.fk_qr_value_column_id"
:placeholder="$t('placeholder.selectAColumnForTheQRCodeValue')"
@click.stop
>
<a-select
v-model:value="vModel.fk_qr_value_column_id"
:placeholder="$t('placeholder.selectAColumnForTheQRCodeValue')"
@click.stop
>
<a-select-option v-for="opt of columnsAllowedAsQrValue" :key="opt" :value="opt.value">
<div class="flex gap-2 w-full truncate items-center" :data-testid="`nc-qr-${opt.label}`">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(option, index) of columnsAllowedAsQrValue" :key="index" :value="option.id">
<div class="flex gap-2 w-full truncate items-center" :data-testid="`nc-qr-${option.title}`">
<div class="inline-flex items-center gap-2 flex-1 truncate">
<component :is="cellIcon(option)" :column-meta="option" class="!mx-0 flex-none w-4 h-4" />
<NcTooltip show-on-truncate-only class="flex-1 truncate">
<template #title>{{ opt.label }}</template>
{{ opt.label }}
<template #title>{{ option.title }}</template>
{{ option.title }}
</NcTooltip>
<component
:is="iconMap.check"
v-if="vModel.fk_qr_value_column_id === opt.value"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<component
:is="iconMap.check"
v-if="vModel.fk_qr_value_column_id === option.id"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
</template>

85
packages/nc-gui/components/smartsheet/column/RatingOptions.vue

@ -38,6 +38,8 @@ const picked = computed({
},
})
const isOpenColorPicker = ref(false)
// set default value
vModel.value.meta = {
iconIdx: 0,
@ -68,26 +70,19 @@ watch(
<template>
<a-row :gutter="8">
<a-col :span="12">
<a-col :span="8">
<a-form-item :label="$t('labels.icon')">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52" dropdown-class-name="nc-dropdown-rating-icon">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<div class="flex gap-2 w-full truncate items-center">
<div class="flex-1">
<component
:is="getMdiIcon(icon.full)"
class="mr-[2px]"
:style="{
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.empty)"
:style="{
color: vModel.meta.color,
}"
/>
<div class="flex-1 flex items-center text-gray-700 gap-2 children:(h-4 w-4)">
<component :is="getMdiIcon(icon.full)" />
<component :is="getMdiIcon(icon.empty)" />
</div>
<component
@ -101,9 +96,56 @@ watch(
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-col :span="8">
<a-form-item :label="$t('general.colour')">
<NcDropdown
v-model:visible="isOpenColorPicker"
placement="bottom"
:auto-close="false"
class="nc-color-picker-dropdown-trigger"
>
<div
class="flex-1 border-1 border-gray-300 rounded-lg h-8 px-[11px] flex items-center justify-between transition-all cursor-pointer"
:class="{
'border-brand-500 shadow-selected': isOpenColorPicker,
}"
>
<div class="flex-1 flex items-center gap-2 children:(h-4 w-4)">
<component
:is="getMdiIcon(iconList[vModel.meta.iconIdx].full)"
:style="{
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(iconList[vModel.meta.iconIdx].empty)"
:style="{
color: vModel.meta.color,
}"
/>
</div>
<GeneralIcon icon="arrowDown" class="text-gray-700 h-4 w-4" />
</div>
<template #overlay>
<div>
<LazyGeneralAdvanceColorPicker
v-model="picked"
:is-open="isOpenColorPicker"
@input="(el:string)=>vModel.meta.color=el"
></LazyGeneralAdvanceColorPicker>
</div>
</template>
</NcDropdown>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="$t('labels.max')">
<a-select v-model:value="vModel.meta.max" class="w-52" dropdown-class-name="nc-dropdown-rating-color">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(v, i) in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" :key="i" :value="v">
<div class="flex gap-2 w-full justify-between items-center">
{{ v }}
@ -119,15 +161,6 @@ watch(
</a-form-item>
</a-col>
</a-row>
<a-row class="w-full justify-center">
<LazyGeneralColorPicker
v-model="picked"
:row-size="8"
:colors="['#FF94B6', '#6A8D9D', '#6DAE42', '#4AC0BF', '#905FB3', '#FF8320', '#6BCC72', '#FF4138']"
@input="(el:string) => (vModel.meta.color = el)"
/>
</a-row>
</template>
<style scoped lang="scss">

55
packages/nc-gui/components/smartsheet/column/RichLongTextDefaultValue.vue

@ -1,28 +1,75 @@
<script lang="ts" setup>
const props = defineProps<{
value: any
isVisibleDefaultValueInput: boolean
}>()
const emits = defineEmits(['update:value'])
provide(EditColumnInj, ref(true))
const vModel = useVModel(props, 'value', emits)
const isVisibleDefaultValueInput = useVModel(props, 'isVisibleDefaultValueInput', emits)
const cdfValue = computed({
get: () => vModel.value.cdf,
set: (value) => {
vModel.value.cdf = value
if (value === '<br />') {
vModel.value.cdf = null
} else {
vModel.value.cdf = value
}
},
})
</script>
<template>
<div>
<div class="!my-3 text-xs">{{ $t('placeholder.defaultValue') }}</div>
<div v-if="!isVisibleDefaultValueInput">
<NcButton
size="small"
type="text"
class="!text-gray-500 !hover:text-gray-700"
data-testid="nc-show-default-value-btn"
@click.stop="isVisibleDefaultValueInput = true"
>
<div class="flex items-center gap-2">
<span>{{ $t('general.set') }} {{ $t('placeholder.defaultValue').toLowerCase() }}</span>
<GeneralIcon icon="plus" class="flex-none h-4 w-4" />
</div>
</NcButton>
</div>
<div v-else>
<div class="w-full flex items-center gap-2 mb-2">
<div class="text-small leading-[18px] flex-1 text-gray-700">{{ $t('placeholder.defaultValue') }}</div>
</div>
<div class="flex flex-row gap-2">
<div
class="border-1 relative pt-11 flex items-center w-full px-0 border-gray-300 rounded-md max-h-70 pb-1 focus-within:border-brand-500"
class="nc-default-value-wrapper nc-rich-long-text-default-value border-1 relative pt-7 flex items-center w-full px-0 border-gray-300 rounded-md max-h-70 pb-1 focus-within:(border-brand-500 shadow-selected) transition-all duration-0.3s"
>
<LazyCellRichText v-model:value="cdfValue" class="border-t-1 border-gray-100 !max-h-80 !min-h-30" show-menu />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.nc-rich-long-text-default-value {
:deep(.nc-rich-text) {
.bubble-menu.embed-mode.edit-column-mode {
@apply gap-x-0 p-0 h-7 border-0;
.nc-button {
@apply !mt-0 h-7 p-1 min-w-7;
svg {
@apply h-4 w-4;
}
}
.divider {
@apply !m-0 !h-7 border-gray-100;
}
}
}
}
</style>

82
packages/nc-gui/components/smartsheet/column/RollupOptions.vue

@ -13,7 +13,7 @@ const vModel = useVModel(props, 'value', emit)
const meta = inject(MetaInj, ref())
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit, disableSubmitBtn } = useColumnCreateStoreOrThrow()
const baseStore = useBase()
@ -134,27 +134,49 @@ watch(
}
},
)
watchEffect(() => {
if (!refTables.value.length) {
disableSubmitBtn.value = true
} else if (refTables.value.length && disableSubmitBtn.value) {
disableSubmitBtn.value = false
}
})
</script>
<template>
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div v-if="refTables.length" class="flex flex-col gap-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id">
<a-form-item
class="flex w-1/2 !max-w-[calc(50%_-_4px)] pb-2"
:label="`${$t('general.link')} ${$t('objects.field')}`"
v-bind="validateInfos.fk_relation_column_id"
>
<a-select
v-model:value="vModel.fk_relation_column_id"
placeholder="-select-"
dropdown-class-name="!w-64 nc-dropdown-relation-table !rounded-md"
@change="onRelationColChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex gap-2 w-full justify-between truncate items-center">
<NcTooltip class="font-semibold truncate min-w-1/2" show-on-truncate-only>
<template #title>{{ table.column.title }}</template>
{{ table.column.title }}</NcTooltip
>
<div class="min-w-1/2 flex items-center gap-2">
<component :is="cellIcon(table.column)" :column-meta="table.column" class="!mx-0" />
<NcTooltip class="truncate min-w-[calc(100%_-_24px)]" show-on-truncate-only>
<template #title>{{ table.column.title }}</template>
{{ table.column.title }}
</NcTooltip>
</div>
<div class="inline-flex items-center truncate gap-2">
<div class="text-[0.65rem] flex-1 truncate text-gray-600 nc-relation-details">
<span class="uppercase">{{ table.col.type }}</span>
<span class="truncate">{{ table.title || table.table_name }}</span>
<div class="text-[0.65rem] leading-4 flex-1 truncate text-gray-600 nc-relation-details">
<NcTooltip class="truncate" show-on-truncate-only>
<template #title>{{ table.title || table.table_name }}</template>
{{ table.title || table.table_name }}
</NcTooltip>
</div>
<component
@ -169,17 +191,26 @@ watch(
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_rollup_column_id">
<a-form-item
class="flex w-1/2"
:label="`${$t('datatype.Rollup')} ${$t('objects.field')}`"
v-bind="vModel.fk_relation_column_id ? validateInfos.fk_rollup_column_id : undefined"
>
<a-select
v-model:value="vModel.fk_rollup_column_id"
name="fk_rollup_column_id"
placeholder="-select-"
:disabled="!vModel.fk_relation_column_id"
dropdown-class-name="nc-dropdown-relation-column !rounded-xl"
@change="onDataTypeChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(column, index) of filteredColumns" :key="index" :value="column.id">
<div class="flex gap-2 truncate items-center">
<div class="flex items-center flex-1 truncate font-semibold">
<component :is="cellIcon(column)" :column-meta="column" />
<div class="w-full flex gap-2 truncate items-center justify-between">
<div class="flex items-center gap-2 flex-1 truncate">
<component :is="cellIcon(column)" :column-meta="column" class="!mx-0" />
<div class="truncate flex-1">{{ column.title }}</div>
</div>
<component
@ -194,13 +225,21 @@ watch(
</a-form-item>
</div>
<a-form-item :label="$t('labels.aggregateFunction')" v-bind="validateInfos.rollup_function">
<a-form-item
:label="$t('labels.aggregateFunction')"
v-bind="vModel.fk_relation_column_id ? validateInfos.rollup_function : undefined"
>
<a-select
v-model:value="vModel.rollup_function"
:disabled="!vModel.fk_relation_column_id"
placeholder="-select-"
dropdown-class-name="nc-dropdown-rollup-function"
class="!mt-0.5"
@change="onDataTypeChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="(func, index) of aggFunctionsList" :key="index" :value="func.value">
<div class="flex gap-2 justify-between items-center">
{{ func.text }}
@ -215,6 +254,19 @@ watch(
</a-select>
</a-form-item>
</div>
<div v-else>
<a-alert type="warning" show-icon>
<template #icon><GeneralIcon icon="alertTriangle" class="h-6 w-6" width="24" height="24" /></template>
<template #message> Alert </template>
<template #description>
{{
$t('msg.linkColumnClearNotSupportedYet', {
type: 'Rollup',
})
}}
</template>
</a-alert>
</div>
</template>
<style scoped>

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

@ -344,7 +344,10 @@ const loadListData = async ($state: any) => {
<div class="w-full">
<div
ref="optionsWrapperDomRef"
class="nc-col-option-select-option overflow-x-auto scrollbar-thin-dull"
class="nc-col-option-select-option overflow-x-auto scrollbar-thin-dull rounded-lg"
:class="{
'border-1 border-gray-200': renderedOptions.length,
}"
:style="{
maxHeight: props.fromTableExplorer ? 'calc(100vh - (var(--topbar-height) * 3.6) - 320px)' : 'calc(min(30vh, 250px))',
}"
@ -361,42 +364,41 @@ const loadListData = async ($state: any) => {
</InfiniteLoading>
<Draggable :list="renderedOptions" item-key="id" handle=".nc-child-draggable-icon" @change="syncOptions">
<template #item="{ element, index }">
<div class="flex py-1 items-center nc-select-option">
<div class="flex py-1 items-center nc-select-option hover:bg-gray-100 group">
<div
class="flex items-center w-full"
:data-testid="`select-column-option-${index}`"
:class="{ removed: element.status === 'remove' }"
>
<component
:is="iconMap.dragVertical"
<div
v-if="!isKanban"
small
class="nc-child-draggable-icon handle"
class="nc-child-draggable-icon p-2 flex cursor-pointer"
:data-testid="`select-option-column-handle-icon-${element.title}`"
/>
<a-dropdown
v-model:visible="colorMenus[index]"
:trigger="['click']"
overlay-class-name="nc-dropdown-select-color-options rounded-md overflow-hidden border-1 border-gray-200 "
>
<component :is="iconMap.dragVertical" small class="handle" />
</div>
<NcDropdown v-model:visible="colorMenus[index]" :auto-close="false">
<div class="flex-none h-6 w-6 flex cursor-pointer mx-1">
<div class="h-6 w-6 rounded flex items-center" :style="{ backgroundColor: element.color }">
<GeneralIcon icon="arrowDown" class="flex-none h-4 w-4 m-auto !text-gray-600" />
</div>
</div>
<template #overlay>
<LazyGeneralColorPicker
v-model="element.color"
:pick-button="true"
@close-modal="colorMenus[index] = false"
@input="(el:string) => (element.color = el)"
/>
<div>
<LazyGeneralAdvanceColorPicker
v-model="element.color"
:is-open="colorMenus[index]"
@input="(el:string) => (element.color = el)"
></LazyGeneralAdvanceColorPicker>
</div>
</template>
<MdiArrowDownDropCircle
class="mr-2 text-[1.5em] outline-0 hover:!text-[1.75em] cursor-pointer"
:class="{ 'text-[1.75em]': colorMenus[index] }"
:style="{ color: element.color }"
/>
</a-dropdown>
</NcDropdown>
<a-input
v-model:value="element.title"
class="caption !rounded-lg nc-select-col-option-select-option"
class="caption !rounded-lg nc-select-col-option-select-option !bg-transparent"
:data-testid="`select-column-option-input-${index}`"
:disabled="element.status === 'remove'"
@keydown.enter.prevent="element.title?.trim() && addNewOption()"
@ -407,18 +409,22 @@ const loadListData = async ($state: any) => {
<div
v-if="element.status !== 'remove'"
:data-testid="`select-column-option-remove-${index}`"
class="ml-1 hover:!text-black-500 text-gray-500 cursor-pointer hover:bg-gray-50 py-1 px-1.5 rounded-md"
class="mx-1 hover:!text-black-500 text-gray-500 cursor-pointer hover:bg-gray-200 py-1 px-1.5 rounded-md h-7 flex items-center invisible group-hover:visible"
@click="removeRenderedOption(index)"
>
<component :is="iconMap.close" class="-mt-0.25" />
<component :is="iconMap.close" class="-mt-0.25 w-4 h-4" />
</div>
<MdiArrowULeftBottom
<div
v-else
class="ml-2 hover:!text-black-500 text-gray-500 cursor-pointer"
:data-testid="`select-column-option-remove-undo-${index}`"
class="mx-1 hover:!text-black-500 text-gray-500 cursor-pointer hover:bg-gray-200 py-1 px-1.5 rounded-md h-7 flex items-center invisible group-hover:visible"
@click="undoRemoveRenderedOption(index)"
/>
>
<MdiArrowULeftBottom
class="hover:!text-black-500 text-gray-500 cursor-pointer w-4 h-4"
@click="undoRemoveRenderedOption(index)"
/>
</div>
</div>
</template>
</Draggable>
@ -437,12 +443,21 @@ const loadListData = async ($state: any) => {
<div v-if="validateInfos?.colOptions?.help?.[0]?.[0]" class="text-error text-[10px] mb-1 mt-2">
{{ validateInfos.colOptions.help[0][0] }}
</div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()">
<NcButton
type="secondary"
class="w-full caption"
:class="{
'mt-2': renderedOptions.length,
}"
size="small"
data-testid="nc-add-select-option-btn"
@click="addNewOption()"
>
<div class="flex items-center">
<component :is="iconMap.plus" />
<span class="flex-auto">Add option</span>
</div>
</a-button>
</NcButton>
<!-- <div v-if="isEeUI" class="w-full cursor-pointer" @click="optionsMagic()">
<GeneralIcon icon="magic" :class="{ 'nc-animation-pulse': loadMagic }" class="w-full flex mt-2 text-orange-400" />
</div> -->
@ -463,4 +478,21 @@ const loadListData = async ($state: any) => {
width: calc(100% + 5px);
display: block;
}
:deep(.nc-select-col-option-select-option) {
@apply !truncate;
&:not(:focus):hover {
@apply !border-transparent;
}
&:not(:focus) {
@apply !border-transparent;
}
&:focus,
&:focus-visible {
@apply !border-[var(--ant-primary-color-hover)];
}
}
</style>

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

@ -1,3 +1,3 @@
<template>
<div />
<div class="hidden" />
</template>

41
packages/nc-gui/components/smartsheet/column/TimeOptions.vue

@ -0,0 +1,41 @@
<script setup lang="ts">
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
// set default value
vModel.value.meta = {
is12hrFormat: false,
...(vModel.value.meta ?? {}),
}
</script>
<template>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2 children:flex-1">
<a-form-item>
<a-radio-group v-if="vModel.meta" v-model:value="vModel.meta.is12hrFormat" class="nc-time-form-layout">
<a-radio :value="true">12 Hrs</a-radio>
<a-radio :value="false">24 Hrs</a-radio>
</a-radio-group>
</a-form-item>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.nc-time-form-layout) {
@apply flex justify-between gap-2 children:(flex-1 m-0 px-2 py-1 border-1 border-gray-200 rounded-lg);
.ant-radio-wrapper {
@apply transition-all;
&.ant-radio-wrapper-checked {
@apply border-brand-500;
}
}
}
</style>

115
packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue

@ -0,0 +1,115 @@
<script lang="ts" setup>
import type { UITypes } from 'nocodb-sdk'
import { UITypesName } from 'nocodb-sdk'
const props = defineProps<{
options: typeof uiTypes
}>()
const emits = defineEmits<{ selected: [UITypes] }>()
const { options } = toRefs(props)
const searchQuery = ref('')
const filteredOptions = computed(
() => options.value?.filter((c) => c.name.toLowerCase().includes(searchQuery.value.toLowerCase())) ?? [],
)
const inputRef = ref()
const activeFieldIndex = ref(-1)
const onClick = (uidt: UITypes) => {
if (!uidt) return
emits('selected', uidt)
}
const handleAutoScrollOption = () => {
const option = document.querySelector('.nc-field-list-option-active')
if (option) {
setTimeout(() => {
option?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 50)
}
}
const onArrowDown = () => {
activeFieldIndex.value = Math.min(activeFieldIndex.value + 1, filteredOptions.value.length - 1)
handleAutoScrollOption()
}
const onArrowUp = () => {
activeFieldIndex.value = Math.max(activeFieldIndex.value - 1, 0)
handleAutoScrollOption()
}
const handleKeydownEnter = () => {
if (filteredOptions.value[activeFieldIndex.value]) {
onClick(filteredOptions.value[activeFieldIndex.value].name)
} else if (filteredOptions.value[0]) {
onClick(filteredOptions.value[activeFieldIndex.value].name)
}
}
onMounted(() => {
searchQuery.value = ''
activeFieldIndex.value = -1
})
</script>
<template>
<div
class="flex-1 border-1 border-gray-200 rounded-lg flex flex-col py-2"
data-testid="nc-column-uitypes-options-list-wrapper"
@keydown.arrow-down.prevent="onArrowDown"
@keydown.arrow-up.prevent="onArrowUp"
@keydown.enter.prevent="onClick(filteredOptions[activeFieldIndex].name)"
>
<div class="w-full pb-2 px-2" @click.stop>
<a-input
ref="inputRef"
v-model:value="searchQuery"
placeholder="Search field type"
class="nc-column-type-search-input nc-toolbar-dropdown-search-field-input"
@keydown.enter.stop="handleKeydownEnter"
@change="activeFieldIndex = 0"
>
<template #prefix> <GeneralIcon icon="search" class="nc-search-icon h-4 w-4 mr-1" /> </template>
</a-input>
</div>
<div class="nc-column-list-wrapper flex-col w-full max-h-[290px] nc-scrollbar-thin !overflow-y-auto px-2">
<div v-if="!filteredOptions.length" class="px-2 py-6 text-gray-500 flex flex-col items-center gap-6">
<img
src="~assets/img/placeholder/no-search-result-found.png"
class="!w-[164px] flex-none"
alt="No search results found"
/>
{{ options.length ? $t('title.noResultsMatchedYourSearch') : 'The list is empty' }}
</div>
<div
v-for="(option, index) in filteredOptions"
:key="index"
class="flex w-full py-2 items-center justify-between px-2 hover:bg-gray-100 cursor-pointer rounded-md"
:class="[
`nc-column-list-option-${index}`,
{
'bg-gray-100 nc-column-list-option-active': activeFieldIndex === index,
},
]"
:data-testid="option.name"
@click="onClick(option.name)"
>
<div class="flex gap-2 items-center">
<component :is="option.icon" class="text-gray-700 w-4 h-4" />
<div class="flex-1 text-sm">{{ UITypesName[option.name] }}</div>
<span v-if="option.deprecated" class="!text-xs !text-gray-300">({{ $t('general.deprecated') }})</span>
</div>
</div>
</div>
</div>
</template>

44
packages/nc-gui/components/smartsheet/column/UserOptions.vue

@ -31,8 +31,8 @@ onMounted(() => {
initialIsMulti.value = vModel.value.meta.is_multi
})
const updateIsMulti = (e) => {
vModel.value.meta.is_multi = e.target.checked
const updateIsMulti = (isChecked: boolean) => {
vModel.value.meta.is_multi = isChecked
if (!vModel.value.meta.is_multi) {
vModel.value.cdf = vModel.value.cdf?.split(',')[0] || null
}
@ -40,25 +40,25 @@ const updateIsMulti = (e) => {
</script>
<template>
<div class="flex flex-col">
<div>
<a-checkbox
v-if="vModel.meta"
:checked="vModel.meta.is_multi"
class="ml-1 mb-1"
data-testid="user-column-allow-multiple"
@change="updateIsMulti"
>
<span class="text-[10px] text-gray-600">Allow adding multiple users</span>
</a-checkbox>
</div>
<div v-if="future">
<a-checkbox v-if="vModel.meta" v-model:checked="vModel.meta.notify" class="ml-1 mb-1">
<span class="text-[10px] text-gray-600">Notify users with base access when they're added</span>
</a-checkbox>
</div>
<div v-if="initialIsMulti && isEdit && !vModel.meta.is_multi" class="text-error text-[10px] mb-1 mt-2">
<span>Changing from multiple mode to single will retain only first user in each cell!!!</span>
</div>
<div class="flex flex-col gap-4">
<a-form-item>
<div v-if="vModel.meta" class="flex items-center gap-1">
<NcSwitch :checked="vModel.meta.is_multi" data-testid="user-column-allow-multiple" @change="updateIsMulti">
<div class="text-sm text-gray-800 select-none">Allow adding multiple users</div>
</NcSwitch>
</div>
</a-form-item>
<a-form-item v-if="future">
<div v-if="vModel.meta" class="flex items-center gap-1">
<NcSwitch v-model:checked="vModel.meta.notify" data-testid="user-column-notify-user" @change="updateIsMulti">
<div class="text-sm text-gray-800 select-none">Notify users with base access when they're added</div>
</NcSwitch>
</div>
</a-form-item>
<a-alert v-if="initialIsMulti && isEdit && !vModel.meta.is_multi" type="warning" show-icon>
<template #icon><GeneralIcon icon="alertTriangle" class="h-6 w-6" width="24" height="24" /></template>
<template #message> Alert </template>
<template #description> Changing from multiple mode to single will retain only first user in each cell! </template>
</a-alert>
</div>
</template>

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

@ -179,6 +179,12 @@ const setFieldMoveHook = (field: TableExplorerColumn, before = false) => {
}
const changeField = (field?: TableExplorerColumn, event?: MouseEvent) => {
if (field && field?.pk) {
// Editing primary key not supported
message.info(t('msg.info.editingPKnotSupported'))
return
}
if (event) {
if (event.target instanceof HTMLElement) {
if (event.target.closest('.no-action')) return

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

@ -191,7 +191,7 @@ const onClick = (e: Event) => {
class="h-full"
:trigger="['click']"
:placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'"
overlay-class-name="nc-dropdown-edit-column"
:overlay-class-name="`nc-dropdown-edit-column ${editColumnDropdown ? 'active' : ''}`"
>
<div v-if="isExpandedForm && !isExpandedBulkUpdateForm" class="h-[1px]" @dblclick.stop>&nbsp;</div>
<div v-else />

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ColumnReqType } from 'nocodb-sdk'
import { PlanLimitTypes, RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { PlanLimitTypes, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { SmartsheetStoreEvents } from '#imports'
const props = defineProps<{ virtual?: boolean; isOpen: boolean; isHiddenCol?: boolean }>()
@ -366,7 +366,7 @@ const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
'min-w-[256px]': isExpandedForm,
}"
>
<NcMenuItem @click="onEditPress">
<NcMenuItem :disabled="column?.pk || isSystemColumn(column)" @click="onEditPress">
<div class="nc-column-edit nc-header-menu-item">
<component :is="iconMap.ncEdit" class="text-gray-700" />
<!-- Edit -->

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

@ -248,7 +248,7 @@ const onClick = (e: Event) => {
class="h-full"
:trigger="['click']"
:placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'"
overlay-class-name="nc-dropdown-edit-column"
:overlay-class-name="`nc-dropdown-edit-column ${editColumnDropdown ? 'active' : ''}`"
>
<div v-if="isExpandedForm && !isExpandedBulkUpdateForm" class="h-[1px]" @dblclick.stop>&nbsp;</div>
<div v-else />

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

@ -19,6 +19,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
meta: Ref<TableType | undefined>,
column: Ref<ColumnType | undefined>,
tableExplorerColumns?: Ref<ColumnType[] | undefined>,
fromTableExplorer?: Ref<boolean | undefined>,
) => {
const baseStore = useBase()
@ -38,6 +39,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { activeView } = storeToRefs(useViewsStore())
const disableSubmitBtn = ref(false)
const isEdit = computed(() => !!column?.value?.id)
const isMysql = computed(() => isMysqlFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]))
@ -60,7 +63,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const formState = ref<Record<string, any>>({
title: 'title',
uidt: UITypes.SingleLineText,
uidt: fromTableExplorer?.value ? UITypes.SingleLineText : null,
...clone(column.value || {}),
})
@ -79,13 +82,16 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}
// actions
const generateNewColumnMeta = () => {
const generateNewColumnMeta = (ignoreUidt = false) => {
setAdditionalValidations({})
formState.value = {
meta: {},
...sqlUi.value.getNewColumn(generateUniqueColumnSuffix()),
}
formState.value.title = formState.value.column_name
if (ignoreUidt && !fromTableExplorer?.value) {
formState.value.uidt = null
}
}
const validators = computed(() => {
@ -143,6 +149,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const onUidtOrIdTypeChange = () => {
disableSubmitBtn.value = false
const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined)
formState.value = {
...(!isEdit.value && {
@ -324,6 +332,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isPg,
isMysql,
isXcdbBase,
disableSubmitBtn,
}
},
)

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

@ -11,7 +11,7 @@ export function useData(args: {
callbacks?: {
changePage?: (page: number) => Promise<void>
loadData?: () => Promise<void>
globalCallback?: (...args: any[]) => Promise<void>,
globalCallback?: (...args: any[]) => Promise<void>
syncCount?: () => Promise<void>
syncPagination?: () => Promise<void>
}

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

@ -98,6 +98,7 @@
"bulkUpdate": "Bulk Update",
"deleting": "Deleting",
"update": "Update",
"updating": "Updating",
"rename": "Rename",
"reload": "Reload",
"reset": "Reset",
@ -205,7 +206,11 @@
"apply": "Apply",
"text": "Text",
"appearance": "Appearance",
"now": "Now"
"now": "Now",
"set": "Set",
"format": "Format",
"colour": "Colour",
"use": "Use"
},
"objects": {
"owner": "Owner",
@ -331,8 +336,8 @@
"parameter": "Parameter",
"headers": "Headers",
"parameterName": "Parameter Name",
"currencyLocale": "Currency Locale",
"currencyCode": "Currency Code",
"currencyLocale": "Locale",
"currencyCode": "Code",
"searchMembers": "Search Members",
"noMembersFound": "No members found",
"dateJoined": "Date Joined",
@ -536,7 +541,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -766,7 +771,9 @@
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection"
"clearSelection": "Clear selection",
"displayAsProgress": "Display as progress",
"relationType": "Relation type"
},
"activity": {
"renameBase": "Rename Base",
@ -1123,7 +1130,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1196,7 +1203,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

112
packages/nc-gui/utils/colorsUtils.ts

@ -70,6 +70,118 @@ export const themeV2Colors = {
'pink': colors.pink,
}
export const themeV3Colors = {
brand: {
50: '#EBF0FF',
100: '#D6E0FF',
200: '#ADC2FF',
300: '#85A3FF',
400: '#5C85FF',
500: '#3366FF',
600: '#2952CC',
700: '#1F3D99',
800: '#142966',
900: '#0A1433',
},
gray: {
10: '#FCFCFC',
50: '#F9F9FA',
100: '#F4F4F5',
200: '#E7E7E9',
300: '#D5D5D9',
400: '#9AA2AF',
500: '#6A7184',
600: '#4A5268',
700: '#374151',
800: '#1F293A',
900: '#101015',
},
red: {
50: '#FFF2F1',
100: '#FFDBD9',
200: '#FFB7B2',
300: '#FF928C',
400: '#FF6E65',
500: '#FF4A3F',
600: '#E8463C',
700: '#CB3F36',
800: '#B23830',
900: '#7D2721',
},
pink: {
50: '#FFEEFB',
100: '#FED8F4',
200: '#FEB0E8',
300: '#FD89DD',
400: '#FD61D1',
500: '#FC3AC6',
600: '#CA2E9E',
700: '#972377',
800: '#65174F',
900: '#320C28',
},
orange: {
50: '#FFF5EF',
100: '#FEE6D6',
200: '#FDCDAD',
300: '#FCB483',
400: '#FB9B5A',
500: '#FA8231',
600: '#E1752C',
700: '#C86827',
800: '#964E1D',
900: '#4B270F',
},
purple: {
50: '#F3ECFA',
100: '#E5D4F5',
200: '#CBA8EB',
300: '#B17DE1',
400: '#9751D7',
500: '#7D26CD',
600: '#641EA4',
700: '#4B177B',
800: '#320F52',
900: '#190829',
},
blue: {
50: '#EDF9FF',
100: '#D7F2FF',
200: '#AFE5FF',
300: '#86D9FF',
400: '#5ECCFF',
500: '#36BFFF',
600: '#2B99CC',
700: '#207399',
800: '#164C66',
900: '#0B2633',
},
yellow: {
50: '#fffbf2',
100: '#fff0d1',
200: '#fee5b0',
300: '#fdd889',
400: '#fdcb61',
500: '#fcbe3a',
600: '#ca982e',
700: '#977223',
800: '#654c17',
900: '#32260c',
},
maroon: {
50: '#FFF0F7',
100: '#FFCFE6',
200: '#FFABD2',
300: '#EC7DB1',
400: '#D45892',
500: '#B33771',
600: '#9D255D',
700: '#801044',
800: '#690735',
900: '#42001F',
},
}
const isValidHex = (hex: string) => /^#([A-Fa-f0-9]{3,4}){1,2}$/.test(hex)
const getChunksFromString = (st: string, chunkSize: number) => st.match(new RegExp(`.{${chunkSize}}`, 'g'))

113
packages/nc-gui/windi.config.ts

@ -11,7 +11,7 @@ import animations from '@windicss/plugin-animations'
// @ts-expect-error no types for plugin-question-mark
import questionMark from '@windicss/plugin-question-mark'
import { theme as colors, themeColors, themeV2Colors } from './utils/colorsUtils'
import { theme as colors, themeColors, themeV2Colors, themeV3Colors } from './utils/colorsUtils'
const isEE = process.env.EE
@ -118,116 +118,7 @@ export default defineConfig({
...windiColors,
...themeColors,
...themeV2Colors,
brand: {
50: '#EBF0FF',
100: '#D6E0FF',
200: '#ADC2FF',
300: '#85A3FF',
400: '#5C85FF',
500: '#3366FF',
600: '#2952CC',
700: '#1F3D99',
800: '#142966',
900: '#0A1433',
},
gray: {
10: '#FCFCFC',
50: '#F9F9FA',
100: '#F4F4F5',
200: '#E7E7E9',
300: '#D5D5D9',
400: '#9AA2AF',
500: '#6A7184',
600: '#4A5268',
700: '#374151',
800: '#1F293A',
900: '#101015',
},
red: {
50: '#FFF2F1',
100: '#FFDBD9',
200: '#FFB7B2',
300: '#FF928C',
400: '#FF6E65',
500: '#FF4A3F',
600: '#E8463C',
700: '#CB3F36',
800: '#B23830',
900: '#7D2721',
},
pink: {
50: '#FFEEFB',
100: '#FED8F4',
200: '#FEB0E8',
300: '#FD89DD',
400: '#FD61D1',
500: '#FC3AC6',
600: '#CA2E9E',
700: '#972377',
800: '#65174F',
900: '#320C28',
},
orange: {
50: '#FFF5EF',
100: '#FEE6D6',
200: '#FDCDAD',
300: '#FCB483',
400: '#FB9B5A',
500: '#FA8231',
600: '#E1752C',
700: '#C86827',
800: '#964E1D',
900: '#4B270F',
},
purple: {
50: '#F3ECFA',
100: '#E5D4F5',
200: '#CBA8EB',
300: '#B17DE1',
400: '#9751D7',
500: '#7D26CD',
600: '#641EA4',
700: '#4B177B',
800: '#320F52',
900: '#190829',
},
blue: {
50: '#EDF9FF',
100: '#D7F2FF',
200: '#AFE5FF',
300: '#86D9FF',
400: '#5ECCFF',
500: '#36BFFF',
600: '#2B99CC',
700: '#207399',
800: '#164C66',
900: '#0B2633',
},
yellow: {
50: '#fffbf2',
100: '#fff0d1',
200: '#fee5b0',
300: '#fdd889',
400: '#fdcb61',
500: '#fcbe3a',
600: '#ca982e',
700: '#977223',
800: '#654c17',
900: '#32260c',
},
maroon: {
50: '#FFF0F7',
100: '#FFCFE6',
200: '#FFABD2',
300: '#EC7DB1',
400: '#D45892',
500: '#B33771',
600: '#9D255D',
700: '#801044',
800: '#690735',
900: '#42001F',
},
...themeV3Colors,
primary: 'rgba(var(--color-primary), var(--tw-bg-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-bg-opacity))',
dark: colors.dark,

9
packages/nocodb/src/services/columns.service.ts

@ -309,7 +309,14 @@ export class ColumnsService {
title: colBody.title,
});
}
if ('meta' in colBody && column.uidt === UITypes.Links) {
if (
'meta' in colBody &&
[
UITypes.Links,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
].includes(column.uidt)
) {
await Column.updateMeta({
colId: param.columnId,
meta: colBody.meta,

52
tests/playwright/pages/Dashboard/Details/FieldsPage.ts

@ -49,6 +49,26 @@ export class FieldsPage extends BasePage {
await this.getField({ title }).getByTestId('nc-field-restore-changes').click();
}
defaultValueBtn() {
const showDefautlValueBtn = this.addOrEditColumn.getByTestId('nc-show-default-value-btn');
return {
locator: showDefautlValueBtn,
isVisible: async () => {
return await showDefautlValueBtn.isVisible();
},
click: async () => {
if (await showDefautlValueBtn.isVisible()) {
await showDefautlValueBtn.waitFor();
await showDefautlValueBtn.click({ force: true });
await showDefautlValueBtn.waitFor({ state: 'hidden' });
await this.addOrEditColumn.locator('.nc-default-value-wrapper').waitFor({ state: 'visible' });
}
},
};
}
async createOrUpdate({
title,
type = UITypes.SingleLineText,
@ -102,6 +122,9 @@ export class FieldsPage extends BasePage {
await this.selectType({ type });
await this.rootPage.waitForTimeout(500);
// Click set default value to show default value input, on close field modal it will automacally hide input if value is not set
await this.defaultValueBtn().click();
switch (type) {
case 'SingleSelect':
case 'MultiSelect':
@ -184,10 +207,7 @@ export class FieldsPage extends BasePage {
.click();
break;
case 'Links':
await this.addOrEditColumn
.locator('.nc-ltar-relation-type >> .ant-radio')
.nth(relationType === 'Has Many' ? 1 : 2)
.click();
await this.addOrEditColumn.locator('.nc-ltar-relation-type').getByTestId(relationType).click();
await this.addOrEditColumn.locator('.ant-select-single').nth(1).click();
await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).fill(childTable);
await this.rootPage
@ -228,13 +248,27 @@ export class FieldsPage extends BasePage {
}
async selectType({ type }: { type: string }) {
await this.addOrEditColumn.locator('.ant-select-selector > .ant-select-selection-item').click();
if (await this.addOrEditColumn.getByTestId('nc-column-uitypes-options-list-wrapper').isVisible()) {
const searchInput = this.addOrEditColumn.locator('.nc-column-type-search-input >> input');
await this.addOrEditColumn.locator('.ant-select-selection-search-input[aria-expanded="true"]').waitFor();
await this.addOrEditColumn.locator('.ant-select-selection-search-input[aria-expanded="true"]').fill(type);
await searchInput.waitFor({ state: 'visible' });
// Select column type
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').locator(`text="${type}"`).click();
await searchInput.click();
await searchInput.fill(type);
await this.addOrEditColumn.locator('.nc-column-list-wrapper').getByTestId(type).waitFor();
await this.addOrEditColumn.locator('.nc-column-list-wrapper').getByTestId(type).click();
await this.addOrEditColumn.locator('.nc-column-type-input').waitFor();
} else {
await this.addOrEditColumn.locator('.ant-select-selector > .ant-select-selection-item').click();
await this.addOrEditColumn.locator('.ant-select-selection-search-input[aria-expanded="true"]').waitFor();
await this.addOrEditColumn.locator('.ant-select-selection-search-input[aria-expanded="true"]').fill(type);
// Select column type
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').getByTestId(type).click();
}
}
async saveChanges() {

15
tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts

@ -60,6 +60,11 @@ export class SelectOptionColumnPageObject extends BasePage {
async deleteOption({ columnTitle, index }: { index: number; columnTitle: string }) {
await this.column.openEdit({ title: columnTitle });
await this.column.get().locator(`[data-testid="select-column-option-${index}"]`).hover();
await this.column
.get()
.locator(`[data-testid="select-column-option-remove-${index}"]`)
.waitFor({ state: 'visible' });
await this.column.get().locator(`[data-testid="select-column-option-remove-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/);
@ -70,10 +75,20 @@ export class SelectOptionColumnPageObject extends BasePage {
async deleteOptionWithUndo({ columnTitle, index }: { index: number; columnTitle: string }) {
await this.column.openEdit({ title: columnTitle });
await this.column.get().locator(`[data-testid="select-column-option-${index}"]`).hover();
await this.column
.get()
.locator(`[data-testid="select-column-option-remove-${index}"]`)
.waitFor({ state: 'visible' });
await this.column.get().locator(`[data-testid="select-column-option-remove-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/);
await this.column.get().locator(`[data-testid="select-column-option-${index}"]`).hover();
await this.column
.get()
.locator(`[data-testid="select-column-option-remove-undo-${index}"]`)
.waitFor({ state: 'visible' });
await this.column.get().locator(`[data-testid="select-column-option-remove-undo-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).not.toHaveClass(/removed/);

2
tests/playwright/pages/Dashboard/Grid/Column/UserOptionColumn.ts

@ -80,7 +80,7 @@ export class UserOptionColumnPageObject extends BasePage {
}
async clearDefaultValue(): Promise<void> {
await this.column.get().locator('.nc-cell-user + svg.nc-icon').click();
await this.get().locator('.nc-cell-user + svg.nc-icon').click();
}
async verifyDefaultValueOptionCount({

74
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -32,6 +32,26 @@ export class ColumnPageObject extends BasePage {
await this.getColumnHeader(title).click();
}
defaultValueBtn() {
const showDefautlValueBtn = this.get().getByTestId('nc-show-default-value-btn');
return {
locator: showDefautlValueBtn,
isVisible: async () => {
return await showDefautlValueBtn.isVisible();
},
click: async () => {
if (await showDefautlValueBtn.isVisible()) {
await showDefautlValueBtn.waitFor();
await showDefautlValueBtn.click({ force: true });
await showDefautlValueBtn.waitFor({ state: 'hidden' });
await this.get().locator('.nc-default-value-wrapper').waitFor({ state: 'visible' });
}
},
};
}
async create({
title,
type = 'SingleLineText',
@ -86,7 +106,7 @@ export class ColumnPageObject extends BasePage {
await this.rootPage.waitForTimeout(500);
await this.fillTitle({ title });
await this.rootPage.waitForTimeout(500);
await this.selectType({ type });
await this.selectType({ type, isCreateColumn: true });
await this.rootPage.waitForTimeout(500);
switch (type) {
@ -96,11 +116,7 @@ export class ColumnPageObject extends BasePage {
case 'Duration':
if (format) {
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: format,
})
.click();
await this.rootPage.locator(`.ant-select-item .ant-select-item-option-content`).getByTestId(format).click();
}
break;
case 'Date':
@ -171,10 +187,7 @@ export class ColumnPageObject extends BasePage {
.click();
break;
case 'Links':
await this.get()
.locator('.nc-ltar-relation-type >> .ant-radio')
.nth(relationType === 'Has Many' ? 1 : 2)
.click();
await this.get().locator('.nc-ltar-relation-type').getByTestId(relationType).click();
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).fill(childTable);
await this.rootPage
@ -216,18 +229,30 @@ export class ColumnPageObject extends BasePage {
await this.get().locator('.nc-column-name-input').fill(title);
}
async selectType({ type, first }: { type: string; first?: boolean }) {
if (first) {
await this.get().locator('.ant-select-selector > .ant-select-selection-item').first().click();
async selectType({ type, first, isCreateColumn }: { type: string; first?: boolean; isCreateColumn?: boolean }) {
if (isCreateColumn || (await this.get().getByTestId('nc-column-uitypes-options-list-wrapper').isVisible())) {
const searchInput = this.get().locator('.nc-column-type-search-input >> input');
await searchInput.waitFor({ state: 'visible' });
await searchInput.click();
await searchInput.fill(type);
await this.get().locator('.nc-column-list-wrapper').getByTestId(type).waitFor();
await this.get().locator('.nc-column-list-wrapper').getByTestId(type).click();
await this.get().locator('.nc-column-type-input').waitFor();
} else {
await this.get().locator('.ant-select-selector > .ant-select-selection-item').click();
}
if (first) {
await this.get().locator('.ant-select-selector > .ant-select-selection-item').first().click();
} else {
await this.get().locator('.ant-select-selector > .ant-select-selection-item').click();
}
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').waitFor();
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').fill(type);
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').waitFor();
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').fill(type);
// Select column type
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').locator(`text="${type}"`).click();
// Select column type
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').getByTestId(type).click();
}
}
async changeReferencedColumnForQrCode({ titleOfReferencedColumn }: { titleOfReferencedColumn: string }) {
@ -305,17 +330,16 @@ export class ColumnPageObject extends BasePage {
await this.selectType({ type, first: true });
}
// Click set default value to show default value input, on close field modal it will automacally hide input if value is not set
await this.defaultValueBtn().click();
switch (type) {
case 'Formula':
await this.get().locator('.nc-formula-input').fill(formula);
break;
case 'Duration':
await this.get().locator('.ant-select-single').nth(1).click();
await this.rootPage
.locator(`.ant-select-item`, {
hasText: format,
})
.click();
await this.rootPage.locator(`.ant-select-item`).getByTestId(format).click();
break;
case 'DateTime':
// Date Format
@ -382,7 +406,7 @@ export class ColumnPageObject extends BasePage {
async save({ isUpdated }: { isUpdated?: boolean } = {}) {
await this.waitForResponse({
uiAction: async () => await this.get().locator('button:has-text("Save")').click(),
uiAction: async () => await this.get().locator('button[data-testid="nc-field-modal-submit-btn"]').click(),
requestUrlPathToMatch: 'api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
responseJsonMatcher: json => json['pageInfo'],

2
tests/playwright/pages/Dashboard/common/Toolbar/AddEditKanbanStack.ts

@ -14,7 +14,7 @@ export class ToolbarAddEditStackPage extends BasePage {
}
async addOption({ title }: { title: string }) {
await this.get().locator(`.ant-btn-dashed`).click();
await this.get().getByTestId('nc-add-select-option-btn').click();
await this.get().locator(`.nc-select-option >> input`).last().fill(title);
await this.get().locator(`[type="submit"]`).click();
await this.verifyToast({ message: 'Column updated' });

10
tests/playwright/tests/db/columns/columnDuration.spec.ts

@ -7,27 +7,27 @@ import { enableQuickRun } from '../../../setup/db';
// this will trigger update to previously committed data
const durationData = [
{
format: 'h:mm (e.g. 1:23)',
format: 'h:mm',
input: ['1:30', '30', '60', '80', '12:34', '15:130', '123123', '10'],
output: ['01:30', '00:30', '01:00', '01:20', '12:34', '17:10', '2052:03'],
},
{
format: 'h:mm:ss (e.g. 3:45, 1:23:40)',
format: 'h:mm:ss',
input: ['11:22:33', '1234', '50', '1:1111', '1:11:1111', '15:130', '123123', '10'],
output: ['11:22:33', '00:20:34', '00:00:50', '00:19:31', '01:29:31', '00:17:10', '34:12:03'],
},
{
format: 'h:mm:ss.s (e.g. 3:34.6, 1:23:40.0)',
format: 'h:mm:ss.s',
input: ['1234', '12:34', '12:34:56', '12:34:999', '12:999:56', '12:34:56.12', '12:34:56.199', '10'],
output: ['00:20:34.0', '00:12:34.0', '12:34:56.0', '12:50:39.0', '28:39:56.0', '12:34:56.1', '12:34:56.2'],
},
{
format: 'h:mm:ss.ss (e.g. 3.45.67, 1:23:40.00)',
format: 'h:mm:ss.ss',
input: ['1234', '12:34', '12:34:56', '12:34:999', '12:999:56', '12:34:56.12', '12:34:56.199', '10'],
output: ['00:20:34.00', '00:12:34.00', '12:34:56.00', '12:50:39.00', '28:39:56.00', '12:34:56.12', '12:34:56.20'],
},
{
format: 'h:mm:ss.sss (e.g. 3.45.678, 1:23:40.000)',
format: 'h:mm:ss.sss',
input: ['1234', '12:34', '12:34:56', '12:34:999', '12:999:56', '12:34:56.12', '12:34:56.199', '10'],
output: [
'00:20:34.000',

2
tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts

@ -43,7 +43,7 @@ test.describe('LTAR create & update', () => {
title: 'Link1-2mm',
type: 'Links',
childTable: 'Sheet2',
relationType: 'Many To many',
relationType: 'Many to Many',
});
await dashboard.treeView.openTable({ title: 'Sheet2', networkResponse: false });

1
tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

@ -72,6 +72,7 @@ test.describe('Verify shortcuts', () => {
await page.keyboard.press('Alt+c');
await grid.column.fillTitle({ title: 'New Column' });
await grid.column.selectType({ type: UITypes.SingleLineText });
await grid.column.save();
await grid.column.verify({ title: 'New Column' });

Loading…
Cancel
Save