Browse Source

Merge pull request #8188 from nocodb/nc-feat/update-toolbar-menu-dropdown-with-new-design

Nc feat/update toolbar menu dropdown with new design
pull/8228/head
Ramesh Mane 8 months ago committed by GitHub
parent
commit
ef132ff4bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. BIN
      packages/nc-gui/assets/img/placeholder/no-search-result-found.png
  2. 70
      packages/nc-gui/assets/style.scss
  3. 4
      packages/nc-gui/components/nc/Select.vue
  4. 26
      packages/nc-gui/components/nc/Switch.vue
  5. 7
      packages/nc-gui/components/smartsheet/Form.vue
  6. 24
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  7. 74
      packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue
  8. 74
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  9. 4
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  10. 184
      packages/nc-gui/components/smartsheet/toolbar/FieldListWithSearch.vue
  11. 130
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  12. 23
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  13. 2
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  14. 106
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  15. 24
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  16. 5
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  17. 7
      packages/nc-gui/composables/useViewColumns.ts
  18. 3
      packages/nc-gui/lang/en.json
  19. 2
      packages/nc-gui/windi.config.ts
  20. 4
      tests/playwright/pages/Dashboard/common/Toolbar/Groupby.ts

BIN
packages/nc-gui/assets/img/placeholder/no-search-result-found.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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

@ -37,7 +37,7 @@ body {
}
.rc-virtual-list-holder-inner {
@apply !px-1.5
@apply !px-1.5;
}
.ant-layout-header {
height: var(--topbar-height) !important;
@ -51,13 +51,17 @@ main {
@apply m-0 h-full w-full bg-white;
}
.nc-input-md {
@apply !rounded-lg !py-2 !px-3 mb-1;
}
.mobile {
.nc-scrollbar-md, .nc-scrollbar-lg, .nc-scrollbar-x-md, .nc-scrollbar-dark-md, .nc-scrollbar-x-md-dark, .nc-scrollbar-x-lg {
.nc-scrollbar-md,
.nc-scrollbar-lg,
.nc-scrollbar-x-md,
.nc-scrollbar-dark-md,
.nc-scrollbar-x-md-dark,
.nc-scrollbar-x-lg {
&::-webkit-scrollbar {
width: 0px;
}
@ -116,7 +120,6 @@ main {
overflow-x: auto !important;
scrollbar-width: thin !important;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
@ -131,7 +134,6 @@ main {
-webkit-border-radius: 10px;
border-radius: 10px;
width: 4px;
@apply bg-gray-200;
}
@ -145,7 +147,6 @@ main {
overflow-x: hidden;
scrollbar-width: thin !important;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
@ -177,7 +178,6 @@ main {
overflow-x: auto !important;
scrollbar-width: thin !important;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
@ -192,14 +192,11 @@ main {
-webkit-border-radius: 10px;
border-radius: 10px;
width: 4px;
background-color: rgba(0, 0, 0, 0.3)
background-color: rgba(0, 0, 0, 0.3);
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.4)
background-color: rgba(0, 0, 0, 0.4);
}
}
@ -220,7 +217,6 @@ main {
-webkit-border-radius: 10px;
border-radius: 10px;
width: 8px;
@apply bg-gray-200;
}
@ -255,11 +251,11 @@ a {
.rc-virtual-list-scrollbar {
@apply !w-1;
}
.rc-virtual-list-scrollbar-thumb{
.rc-virtual-list-scrollbar-thumb {
@apply !bg-gray-200;
&:hover{
&:hover {
@apply !bg-gray-300;
}
}
@ -465,9 +461,9 @@ a {
.ant-dropdown-menu-submenu {
@apply !py-0;
&.ant-dropdown-menu-submenu-popup{
@apply border-1 border-gray-200
&.ant-dropdown-menu-submenu-popup {
@apply border-1 border-gray-200;
}
.ant-dropdown-menu,
.ant-menu {
@ -545,11 +541,11 @@ a {
@apply bg-gray-300 bg-opacity-20;
}
.ant-select-item-option:hover{
.ant-select-item-option:hover {
@apply !bg-gray-100;
}
.ant-select-item-option-selected{
.ant-select-item-option-selected {
@apply !bg-white;
}
/* Hide the element with id nc-selected-item-icon */
@ -658,7 +654,7 @@ a {
}
.nc-toolbar-dropdown {
@apply !rounded-2xl;
@apply !rounded-lg;
}
input[type='number'] {
@ -712,7 +708,8 @@ input[type='number'] {
.nc-emoji {
@apply xs:(text-lg);
}
.material-symbols, .nc-icon {
.material-symbols,
.nc-icon {
@apply !xs:(text-xl -mt-0.25);
}
@ -729,7 +726,6 @@ input[type='number'] {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
}
.nc-button.ant-btn.nc-sidebar-node-btn.nc-sidebar-expand {
@apply xs:(opacity-100 hover:bg-gray-50);
@ -740,18 +736,18 @@ input[type='number'] {
.ant-message-notice-content {
@apply !rounded-md;
.ant-message-custom-content{
.ant-message-custom-content {
@apply flex items-center;
}
}
svg.nc-cell-icon, svg.nc-virtual-cell-icon {
svg.nc-cell-icon,
svg.nc-virtual-cell-icon {
@apply w-1em h-1em flex-none;
font-size: 1rem;
}
// For select type field list layout
// For select type field list layout
.nc-field-layout-list {
@apply !flex !flex-col !items-start w-full !space-y-0.5 !max-w-full;
@ -786,3 +782,21 @@ svg.nc-cell-icon, svg.nc-virtual-cell-icon {
}
}
.nc-toolbar-dropdown-search-field-input {
@apply !rounded-lg;
.nc-search-icon {
@apply text-gray-400;
}
&:hover .nc-search-icon,
&.ant-input-affix-wrapper-focused .nc-search-icon {
@apply text-gray-800;
}
}
// switch - on tab focus show outline
.ant-switch:focus-visible,
.ant-switch-checked:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}

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

@ -106,7 +106,7 @@ const onChange = (value: string) => {
}
.nc-select-dropdown {
@apply !rounded-xl py-1.5;
@apply !rounded-lg py-1.5;
.rc-virtual-list-holder {
overflow-y: auto;
@ -129,7 +129,7 @@ const onChange = (value: string) => {
}
&::-webkit-scrollbar-thumb {
width: 4px;
@apply bg-gray-300;
@apply bg-gray-300 rounded-md;
}
&::-webkit-scrollbar-thumb:hover {
@apply bg-gray-400;

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

@ -1,5 +1,5 @@
<script lang="ts" setup>
const props = withDefaults(defineProps<{ checked: boolean; disabled?: boolean; size?: 'default' | 'small' }>(), {
const props = withDefaults(defineProps<{ checked: boolean; disabled?: boolean; size?: 'default' | 'small' | 'xsmall' }>(), {
size: 'small',
})
@ -31,3 +31,27 @@ const onChange = (e: boolean) => {
<slot />
</span>
</template>
<style lang="scss" scoped>
.size-xsmall {
@apply h-3.5 min-w-[26px] leading-[14px];
:deep(.ant-switch-handle) {
@apply h-[10px] w-[10px] top-[2px] left-[calc(100%_-_24px)];
}
:deep(.ant-switch-inner) {
@apply !mr-[5px] !ml-[18px] !my-0;
}
&.ant-switch-checked {
:deep(.ant-switch-handle) {
@apply left-[calc(100%_-_12px)];
}
:deep(.ant-switch-inner) {
@apply !mr-[18px] !ml-[5px];
}
}
}
</style>

7
packages/nc-gui/components/smartsheet/Form.vue

@ -2028,13 +2028,6 @@ useEventListener(
}
}
}
.nc-form-wrapper {
.ant-switch:focus-visible,
.ant-switch-checked:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
}
</style>
<style lang="scss">

24
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -337,15 +337,16 @@ function isDateType(uidt: UITypes) {
<div
class="menu-filter-dropdown"
:class="{
'max-h-[max(80vh,500px)] min-w-112 py-6 pl-6': !nested,
'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !nested,
'w-full ': nested,
'py-4': !filters.length,
}"
>
<div
v-if="filters && filters.length"
ref="wrapperDomRef"
class="flex flex-col gap-y-3 nc-filter-grid pb-2 w-full"
:class="{ 'max-h-420px nc-scrollbar-md pr-3.5 nc-filter-top-wrapper': !nested }"
class="flex flex-col gap-y-3 nc-filter-grid w-full"
:class="{ 'max-h-420px nc-scrollbar-thin nc-filter-top-wrapper pr-4 my-2 py-1': !nested }"
@click.stop
>
<template v-for="(filter, i) in filters" :key="i">
@ -544,7 +545,14 @@ function isDateType(uidt: UITypes) {
</div>
<template v-if="isEeUI && !isPublic">
<div v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)" ref="addFiltersRowDomRef" class="flex gap-2">
<div
v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)"
ref="addFiltersRowDomRef"
class="flex gap-2"
:class="{
'mt-1 mb-2': filters.length,
}"
>
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
@ -563,7 +571,13 @@ function isDateType(uidt: UITypes) {
</div>
</template>
<template v-else>
<div ref="addFiltersRowDomRef" class="flex gap-2">
<div
ref="addFiltersRowDomRef"
class="flex gap-2"
:class="{
'mt-1 mb-2': filters.length,
}"
>
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />

74
packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue

@ -12,12 +12,6 @@ const emits = defineEmits(['created'])
const { isParentOpen, columns } = toRefs(props)
const inputRef = ref()
const search = ref('')
const activeFieldIndex = ref(-1)
const activeView = inject(ActiveViewInj, ref())
const meta = inject(MetaInj, ref())
@ -58,72 +52,22 @@ const options = computed<ColumnType[]>(
)
}
})
.filter((c: ColumnType) => !groupBy.value.find((g) => g.column?.id === c.id))
.filter((c: ColumnType) => c.title?.toLowerCase().includes(search.value.toLowerCase())) ?? [],
.filter((c: ColumnType) => !groupBy.value.find((g) => g.column?.id === c.id)) ?? [],
)
const onClick = (column: ColumnType) => {
emits('created', column)
}
watch(
isParentOpen,
() => {
if (!isParentOpen.value) return
setTimeout(() => {
inputRef.value?.focus()
}, 100)
},
{
immediate: true,
},
)
onMounted(() => {
search.value = ''
activeFieldIndex.value = -1
})
const onArrowDown = () => {
activeFieldIndex.value = Math.min(activeFieldIndex.value + 1, options.value.length - 1)
}
const onArrowUp = () => {
activeFieldIndex.value = Math.max(activeFieldIndex.value - 1, 0)
}
</script>
<template>
<div
class="flex flex-col w-full pt-4 pb-2 min-w-64 nc-group-by-create-modal"
@keydown.arrow-down.prevent="onArrowDown"
@keydown.arrow-up.prevent="onArrowUp"
@keydown.enter.prevent="onClick(options[activeFieldIndex])"
>
<div class="flex pb-3 px-4 border-b-1 border-gray-100">
<input ref="inputRef" v-model="search" class="w-full focus:outline-none" :placeholder="$t('msg.selectFieldToGroup')" />
</div>
<div class="flex-col w-full max-h-100 max-w-76 nc-scrollbar-md !overflow-y-auto">
<div v-if="!options.length" class="flex text-gray-500 px-4 py-2.25">{{ $t('general.empty') }}</div>
<div
v-for="(option, index) in options"
:key="index"
v-e="['c:group-by:add:column:select']"
class="flex flex-row h-10 items-center gap-x-1.5 px-2.5 hover:bg-gray-100 cursor-pointer nc-group-by-column-search-item"
:class="{
'bg-gray-100': activeFieldIndex === index,
}"
@click="onClick(option)"
>
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate">
<template #title> {{ option.title }}</template>
<span>
{{ option.title }}
</span>
</NcTooltip>
</div>
</div>
<div class="nc-group-by-create-modal">
<SmartsheetToolbarFieldListWithSearch
:is-parent-open="isParentOpen"
:search-input-placeholder="$t('msg.selectFieldToGroup')"
:options="options"
toolbar-menu="groupBy"
@selected="onClick"
/>
</div>
</template>

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

@ -11,12 +11,6 @@ const emits = defineEmits(['created'])
const { isParentOpen } = toRefs(props)
const inputRef = ref()
const search = ref('')
const activeFieldIndex = ref(-1)
const activeView = inject(ActiveViewInj, ref())
const meta = inject(MetaInj, ref())
@ -55,72 +49,22 @@ const options = computed<ColumnType[]>(
/** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
}
})
.filter((c: ColumnType) => !sorts.value.find((s) => s.fk_column_id === c.id))
.filter((c: ColumnType) => c.title?.toLowerCase().includes(search.value.toLowerCase())) ?? [],
.filter((c: ColumnType) => !sorts.value.find((s) => s.fk_column_id === c.id)) ?? [],
)
const onClick = (column: ColumnType) => {
emits('created', column)
}
watch(
isParentOpen,
() => {
if (!isParentOpen.value) return
setTimeout(() => {
inputRef.value?.focus()
}, 100)
},
{
immediate: true,
},
)
onMounted(() => {
search.value = ''
activeFieldIndex.value = -1
})
const onArrowDown = () => {
activeFieldIndex.value = Math.min(activeFieldIndex.value + 1, options.value.length - 1)
}
const onArrowUp = () => {
activeFieldIndex.value = Math.max(activeFieldIndex.value - 1, 0)
}
</script>
<template>
<div
class="flex flex-col w-full pt-4 pb-2 min-w-64 nc-sort-create-modal"
@keydown.arrow-down.prevent="onArrowDown"
@keydown.arrow-up.prevent="onArrowUp"
@keydown.enter.prevent="onClick(options[activeFieldIndex])"
>
<div class="flex pb-3 px-4 border-b-1 border-gray-100">
<input ref="inputRef" v-model="search" class="w-full focus:outline-none" :placeholder="$t('msg.selectFieldToSort')" />
</div>
<div class="flex-col w-full max-h-100 max-w-76 nc-scrollbar-md !overflow-y-auto">
<div v-if="!options.length" class="flex text-gray-500 px-4 py-2.25">{{ $t('general.empty') }}</div>
<div
v-for="(option, index) in options"
:key="index"
v-e="['c:sort:add:column:select']"
class="flex flex-row h-10 items-center gap-x-1.5 px-2.5 rounded-md m-1.5 hover:bg-gray-100 cursor-pointer nc-sort-column-search-item"
:class="{
'bg-gray-100': activeFieldIndex === index,
}"
@click="onClick(option)"
>
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate" show-on-truncate-only>
<template #title> {{ option.title }}</template>
<template #default>
{{ option.title }}
</template>
</NcTooltip>
</div>
</div>
<div class="nc-sort-create-modal">
<SmartsheetToolbarFieldListWithSearch
:is-parent-open="isParentOpen"
:search-input-placeholder="$t('msg.selectFieldToSort')"
:options="options"
toolbar-menu="sort"
@selected="onClick"
/>
</div>
</template>

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

@ -94,8 +94,8 @@ if (!localValue.value && allowEmpty !== true) {
>
<a-select-option v-for="option in options" :key="option.value" :label="option.label" :value="option.value">
<div class="flex items-center w-full justify-between w-full gap-2 max-w-50">
<div class="flex gap-1 flex-1 items-center truncate items-center h-full">
<component :is="option.icon" class="min-w-5 !mx-0" />
<div class="flex gap-1.5 flex-1 items-center truncate items-center h-full">
<component :is="option.icon" class="!w-3.5 !h-3.5 !mx-0 !text-gray-500" />
<NcTooltip
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
class="max-w-[15rem] truncate select-none"

184
packages/nc-gui/components/smartsheet/toolbar/FieldListWithSearch.vue

@ -0,0 +1,184 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
const props = defineProps<{
// As we need to focus search box when the parent is opened
isParentOpen: boolean
toolbarMenu: 'groupBy' | 'sort' | 'globalSearch'
searchInputPlaceholder?: string
selectedOptionId?: string
options: ColumnType[]
showSelectedOption?: boolean
}>()
const emits = defineEmits<{ selected: [ColumnType] }>()
const { isParentOpen, toolbarMenu, searchInputPlaceholder, selectedOptionId, options, showSelectedOption } = toRefs(props)
const searchQuery = ref('')
const filteredOptions = computed(
() => options.value?.filter((c: ColumnType) => c.title?.toLowerCase().includes(searchQuery.value.toLowerCase())) ?? [],
)
const inputRef = ref()
const activeFieldIndex = ref(-1)
const configByToolbarMenu = computed(() => {
switch (toolbarMenu.value) {
case 'groupBy':
return {
selectOptionEvent: ['c:group-by:add:column:select'],
optionClassName: 'nc-group-by-column-search-item',
}
case 'sort':
return {
selectOptionEvent: ['c:sort:add:column:select'],
optionClassName: 'nc-sort-column-search-item',
}
case 'globalSearch':
return {
selectOptionEvent: ['c:search:field:select'],
optionClassName: '',
}
default:
return {
selectOptionEvent: undefined,
optionClassName: '',
}
}
})
const onClick = (column: ColumnType) => {
if (!column) return
emits('selected', column)
}
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])
} else if (filteredOptions.value[0]) {
onClick(filteredOptions.value[activeFieldIndex.value])
}
}
onMounted(() => {
searchQuery.value = ''
activeFieldIndex.value = -1
})
watch(
isParentOpen,
() => {
if (!isParentOpen.value) return
searchQuery.value = ''
setTimeout(() => {
inputRef.value?.focus()
}, 100)
},
{
immediate: true,
},
)
</script>
<template>
<div
class="flex flex-col pt-2 w-64"
@keydown.arrow-down.prevent="onArrowDown"
@keydown.arrow-up.prevent="onArrowUp"
@keydown.enter.prevent="onClick(filteredOptions[activeFieldIndex])"
>
<div class="w-full pb-2 px-2" @click.stop>
<a-input
ref="inputRef"
v-model:value="searchQuery"
:placeholder="searchInputPlaceholder || $t('placeholder.searchFields')"
class="nc-toolbar-dropdown-search-field-input"
@keydown.enter.stop="handleKeydownEnter"
@change="activeFieldIndex = 0"
>
<template #prefix> <GeneralIcon icon="search" class="nc-search-icon h-3.5 w-3.5 mr-1" /> </template
></a-input>
</div>
<div class="nc-field-list-wrapper flex-col w-full max-h-100 nc-scrollbar-thin !overflow-y-auto px-2 pb-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"
v-e="configByToolbarMenu.selectOptionEvent"
class="flex w-full py-[5px] items-center justify-between px-2 hover:bg-gray-100 cursor-pointer rounded-md"
:class="[
`${configByToolbarMenu.optionClassName}`,
`nc-field-list-option-${index}`,
{
'bg-gray-100 nc-field-list-option-active': activeFieldIndex === index,
},
]"
@click="onClick(option)"
>
<div
class="flex items-center gap-x-1.5"
:class="{
'max-w-[calc(100%_-_28px)]': showSelectedOption,
'max-w-full': !showSelectedOption,
}"
>
<SmartsheetHeaderIcon :column="option" class="!w-3.5 !h-3.5 !text-gray-500" />
<NcTooltip class="truncate" show-on-truncate-only>
<template #title> {{ option.title }}</template>
<span>
{{ option.title }}
</span>
</NcTooltip>
</div>
<GeneralIcon
v-if="showSelectedOption && option.id === selectedOptionId"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.nc-field-list-wrapper {
max-height: min(400px, calc(100vh - 120px));
}
</style>

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

@ -300,6 +300,19 @@ const showSystemField = computed({
},
})
const isDragging = ref<boolean>(false)
const fieldsMenuSearchRef = ref<HTMLInputElement>()
watch(open, (value) => {
if (!value) return
filterQuery.value = ''
setTimeout(() => {
fieldsMenuSearchRef.value?.focus()
}, 100)
})
useMenuCloseOnEsc(open)
</script>
@ -337,42 +350,60 @@ useMenuCloseOnEsc(open)
</div>
<template #overlay>
<div class="p-4 pr-0 bg-white w-90 rounded-2xl nc-table-toolbar-menu" data-testid="nc-fields-menu" @click.stop>
<div
class="pt-2 bg-white w-full min-w-72 max-w-80 rounded-lg nc-table-toolbar-menu"
data-testid="nc-fields-menu"
@click.stop
>
<div
v-if="!filterQuery && !isPublic && (activeView?.type === ViewTypes.GALLERY || activeView?.type === ViewTypes.KANBAN)"
class="flex flex-col gap-y-2 pr-4 mb-6"
class="flex flex-col gap-y-2 px-2 mb-6"
>
<div class="flex text-sm select-none">Select cover image field</div>
<a-select
v-model:value="coverImageColumnId"
:options="coverOptions"
class="w-full"
dropdown-class-name="nc-dropdown-cover-image"
dropdown-class-name="nc-dropdown-cover-image !rounded-lg"
@click.stop
>
<template #suffixIcon><GeneralIcon class="text-gray-700" icon="arrowDown" /></template>
</a-select>
</div>
<div class="pr-4" @click.stop>
<a-input v-model:value="filterQuery" :placeholder="$t('placeholder.searchFields')" class="!rounded-lg">
<template #prefix> <img class="h-3.5 w-3.5 mr-1" src="~/assets/nc-icons/search.svg" /> </template
<div class="px-2" @click.stop>
<a-input
ref="fieldsMenuSearchRef"
v-model:value="filterQuery"
:placeholder="$t('placeholder.searchFields')"
class="nc-toolbar-dropdown-search-field-input"
>
<template #prefix> <GeneralIcon icon="search" class="nc-search-icon h-3.5 w-3.5 mr-1" /> </template
></a-input>
</div>
<div v-if="!filterQuery" class="pr-4">
<div class="pt-0.25 w-full bg-gray-50"></div>
</div>
<div class="flex flex-col my-1.5 nc-scrollbar-md max-h-[47.5vh] pr-3">
<div class="flex flex-col mt-2 pb-2 nc-scrollbar-thin max-h-[47vh] px-2">
<div class="nc-fields-list">
<div
v-if="!fields?.filter((el) => el.title.toLowerCase().includes(filterQuery.toLowerCase())).length"
class="px-0.5 py-2 text-gray-500"
class="px-2 py-6 text-gray-500 flex flex-col items-center gap-6 text-center"
>
{{ $t('title.noFieldsFound') }}
<img
src="~assets/img/placeholder/no-search-result-found.png"
class="!w-[164px] flex-none"
alt="No search results found"
/>
{{ $t('title.noResultsMatchedYourSearch') }}
</div>
<Draggable v-model="fields" item-key="id" @change="onMove($event)">
<Draggable
v-model="fields"
item-key="id"
ghost-class="nc-fields-menu-items-ghost"
@change="onMove($event)"
@start="isDragging = true"
@end="isDragging = false"
>
<template #item="{ element: field }">
<div
v-if="
@ -382,13 +413,13 @@ useMenuCloseOnEsc(open)
"
:key="field.id"
:data-testid="`nc-fields-menu-${field.title}`"
class="px-2 py-2 flex flex-row items-center first:border-t-1 border-b-1 border-x-1 first:rounded-t-lg last:rounded-b-lg border-gray-200"
class="pl-2 flex flex-row items-center rounded-md hover:bg-gray-100"
@click.stop
>
<component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-600 mr-1" />
<div
v-e="['a:fields:show-hide']"
class="flex flex-row items-center w-full truncate cursor-pointer ml-1"
class="flex flex-row items-center w-full truncate cursor-pointer ml-1 py-[5px] pr-2"
@click="
() => {
field.show = !field.show
@ -396,8 +427,8 @@ useMenuCloseOnEsc(open)
}
"
>
<component :is="getIcon(metaColumnById[field.fk_column_id])" />
<NcTooltip class="flex-1 px-1 truncate" show-on-truncate-only>
<component :is="getIcon(metaColumnById[field.fk_column_id])" class="!w-3.5 !h-3.5 !text-gray-500" />
<NcTooltip class="flex-1 pl-1 pr-2 truncate" show-on-truncate-only :disabled="isDragging">
<template #title>
{{ field.title }}
</template>
@ -438,54 +469,32 @@ useMenuCloseOnEsc(open)
<component :is="iconMap.underline" class="!w-3 !h-3" />
</NcButton>
</div>
<NcSwitch :checked="field.show" :disabled="field.isViewEssentialField" @change="$t('a:fields:show-hide')" />
<NcSwitch
:checked="field.show"
:disabled="field.isViewEssentialField"
size="xsmall"
@change="$t('a:fields:show-hide')"
/>
</div>
<div class="flex-1" />
</div>
</template>
<template v-if="activeView?.type === ViewTypes.GRID" #header>
<div
v-if="gridDisplayValueField && filteredFieldList[0].title.toLowerCase().includes(filterQuery.toLowerCase())"
:key="`pv-${gridDisplayValueField.id}`"
:class="{
'rounded-t-lg': filteredFieldList.length > 1,
'rounded-lg': filteredFieldList.length === 1,
}"
:data-testid="`nc-fields-menu-${gridDisplayValueField.title}`"
class="pl-7.4 pr-2 py-2 flex flex-row items-center border-1 border-gray-200"
@click.stop
>
<component :is="getIcon(metaColumnById[filteredFieldList[0].fk_column_id as string])" />
<NcTooltip class="px-1 flex-1 truncate" show-on-truncate-only>
<template #title>{{ filteredFieldList[0].title }}</template>
<template #default>{{ filteredFieldList[0].title }}</template>
</NcTooltip>
<NcSwitch :checked="true" :disabled="true" />
</div>
</template>
</Draggable>
</div>
</div>
<div class="flex pr-4 mt-1 gap-2">
<NcButton
v-if="!filterQuery"
class="nc-fields-show-all-fields !text-gray-500 !w-1/2"
size="small"
type="ghost"
@click="showAllColumns = !showAllColumns"
>
{{ showAllColumns ? $t('title.hideAll') : $t('general.showAll') }} {{ $t('objects.fields').toLowerCase() }}
<div v-if="!filterQuery" class="flex px-2 gap-2 py-2">
<NcButton class="nc-fields-show-all-fields" size="small" type="ghost" @click="showAllColumns = !showAllColumns">
{{ showAllColumns ? 'Hide all' : 'Show all' }} fields
</NcButton>
<NcButton
v-if="!isPublic && !filterQuery"
class="nc-fields-show-system-fields !text-gray-500 !w-1/2"
v-if="!isPublic"
class="nc-fields-show-system-fields"
size="small"
type="ghost"
@click="showSystemField = !showSystemField"
>
{{ showSystemField ? $t('title.hideSystemFields') : $t('activity.showSystemFields') }}
{{ showSystemField ? 'Hide system fields' : 'Show system fields' }}
</NcButton>
</div>
</div>
@ -494,15 +503,16 @@ useMenuCloseOnEsc(open)
</template>
<style lang="scss" scoped>
// :deep(.ant-checkbox-inner) {
// @apply transform scale-60;
// }
// :deep(.ant-checkbox) {
// @apply top-auto;
// }
:deep(.xxsmall) {
@apply !min-w-0;
}
.nc-fields-menu-items-ghost {
@apply bg-gray-50;
}
.nc-fields-show-all-fields,
.nc-fields-show-system-fields {
@apply !text-xs !w-1/2 !text-gray-500 !border-none bg-gray-100 hover:(!text-gray-600 bg-gray-200);
}
</style>

23
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -251,19 +251,14 @@ watch(meta, async () => {
/>
<div
v-else
:class="{ ' min-w-[400px]': _groupBy.length }"
class="flex flex-col bg-white overflow-auto nc-group-by-list menu-filter-dropdown max-h-[max(80vh,500px)] py-6 pl-6"
class="flex flex-col bg-white overflow-auto nc-group-by-list menu-filter-dropdown max-h-[max(80vh,500px)] min-w-102 pt-2 pb-2 pl-4"
data-testid="nc-group-by-menu"
>
<div
class="group-by-grid pb-1 max-h-100 nc-scrollbar-md pr-5"
:class="{ 'mb-2': availableColumns.length && fieldsToGroupBy.length > _groupBy.length && _groupBy.length < 3 }"
@click.stop
>
<div class="group-by-grid max-h-100 nc-scrollbar-thing pr-4 py-2" @click.stop>
<template v-for="[i, group] of Object.entries(_groupBy)" :key="`grouped-by-${group.fk_column_id}`">
<LazySmartsheetToolbarFieldListAutoCompleteDropdown
v-model="group.fk_column_id"
class="caption nc-sort-field-select"
class="caption nc-sort-field-select w-44 flex flex-grow"
:columns="fieldsToGroupBy"
:allow-empty="true"
@change="saveGroupBy"
@ -284,7 +279,7 @@ watch(meta, async () => {
:key="j"
:value="option.value"
>
<div class="flex items-center justify-between gap-2">
<div class="w-full flex items-center justify-between gap-2">
<div class="truncate flex-1">{{ option.text }}</div>
<component
:is="iconMap.check"
@ -296,10 +291,10 @@ watch(meta, async () => {
</a-select-option>
</NcSelect>
<a-tooltip placement="right" title="Remove">
<a-tooltip placement="right" title="Remove" class="flex-none min-w-40">
<NcButton
v-e="['c:group-by:remove']"
class="nc-group-by-item-remove-btn"
class="nc-group-by-item-remove-btn min-w-40"
size="small"
type="text"
@click.stop="removeFieldFromGroupBy(i)"
@ -317,7 +312,7 @@ watch(meta, async () => {
>
<NcButton
v-e="['c:group-by:add']"
class="nc-add-group-by-btn !text-brand-500"
class="nc-add-group-by-btn !text-brand-500 mt-1 mb-2"
style="width: fit-content"
size="small"
type="text"
@ -346,7 +341,7 @@ watch(meta, async () => {
<style scoped>
.group-by-grid {
display: grid;
grid-template-columns: auto 150px 22px;
@apply gap-[12px];
grid-template-columns: auto 150px auto;
@apply gap-x-2 gap-y-3;
}
</style>

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

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

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

@ -1,13 +1,12 @@
<script lang="ts" setup>
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import type { TableType } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk'
import {
ActiveViewInj,
ReloadViewDataHookInj,
computed,
iconMap,
inject,
onClickOutside,
ref,
useFieldQuery,
useSmartsheetStoreOrThrow,
@ -25,21 +24,8 @@ const isDropdownOpen = ref(false)
const { isMobileMode } = useGlobal()
const isFocused = ref(false)
const searchDropdown = ref(null)
onClickOutside(searchDropdown, () => (isDropdownOpen.value = false))
const columns = computed(() =>
(meta.value as TableType)?.columns
?.filter((column) => !isSystemColumn(column) && column?.uidt !== UITypes.Links)
?.map((column) => ({
value: column.id,
label: column.title,
column,
primaryValue: column.pv,
})),
const columns = computed(
() => (meta.value as TableType)?.columns?.filter((column) => !isSystemColumn(column) && column?.uidt !== UITypes.Links) ?? [],
)
watch(
@ -59,10 +45,12 @@ function onPressEnter() {
const displayColumnLabel = computed(() => {
if (search.value.field) {
// use search field label if specified
return columns.value?.find((column) => column.value === search.value.field)?.label
return columns.value?.find((column) => column.id === search.value.field)?.title
}
// use primary value label by default
return columns.value?.find((column) => column.primaryValue)?.label
const pvColumn = columns.value?.find((column) => column.pv)
search.value.field = pvColumn?.id as string
return pvColumn?.title
})
watchDebounced(
@ -76,58 +64,46 @@ watchDebounced(
},
)
watch(columns, () => {
if (columns.value?.length) {
search.value.field = columns.value[0].value as string
}
})
const onSelectOption = (column: ColumnType) => {
search.value.field = column.id as string
isDropdownOpen.value = false
}
</script>
<template>
<div
class="flex flex-row border-1 rounded-lg h-8 xs:(h-10 ml-0) ml-1 border-gray-200 overflow-hidden"
:class="{ 'border-primary': search.query.length !== 0 || isFocused }"
class="flex flex-row border-1 rounded-lg h-8 xs:(h-10 ml-0) ml-1 border-gray-200 overflow-hidden focus-within:border-primary"
:class="{ 'border-primary': search.query.length !== 0 }"
>
<div
ref="searchDropdown"
class="flex items-center group relative px-2 cursor-pointer border-r-1 border-gray-200 hover:bg-gray-100"
:class="{ 'bg-gray-50 ': isDropdownOpen }"
@click="isDropdownOpen = !isDropdownOpen"
<NcDropdown
v-model:visible="isDropdownOpen"
:trigger="['click']"
overlay-class-name="nc-dropdown-toolbar-search-field-option"
>
<GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<div v-if="!isMobileMode" class="w-16 text-[0.75rem] font-medium text-gray-400 truncate">
{{ displayColumnLabel }}
</div>
<div class="xs:(text-gray-600) group-hover:text-gray-700 sm:(text-gray-400)">
<component :is="iconMap.arrowDown" class="text-sm text-inherit" />
</div>
<a-select
v-model:value="search.field"
v-e="['c:search:field:select:open']"
:open="isDropdownOpen"
size="small"
:dropdown-match-select-width="false"
dropdown-class-name="!rounded-lg nc-dropdown-toolbar-search-field-option max-w-64"
class="py-1 !absolute top-2 left-0 w-full h-full z-10 text-xs opacity-0"
@change="onPressEnter"
<div
class="flex items-center group px-2 cursor-pointer border-r-1 border-gray-200 hover:bg-gray-100"
:class="{ 'bg-gray-50 ': isDropdownOpen }"
@click="isDropdownOpen = !isDropdownOpen"
>
<a-select-option v-for="op of columns" :key="op.value" v-e="['c:search:field:select']" :value="op.value">
<div class="text-sm flex items-center gap-2">
<SmartsheetHeaderIcon :column="op.column" />
<NcTooltip class="truncate flex-1" placement="top" show-on-truncate-only>
<template #title>{{ op.label }}</template>
{{ op.label }}
</NcTooltip>
<component
:is="iconMap.check"
v-if="search.field === op.value"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</a-select>
</div>
<GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<div v-if="!isMobileMode" class="w-16 text-xs font-medium text-gray-400 truncate">
{{ displayColumnLabel }}
</div>
<div class="xs:(text-gray-600) group-hover:text-gray-700 sm:(text-gray-400)">
<component :is="iconMap.arrowDown" class="text-sm text-inherit" />
</div>
</div>
<template #overlay>
<SmartsheetToolbarFieldListWithSearch
:is-parent-open="isDropdownOpen"
:selected-option-id="search.field"
show-selected-option
:options="columns"
toolbar-menu="globalSearch"
@selected="onSelectOption"
/>
</template>
</NcDropdown>
<a-input
v-model:value="search.query"
@ -137,8 +113,6 @@ watch(columns, () => {
:bordered="false"
data-testid="search-data-input"
@press-enter="onPressEnter"
@focus="isFocused = true"
@blur="isFocused = false"
>
</a-input>
</div>

24
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -139,13 +139,8 @@ onMounted(() => {
</div>
<template #overlay>
<SmartsheetToolbarCreateSort v-if="!sorts.length" :is-parent-open="open" @created="addSort" />
<div
v-else
:class="{ 'min-w-102': sorts.length }"
class="py-6 pl-6 nc-filter-list max-h-[max(80vh,30rem)]"
data-testid="nc-sorts-menu"
>
<div class="sort-grid max-h-120 nc-scrollbar-md" :class="{ 'pr-3.5': sorts?.length }" @click.stop>
<div v-else class="pt-2 pb-2 pl-4 nc-filter-list max-h-[max(80vh,30rem)] min-w-102" data-testid="nc-sorts-menu">
<div class="sort-grid max-h-120 nc-scrollbar-thin pr-4 my-2 py-1" @click.stop>
<template v-for="(sort, i) of sorts" :key="i">
<SmartsheetToolbarFieldListAutoCompleteDropdown
v-model="sort.fk_column_id"
@ -160,7 +155,7 @@ onMounted(() => {
v-model:value="sort.direction"
class="shrink grow-0 nc-sort-dir-select"
:label="$t('labels.operation')"
dropdown-class-name="sort-dir-dropdown nc-dropdown-sort-dir"
dropdown-class-name="sort-dir-dropdown nc-dropdown-sort-dir !rounded-lg"
@click.stop
@select="saveOrUpdate(sort, i)"
>
@ -170,7 +165,7 @@ onMounted(() => {
v-e="['c:sort:operation:select']"
:value="option.value"
>
<div class="flex items-center justify-between gap-2">
<div class="w-full flex items-center justify-between gap-2">
<div class="truncate flex-1">{{ option.text }}</div>
<component
:is="iconMap.check"
@ -198,14 +193,13 @@ onMounted(() => {
v-if="availableColumns.length"
v-model:visible="showCreateSort"
:trigger="['click']"
class="mt-3"
overlay-class-name="nc-toolbar-dropdown"
>
<template v-if="isEeUI && !isPublic">
<NcButton
v-if="sorts.length < getPlanLimit(PlanLimitTypes.SORT_LIMIT)"
v-e="['c:sort:add']"
class="!text-brand-500"
class="!text-brand-500 mt-1 mb-2"
type="text"
size="small"
@click.stop="showCreateSort = true"
@ -219,7 +213,13 @@ onMounted(() => {
<span v-else></span>
</template>
<template v-else>
<NcButton v-e="['c:sort:add']" class="!text-brand-500" type="text" size="small" @click.stop="showCreateSort = true">
<NcButton
v-e="['c:sort:add']"
class="!text-brand-500 mt-1 mb-2"
type="text"
size="small"
@click.stop="showCreateSort = true"
>
<div class="flex gap-1 items-center">
<component :is="iconMap.plus" />
<!-- Add Sort Option -->

5
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -153,7 +153,10 @@ const onDelete = async () => {
>
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyViewID') }} </template>
<div class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group" @click="onViewIdCopy">
<div
class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
@click="onViewIdCopy"
>
<div class="flex text-xs font-bold text-gray-500 ml-1">
{{
$t('labels.viewIdColon', {

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

@ -220,7 +220,12 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
const filteredFieldList = computed(() => {
return (
fields.value?.filter((field: Field) => {
if (metaColumnById?.value?.[field.fk_column_id!]?.pv) return true
if (
metaColumnById?.value?.[field.fk_column_id!]?.pv &&
(!filterQuery.value || field.title.toLowerCase().includes(filterQuery.value.toLowerCase()))
) {
return true
}
// hide system columns if not enabled
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {

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

@ -433,7 +433,8 @@
},
"selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here",
"noOptionsFound": "No options found",
"surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?"
"surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?",
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"selectYear": "Select Year",

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

@ -46,6 +46,8 @@ export default defineConfig({
'color-transition': 'transition-colors duration-100 ease-in',
'scrollbar-thin-primary': 'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white',
'scrollbar-thin-dull': 'scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-100 scrollbar-track-white',
'nc-scrollbar-thin':
'scrollbar scrollbar-thin scrollbar-thumb-gray-200 hover:scrollbar-thumb-gray-300 scrollbar-track-transparent',
},
theme: {

4
tests/playwright/pages/Dashboard/common/Toolbar/Groupby.ts

@ -93,14 +93,14 @@ export class ToolbarGroupByPage extends BasePage {
await this.rootPage
.locator('.nc-group-by-create-modal')
.locator('.nc-group-by-column-search-item >> div', { hasText: regexTitle })
.locator('.nc-group-by-column-search-item', { hasText: regexTitle })
.scrollIntoViewIfNeeded();
// select column
const selectColumn = async () =>
await this.rootPage
.locator('.nc-group-by-create-modal')
.locator('.nc-group-by-column-search-item >> div', { hasText: regexTitle })
.locator('.nc-group-by-column-search-item', { hasText: regexTitle })
.click({ force: true });
await this.waitForResponse({

Loading…
Cancel
Save