mirror of https://github.com/nocodb/nocodb
Browse Source
* fix(nc-gui): update topbar breadcrumb divider * feat(nc-gui): custom list component setup * fix(nc-gui): update reload view data tooltip * feat(nc-gui): custom list component * feat(nc-gui): add table list menu * fix(nc-gui): small changes * fix(nc-gui): add bases list dropdown * fix(nc-gui): show chevron icon in mobile view * feat(nc-gui): add view list dropdown in topbar * fix(nc-gui): auto scroll selected list option on open dropdown * feat(nc-gui): add typedocs for each fun from custom list component * chore(nc-gui): add typedocs for new functions * fix(nc-gui): view search issue on default view * fix(nc-gui): reset selected option hover state on search input * fix(nc-gui): font weight issue * fix(nc-gui): show reload data topbar option * fix(nc-gui): change view action menu position * fix(nc-gui): font weight issue * feat(nc-gui): create new table/view from topbar * fix(nc-gui): update other page headers * fix(nc-gui): project view header * fix(nc-gui): update admin panel workspaces page header * fix(nc-gui): admin panel base/workspace user page header * fix(nc-gui): admin panel scroll issue * fix(nc-gui): update project home page * fix(nc-gui): table list scroll issue * chore(nc-gui): lint * fix(nc-gui): reset breadcrumb btn hover state on open dropdown * fix(nc-gui): review changes * fix(nc-gui): use slash icon instead of text * fix(nc-gui): pr review changes * fix(nc-gui): details tab height issue * fix(nc-gui): add user account pages breadcrumb * fix(nc-gui): hide rename view option * fix(nc-gui): disable scrollIntoView on base rename * fix(nc-gui): on rename view select text * fix(nc-gui): user menu overflow issue if sidebar baselist is scrollable * feat(nc-gui): use virtual scrolling for NcList component * fix(nc-gui): reduce chevron icon opacity * chore(nc-gui): lint * fix(nc-gui): ai review changes * fix(nc-gui): view rename input focus issue * fix(nc-gui): topbar width issue * fix(nc-gui): udpate toolbar height * fix(nc-gui): update chevron icon from breadcrumb * fix(nc-gui): update breadcrumb icon size * fix(nc-gui): add min width for breadcrumb * fix(nc-gui): add topbar bottom border * fix(nc-gui): details tab heigth and alignment issue * fix(nc-gui): hide basename and show only icon * fix(nc-gui): update NcList component * fix(nc-gui): update admin panel header * fix(nc-gui): add header in account settings pages * fix(nc-gui): add account pages header oss * fix(nc-gui): udpate max width * chore(nc-gui): lint * fix(nc-gui(: reduce topbar top padding * fix(nc-gui): typo error * fix(nc-gui): review changes * fix(nc-gui): review changes * fix(nc-gui): slash icon conflict * fix(nc-gui): review changes * fix(nc-gui): remove chevron icon & add list wrapper div to control height * fix(nc-gui): ncList keyboard navigation issue * chore(nc-gui): lintpull/9230/head
Ramesh Mane
3 months ago
committed by
GitHub
54 changed files with 2564 additions and 1205 deletions
After Width: | Height: | Size: 234 B |
After Width: | Height: | Size: 346 B |
After Width: | Height: | Size: 235 B |
After Width: | Height: | Size: 571 B |
@ -1,8 +1,19 @@
|
||||
<template> |
||||
<div class="h-full overflow-y-auto scrollbar-thin-dull pt-2 px-5"> |
||||
<div class="text-xl mt-4 mb-8 text-left font-weight-bold">{{ $t('title.appStore') }}</div> |
||||
<div> |
||||
<LazyDashboardSettingsAppStore /> |
||||
<div class="flex flex-col"> |
||||
<NcPageHeader> |
||||
<template #icon> |
||||
<GeneralIcon icon="appStore" class="flex-none text-gray-700 text-[20px] h-5 w-5" /> |
||||
</template> |
||||
<template #title> |
||||
<span data-rec="true"> |
||||
{{ $t('title.appStore') }} |
||||
</span> |
||||
</template> |
||||
</NcPageHeader> |
||||
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin"> |
||||
<div> |
||||
<LazyDashboardSettingsAppStore /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
@ -0,0 +1,112 @@
|
||||
<script lang="ts" setup> |
||||
const route = useRoute() |
||||
|
||||
interface BreadcrumbType { |
||||
title: string |
||||
active?: boolean |
||||
} |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const breadcrumb = computed<BreadcrumbType[]>(() => { |
||||
const payload: BreadcrumbType[] = [ |
||||
{ |
||||
title: 'Account', |
||||
}, |
||||
] |
||||
|
||||
switch (route.params.page) { |
||||
case 'profile': { |
||||
payload.push({ |
||||
title: t('labels.profile'), |
||||
active: true, |
||||
}) |
||||
break |
||||
} |
||||
case 'tokens': { |
||||
payload.push({ |
||||
title: t('title.tokens'), |
||||
active: true, |
||||
}) |
||||
break |
||||
} |
||||
case 'audit': { |
||||
payload.push({ |
||||
title: t('title.auditLogs'), |
||||
active: true, |
||||
}) |
||||
break |
||||
} |
||||
case 'apps': { |
||||
payload.push({ |
||||
title: t('title.appStore'), |
||||
active: true, |
||||
}) |
||||
break |
||||
} |
||||
} |
||||
|
||||
switch (route.params.nestedPage) { |
||||
case 'password-reset': { |
||||
payload.push( |
||||
...[ |
||||
{ |
||||
title: t('objects.users'), |
||||
}, |
||||
{ |
||||
title: t('title.resetPasswordMenu'), |
||||
active: true, |
||||
}, |
||||
], |
||||
) |
||||
break |
||||
} |
||||
case 'settings': { |
||||
payload.push( |
||||
...[ |
||||
{ |
||||
title: t('objects.users'), |
||||
}, |
||||
{ |
||||
title: t('activity.settings'), |
||||
active: true, |
||||
}, |
||||
], |
||||
) |
||||
break |
||||
} |
||||
} |
||||
|
||||
if ((route.params.page === undefined && route.params.nestedPage === '') || route.params.nestedPage === 'list') { |
||||
payload.push( |
||||
...[ |
||||
{ |
||||
title: t('objects.users'), |
||||
}, |
||||
{ |
||||
title: t('title.userManagement'), |
||||
active: true, |
||||
}, |
||||
], |
||||
) |
||||
} |
||||
|
||||
return payload |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="nc-breadcrumb"> |
||||
<template v-for="(item, i) of breadcrumb" :key="i"> |
||||
<div |
||||
class="nc-breadcrumb-item" |
||||
:class="{ |
||||
active: item.active, |
||||
}" |
||||
> |
||||
{{ item.title }} |
||||
</div> |
||||
<GeneralIcon v-if="i !== breadcrumb.length - 1" icon="ncSlash1" class="nc-breadcrumb-divider" /> |
||||
</template> |
||||
</div> |
||||
</template> |
@ -1,5 +0,0 @@
|
||||
<script lang="ts" setup></script> |
||||
|
||||
<template> |
||||
<WorkspaceIntegrationsView /> |
||||
</template> |
@ -0,0 +1,338 @@
|
||||
<script lang="ts" setup> |
||||
import { useVirtualList } from '@vueuse/core' |
||||
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight' |
||||
|
||||
export type RawValueType = string | number |
||||
|
||||
interface ListItem { |
||||
value?: RawValueType |
||||
label?: string |
||||
[key: string]: any |
||||
} |
||||
|
||||
/** |
||||
* Props interface for the List component |
||||
*/ |
||||
interface Props { |
||||
/** The currently selected value */ |
||||
value: RawValueType |
||||
/** The list of items to display */ |
||||
list: ListItem[] |
||||
/** |
||||
* The key to use for accessing the value from a list item |
||||
* @default 'value' |
||||
*/ |
||||
optionValueKey?: string |
||||
/** |
||||
* The key to use for accessing the label from a list item |
||||
* @default 'label' |
||||
*/ |
||||
optionLabelKey?: string |
||||
/** Whether the list is open or closed */ |
||||
open?: boolean |
||||
/** Whether to close the list after an item is selected */ |
||||
closeOnSelect?: boolean |
||||
/** Placeholder text for the search input */ |
||||
searchInputPlaceholder?: string |
||||
/** Whether to show the currently selected option */ |
||||
showSelectedOption?: boolean |
||||
/** Custom filter function for list items */ |
||||
filterOption?: (input: string, option: ListItem, index: Number) => boolean |
||||
} |
||||
|
||||
interface Emits { |
||||
(e: 'update:value', value: RawValueType): void |
||||
(e: 'update:open', open: boolean): void |
||||
(e: 'change', option: ListItem): void |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
open: false, |
||||
closeOnSelect: true, |
||||
showSelectedOption: true, |
||||
optionValueKey: 'value', |
||||
optionLabelKey: 'label', |
||||
}) |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const vModel = useVModel(props, 'value', emits) |
||||
|
||||
const vOpen = useVModel(props, 'open', emits) |
||||
|
||||
const { optionValueKey, optionLabelKey } = props |
||||
|
||||
const { closeOnSelect, showSelectedOption } = toRefs(props) |
||||
|
||||
const listRef = ref<HTMLDivElement>() |
||||
|
||||
const searchQuery = ref('') |
||||
|
||||
const inputRef = ref() |
||||
|
||||
const activeOptionIndex = ref(-1) |
||||
|
||||
const showHoverEffectOnSelectedOption = ref(true) |
||||
|
||||
const isSearchEnabled = computed(() => props.list.length > 4) |
||||
|
||||
/** |
||||
* Computed property that filters the list of options based on the search query. |
||||
* If a custom filter function is provided via props.filterOption, it will be used instead of the default filtering logic. |
||||
* |
||||
* @returns Filtered list of options |
||||
* |
||||
* @typeparam ListItem - The type of items in the list |
||||
*/ |
||||
const list = computed(() => { |
||||
const query = searchQuery.value.toLowerCase() |
||||
|
||||
return props.list.filter((item, i) => { |
||||
if (props?.filterOption) { |
||||
return props.filterOption(query, item, i) |
||||
} else { |
||||
return item[optionLabelKey]?.toLowerCase()?.includes(query) |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
const { |
||||
list: virtualList, |
||||
containerProps, |
||||
wrapperProps, |
||||
scrollTo, |
||||
} = useVirtualList(list, { |
||||
itemHeight: 38, |
||||
}) |
||||
|
||||
/** |
||||
* Resets the hover effect on the selected option |
||||
* @param clearActiveOption - Whether to clear the active option index |
||||
*/ |
||||
const handleResetHoverEffect = (clearActiveOption = false, newActiveIndex?: number) => { |
||||
if (clearActiveOption && showHoverEffectOnSelectedOption.value) { |
||||
activeOptionIndex.value = -1 |
||||
} |
||||
|
||||
if (newActiveIndex !== undefined) { |
||||
activeOptionIndex.value = newActiveIndex |
||||
} |
||||
|
||||
if (!showHoverEffectOnSelectedOption.value) return |
||||
|
||||
showHoverEffectOnSelectedOption.value = false |
||||
} |
||||
|
||||
/** |
||||
* Handles the selection of an option from the list |
||||
* |
||||
* @param option - The selected list item |
||||
* |
||||
* This function is responsible for handling the selection of an option from the list. |
||||
* It updates the model value, emits a change event, and optionally closes the dropdown. |
||||
*/ |
||||
const handleSelectOption = (option: ListItem) => { |
||||
if (!option?.[optionValueKey]) return |
||||
|
||||
vModel.value = option[optionValueKey] as RawValueType |
||||
emits('change', option) |
||||
if (closeOnSelect.value) { |
||||
vOpen.value = false |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Automatically scrolls to the active option in the list |
||||
*/ |
||||
const handleAutoScrollOption = (useDelay = false) => { |
||||
if (activeOptionIndex.value === -1) return |
||||
|
||||
if (!useDelay) { |
||||
scrollTo(activeOptionIndex.value) |
||||
return |
||||
} |
||||
|
||||
setTimeout(() => { |
||||
scrollTo(activeOptionIndex.value) |
||||
}, 150) |
||||
} |
||||
|
||||
const onArrowDown = () => { |
||||
handleResetHoverEffect() |
||||
|
||||
if (activeOptionIndex.value === list.value.length - 1) return |
||||
|
||||
activeOptionIndex.value = Math.min(activeOptionIndex.value + 1, list.value.length - 1) |
||||
handleAutoScrollOption() |
||||
} |
||||
|
||||
const onArrowUp = () => { |
||||
handleResetHoverEffect() |
||||
|
||||
if (activeOptionIndex.value === 0) return |
||||
|
||||
activeOptionIndex.value = Math.max(activeOptionIndex.value - 1, 0) |
||||
handleAutoScrollOption() |
||||
} |
||||
|
||||
const handleKeydownEnter = () => { |
||||
if (list.value[activeOptionIndex.value]) { |
||||
handleSelectOption(list.value[activeOptionIndex.value]) |
||||
} else if (list.value[0]) { |
||||
handleSelectOption(list.value[activeOptionIndex.value]) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Focuses the input box when the list is opened |
||||
*/ |
||||
const focusInputBox = () => { |
||||
if (!vOpen.value) return |
||||
|
||||
setTimeout(() => { |
||||
inputRef.value?.focus() |
||||
}, 100) |
||||
} |
||||
|
||||
/** |
||||
* Focuses the list wrapper when the list is opened |
||||
* |
||||
* This function is called when the list is opened and search is not enabled. |
||||
* It sets a timeout to focus the list wrapper element after a short delay. |
||||
* This allows for proper rendering and improves accessibility. |
||||
*/ |
||||
const focusListWrapper = () => { |
||||
if (!vOpen.value || isSearchEnabled.value) return |
||||
|
||||
setTimeout(() => { |
||||
listRef.value?.focus() |
||||
}, 100) |
||||
} |
||||
|
||||
watch( |
||||
vOpen, |
||||
() => { |
||||
if (!vOpen.value) return |
||||
|
||||
searchQuery.value = '' |
||||
showHoverEffectOnSelectedOption.value = true |
||||
|
||||
if (vModel.value) { |
||||
activeOptionIndex.value = list.value.findIndex((o) => o?.[optionValueKey] === vModel.value) |
||||
|
||||
nextTick(() => { |
||||
handleAutoScrollOption(true) |
||||
}) |
||||
} else { |
||||
activeOptionIndex.value = -1 |
||||
} |
||||
|
||||
if (isSearchEnabled.value) { |
||||
focusInputBox() |
||||
} else { |
||||
focusListWrapper() |
||||
} |
||||
}, |
||||
{ |
||||
immediate: true, |
||||
}, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
ref="listRef" |
||||
tabindex="0" |
||||
class="flex flex-col pt-2 w-64 !focus:(shadow-none outline-none)" |
||||
@keydown.arrow-down.prevent="onArrowDown" |
||||
@keydown.arrow-up.prevent="onArrowUp" |
||||
@keydown.enter.prevent="handleSelectOption(list[activeOptionIndex])" |
||||
> |
||||
<template v-if="isSearchEnabled"> |
||||
<div class="w-full px-2" @click.stop> |
||||
<a-input |
||||
ref="inputRef" |
||||
v-model:value="searchQuery" |
||||
:placeholder="searchInputPlaceholder || $t('placeholder.searchFields')" |
||||
class="nc-toolbar-dropdown-search-field-input !pl-2 !pr-1.5" |
||||
allow-clear |
||||
:bordered="false" |
||||
@keydown.enter.stop="handleKeydownEnter" |
||||
@change="handleResetHoverEffect(false, 0)" |
||||
> |
||||
<template #prefix> <GeneralIcon icon="search" class="nc-search-icon h-3.5 w-3.5 mr-1" /> </template |
||||
></a-input> |
||||
</div> |
||||
<NcDivider /> |
||||
</template> |
||||
|
||||
<slot name="listHeader"></slot> |
||||
<div class="nc-list-wrapper"> |
||||
<template v-if="list.length"> |
||||
<div class="h-auto !max-h-[247px]"> |
||||
<div |
||||
v-bind="containerProps" |
||||
class="nc-list !h-auto w-full nc-scrollbar-thin px-2 pb-2" |
||||
:style="{ |
||||
maxHeight: '247px !important', |
||||
}" |
||||
> |
||||
<div v-bind="wrapperProps"> |
||||
<div |
||||
v-for="{ data: option, index: idx } in virtualList" |
||||
:key="idx" |
||||
class="flex items-center gap-2 w-full py-2 px-2 hover:bg-gray-100 cursor-pointer rounded-md" |
||||
:class="[ |
||||
`nc-list-option-${idx}`, |
||||
{ |
||||
'nc-list-option-selected': option[optionValueKey] === vModel, |
||||
'bg-gray-100 ': showHoverEffectOnSelectedOption && option[optionValueKey] === vModel, |
||||
'bg-gray-100 nc-list-option-active': activeOptionIndex === idx, |
||||
}, |
||||
]" |
||||
@mouseover="handleResetHoverEffect(true)" |
||||
@click="handleSelectOption(option)" |
||||
> |
||||
<slot name="listItem" :option="option" :index="idx"> |
||||
<NcTooltip class="truncate flex-1" show-on-truncate-only> |
||||
<template #title> |
||||
{{ option[optionLabelKey] }} |
||||
</template> |
||||
{{ option[optionLabelKey] }} |
||||
</NcTooltip> |
||||
<GeneralIcon |
||||
v-if="showSelectedOption && option[optionValueKey] === vModel" |
||||
id="nc-selected-item-icon" |
||||
icon="check" |
||||
class="flex-none text-primary w-4 h-4" |
||||
/> |
||||
</slot> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<template v-if="!list.length"> |
||||
<slot name="emptyState"> |
||||
<div class="h-full text-center flex items-center justify-center gap-3 mt-4"> |
||||
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" class="!my-0" /> |
||||
</div> |
||||
</slot> |
||||
</template> |
||||
</div> |
||||
<slot name="listFooter"></slot> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
:deep(.nc-toolbar-dropdown-search-field-input) { |
||||
&.ant-input-affix-wrapper-focused { |
||||
.ant-input-prefix svg { |
||||
@apply text-brand-500; |
||||
} |
||||
} |
||||
.ant-input { |
||||
@apply placeholder-gray-500; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup></script> |
||||
|
||||
<template> |
||||
<div class="nc-page-header"> |
||||
<div class="flex-1 flex items-start gap-3"> |
||||
<div v-if="$slots.icon" class="h-7 flex items-center children:flex-none"> |
||||
<slot name="icon"></slot> |
||||
</div> |
||||
|
||||
<div class="flex flex-col gap-3"> |
||||
<h1 class="nc-page-header-title truncate"> |
||||
<slot name="title"></slot> |
||||
</h1> |
||||
<p v-if="$slots.subtitle" class="nc-page-header-subtitle"> |
||||
<slot name="subtitle"></slot> |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<div v-if="$slots.action"> |
||||
<slot name="action"></slot> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.nc-page-header { |
||||
@apply h-12 flex items-center gap-3 px-3 py-2; |
||||
|
||||
.nc-page-header-title { |
||||
@apply text-xl font-semibold text-gray-800 my-0; |
||||
} |
||||
.nc-page-header-subtitle { |
||||
@apply text-sm font-weight-500 text-gray-700; |
||||
} |
||||
} |
||||
</style> |
@ -1,137 +1,180 @@
|
||||
<script setup lang="ts"> |
||||
const { isMobileMode } = useGlobal() |
||||
|
||||
const { activeView } = storeToRefs(useViewsStore()) |
||||
const { activeView, openedViewsTab } = storeToRefs(useViewsStore()) |
||||
|
||||
const { base, isSharedBase } = storeToRefs(useBase()) |
||||
const { baseUrl } = useBase() |
||||
|
||||
const { activeTable } = storeToRefs(useTablesStore()) |
||||
const { tableUrl } = useTablesStore() |
||||
|
||||
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) |
||||
|
||||
const openedBaseUrl = computed(() => { |
||||
if (!base.value) return '' |
||||
|
||||
return `${window.location.origin}/#${baseUrl({ |
||||
id: base.value.id!, |
||||
type: 'database', |
||||
isSharedBase: isSharedBase.value, |
||||
})}` |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100" |
||||
class="flex flex-row items-center border-gray-50 transition-all duration-100 select-none" |
||||
:class="{ |
||||
'min-w-36/100 max-w-36/100': !isMobileMode && isLeftSidebarOpen, |
||||
'min-w-39/100 max-w-39/100': !isMobileMode && !isLeftSidebarOpen, |
||||
'w-2/3 text-base ml-1.5': isMobileMode, |
||||
'!max-w-3/4': isSharedBase && !isMobileMode, |
||||
'text-base w-[calc(100%_-_52px)]': isMobileMode, |
||||
'w-[calc(100%_-_44px)]': !isMobileMode && !isLeftSidebarOpen, |
||||
'w-full': !isMobileMode && isLeftSidebarOpen, |
||||
}" |
||||
> |
||||
<template v-if="!isMobileMode"> |
||||
<NuxtLink |
||||
class="!hover:(text-black underline-gray-600) !underline-transparent ml-0.75 max-w-1/4" |
||||
:class="{ |
||||
'!max-w-none': isSharedBase && !isMobileMode, |
||||
'!text-gray-500': activeTable, |
||||
'!text-gray-700': !activeTable, |
||||
}" |
||||
:to="openedBaseUrl" |
||||
> |
||||
<NcTooltip class="!text-inherit"> |
||||
<template #title> |
||||
<span class="capitalize"> |
||||
{{ base?.title }} |
||||
</span> |
||||
</template> |
||||
<div class="flex flex-row items-center gap-x-1.5"> |
||||
<GeneralProjectIcon |
||||
:type="base?.type" |
||||
class="!grayscale min-w-4" |
||||
:style="{ |
||||
filter: 'grayscale(100%) brightness(115%)', |
||||
}" |
||||
/> |
||||
<div |
||||
class="hidden !2xl:(flex truncate ml-1)" |
||||
:class="{ |
||||
'!flex': isSharedBase && !isMobileMode, |
||||
}" |
||||
> |
||||
<span class="truncate !text-inherit capitalize"> |
||||
{{ base?.title }} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</NcTooltip> |
||||
</NuxtLink> |
||||
<div class="px-1.75 text-gray-500">></div> |
||||
</template> |
||||
<template v-if="!(isMobileMode && !activeView?.is_default)"> |
||||
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall"> |
||||
<template #default> |
||||
<MdiTable |
||||
class="min-w-5" |
||||
<SmartsheetTopbarProjectListDropdown v-if="activeTable"> |
||||
<template #default="{ isOpen }"> |
||||
<div |
||||
class="rounded-lg h-8 px-2 text-gray-700 font-weight-500 hover:(bg-gray-100 text-gray-900) flex items-center gap-1 cursor-pointer max-w-1/3" |
||||
:class="{ |
||||
'!text-gray-500': !isMobileMode, |
||||
'!text-gray-700': isMobileMode, |
||||
'!max-w-none': isSharedBase && !isMobileMode, |
||||
'': !isMobileMode && isLeftSidebarOpen, |
||||
}" |
||||
/> |
||||
> |
||||
<NcTooltip :disabled="isSharedBase || isOpen"> |
||||
<template #title> |
||||
<span class="capitalize"> |
||||
{{ base?.title }} |
||||
</span> |
||||
</template> |
||||
|
||||
<GeneralProjectIcon |
||||
:type="base?.type" |
||||
class="!grayscale min-w-4" |
||||
:style="{ |
||||
filter: 'grayscale(100%) brightness(115%)', |
||||
}" |
||||
/> |
||||
</NcTooltip> |
||||
<template v-if="isSharedBase"> |
||||
<NcTooltip |
||||
class="ml-1 truncate nc-active-base-title max-w-full !leading-5" |
||||
show-on-truncate-only |
||||
:disabled="isOpen" |
||||
> |
||||
<template #title> |
||||
<span class="capitalize"> |
||||
{{ base?.title }} |
||||
</span> |
||||
</template> |
||||
|
||||
<span |
||||
class="text-ellipsis capitalize" |
||||
:style="{ |
||||
wordBreak: 'keep-all', |
||||
whiteSpace: 'nowrap', |
||||
display: 'inline', |
||||
}" |
||||
> |
||||
{{ base?.title }} |
||||
</span> |
||||
</NcTooltip> |
||||
<GeneralIcon |
||||
icon="chevronDown" |
||||
class="!text-current opacity-70 flex-none transform transition-transform duration-25 w-3.5 h-3.5" |
||||
:class="{ '!rotate-180': isOpen }" |
||||
/> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
</LazyGeneralEmojiPicker> |
||||
<div |
||||
v-if="activeTable" |
||||
:class="{ |
||||
'max-w-1/2': isMobileMode || activeView?.is_default, |
||||
'max-w-20/100': !isSharedBase && !isMobileMode && !activeView?.is_default, |
||||
'max-w-none': isSharedBase && !isMobileMode, |
||||
}" |
||||
> |
||||
<NcTooltip class="truncate nc-active-table-title max-w-full" show-on-truncate-only> |
||||
<template #title> |
||||
{{ activeTable?.title }} |
||||
</template> |
||||
<span |
||||
class="text-ellipsis overflow-hidden text-gray-500 xs:ml-2" |
||||
</SmartsheetTopbarProjectListDropdown> |
||||
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" /> |
||||
</template> |
||||
<template v-if="!(isMobileMode && !activeView?.is_default)"> |
||||
<SmartsheetTopbarTableListDropdown v-if="activeTable"> |
||||
<template #default="{ isOpen }"> |
||||
<div |
||||
class="rounded-lg h-8 px-2 text-gray-700 font-weight-500 hover:(bg-gray-100 text-gray-900) flex items-center gap-1 cursor-pointer" |
||||
:class="{ |
||||
'text-gray-500': !isMobileMode, |
||||
'text-gray-800 font-medium': isMobileMode || activeView?.is_default, |
||||
}" |
||||
:style="{ |
||||
wordBreak: 'keep-all', |
||||
whiteSpace: 'nowrap', |
||||
display: 'inline', |
||||
'max-w-full': isMobileMode, |
||||
'max-w-1/2': activeView?.is_default, |
||||
'max-w-1/4': !isSharedBase && !isMobileMode && !activeView?.is_default, |
||||
'max-w-none': isSharedBase && !isMobileMode, |
||||
}" |
||||
> |
||||
<template v-if="activeView?.is_default"> |
||||
{{ activeTable?.title }} |
||||
</template> |
||||
<NuxtLink |
||||
v-else |
||||
class="!text-inherit !underline-transparent !hover:(text-black underline-gray-600)" |
||||
:to="tableUrl({ table: activeTable, completeUrl: true, isSharedBase })" |
||||
> |
||||
{{ activeTable?.title }} |
||||
</NuxtLink> |
||||
</span> |
||||
</NcTooltip> |
||||
</div> |
||||
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall" class="mr-1"> |
||||
<template #default> |
||||
<GeneralIcon |
||||
icon="table" |
||||
class="min-w-5" |
||||
:class="{ |
||||
'!text-gray-500': !isMobileMode, |
||||
'!text-gray-700': isMobileMode, |
||||
}" |
||||
/> |
||||
</template> |
||||
</LazyGeneralEmojiPicker> |
||||
|
||||
<NcTooltip class="truncate nc-active-table-title max-w-full !leading-5" show-on-truncate-only :disabled="isOpen"> |
||||
<template #title> |
||||
{{ activeTable?.title }} |
||||
</template> |
||||
<span |
||||
class="text-ellipsis" |
||||
:style="{ |
||||
wordBreak: 'keep-all', |
||||
whiteSpace: 'nowrap', |
||||
display: 'inline', |
||||
}" |
||||
> |
||||
{{ activeTable?.title }} |
||||
</span> |
||||
</NcTooltip> |
||||
<GeneralIcon |
||||
icon="chevronDown" |
||||
class="!text-current opacity-70 flex-none transform transition-transform duration-25 w-3.5 h-3.5" |
||||
:class="{ '!rotate-180': isOpen }" |
||||
/> |
||||
</div> |
||||
</template> |
||||
</SmartsheetTopbarTableListDropdown> |
||||
</template> |
||||
|
||||
<div v-if="!isMobileMode" class="pl-1.25 text-gray-500">></div> |
||||
<GeneralIcon v-if="!isMobileMode" icon="ncSlash1" class="nc-breadcrumb-divider" /> |
||||
|
||||
<template v-if="!(isMobileMode && activeView?.is_default)"> |
||||
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeView?.meta?.icon" readonly size="xsmall"> |
||||
<template #default> |
||||
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" /> |
||||
<!-- <SmartsheetToolbarOpenedViewAction /> --> |
||||
|
||||
<SmartsheetTopbarViewListDropdown> |
||||
<template #default="{ isOpen }"> |
||||
<div |
||||
class="rounded-lg h-8 px-2 text-gray-800 font-semibold hover:(bg-gray-100 text-gray-900) flex items-center gap-1 cursor-pointer" |
||||
:class="{ |
||||
'max-w-full': isMobileMode, |
||||
'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default, |
||||
'max-w-1/2': !isSharedBase && !isMobileMode && !activeView?.is_default, |
||||
'max-w-none': isSharedBase && !isMobileMode, |
||||
}" |
||||
> |
||||
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeView?.meta?.icon" readonly size="xsmall" class="mr-1"> |
||||
<template #default> |
||||
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" /> |
||||
</template> |
||||
</LazyGeneralEmojiPicker> |
||||
|
||||
<NcTooltip class="truncate nc-active-view-title max-w-full !leading-5" show-on-truncate-only :disabled="isOpen"> |
||||
<template #title> |
||||
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} |
||||
</template> |
||||
<span |
||||
class="text-ellipsis" |
||||
:style="{ |
||||
wordBreak: 'keep-all', |
||||
whiteSpace: 'nowrap', |
||||
display: 'inline', |
||||
}" |
||||
> |
||||
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} |
||||
</span> |
||||
</NcTooltip> |
||||
<GeneralIcon |
||||
icon="chevronDown" |
||||
class="!text-current opacity-70 flex-none transform transition-transform duration-25 w-3.5 h-3.5" |
||||
:class="{ '!rotate-180': isOpen }" |
||||
/> |
||||
</div> |
||||
</template> |
||||
</LazyGeneralEmojiPicker> |
||||
</SmartsheetTopbarViewListDropdown> |
||||
|
||||
<SmartsheetToolbarOpenedViewAction /> |
||||
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode" /> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
|
@ -0,0 +1,80 @@
|
||||
<script lang="ts" setup> |
||||
const basesStore = useBases() |
||||
const { basesList } = storeToRefs(basesStore) |
||||
|
||||
const baseStore = useBase() |
||||
const { base: activeBase, isSharedBase } = storeToRefs(baseStore) |
||||
|
||||
const { loadProjectTables } = useTablesStore() |
||||
|
||||
const isOpen = ref<boolean>(false) |
||||
|
||||
/** |
||||
* Handles navigation to a selected project/base. |
||||
* |
||||
* @param base - The project/base to navigate to. |
||||
* @returns A Promise that resolves when the navigation is complete. |
||||
* |
||||
* @remarks |
||||
* This function is called when a user selects a project from the dropdown list. |
||||
* It performs the following steps: |
||||
* 1. Checks if the base has a valid ID. |
||||
* 2. Determines if the project data is already populated. |
||||
* 3. Navigates to the selected project's URL. |
||||
* 4. If the project data isn't populated, it loads the project tables. |
||||
*/ |
||||
const handleNavigateToProject = async (base: NcProject) => { |
||||
if (!base?.id) return |
||||
|
||||
const isProjectPopulated = basesStore.isProjectPopulated(base.id!) |
||||
|
||||
await navigateTo( |
||||
baseStore.baseUrl({ |
||||
id: base.id!, |
||||
type: 'database', |
||||
isSharedBase: isSharedBase.value, |
||||
}), |
||||
) |
||||
|
||||
if (!isProjectPopulated) { |
||||
await loadProjectTables(base.id) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<NcDropdown v-model:visible="isOpen"> |
||||
<slot name="default" :is-open="isOpen"></slot> |
||||
<template #overlay> |
||||
<LazyNcList |
||||
v-if="activeBase.id" |
||||
v-model:open="isOpen" |
||||
:value="activeBase.id" |
||||
:list="basesList" |
||||
option-value-key="id" |
||||
option-label-key="title" |
||||
search-input-placeholder="Search bases" |
||||
@change="handleNavigateToProject" |
||||
> |
||||
<template #listItem="{ option }"> |
||||
<GeneralBaseIconColorPicker :type="option?.type" :model-value="parseProp(option.meta).iconColor" size="xsmall" readonly> |
||||
</GeneralBaseIconColorPicker> |
||||
<NcTooltip class="truncate flex-1" show-on-truncate-only> |
||||
<template #title> |
||||
{{ option?.title }} |
||||
</template> |
||||
{{ option?.title }} |
||||
</NcTooltip> |
||||
<GeneralIcon |
||||
v-if="option.id === activeBase.id" |
||||
id="nc-selected-item-icon" |
||||
icon="check" |
||||
class="flex-none text-primary w-4 h-4" |
||||
/> |
||||
</template> |
||||
</LazyNcList> |
||||
</template> |
||||
</NcDropdown> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,159 @@
|
||||
<script lang="ts" setup> |
||||
import type { TableType } from 'nocodb-sdk' |
||||
|
||||
const { isMobileMode } = useGlobal() |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
const { isUIAllowed } = useRoles() |
||||
|
||||
const { base } = storeToRefs(useBase()) |
||||
|
||||
const { activeTable, activeTables } = storeToRefs(useTablesStore()) |
||||
|
||||
const { openTable } = useTablesStore() |
||||
|
||||
const isOpen = ref<boolean>(false) |
||||
|
||||
const activeTableSourceIndex = computed(() => base.value?.sources?.findIndex((s) => s.id === activeTable.value?.source_id) ?? -1) |
||||
|
||||
const filteredTableList = computed(() => { |
||||
return activeTables.value.filter((t: TableType) => t?.source_id === activeTable.value?.source_id) || [] |
||||
}) |
||||
|
||||
/** |
||||
* Handles navigation to a selected table. |
||||
* |
||||
* @param table - The table to navigate to. |
||||
* |
||||
* @remarks |
||||
* This function is called when a user selects a table from the dropdown list. |
||||
* It checks if the table has a valid ID and then opens the selected table. |
||||
*/ |
||||
const handleNavigateToTable = (table: TableType) => { |
||||
if (table?.id) { |
||||
openTable(table) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Opens a dialog to create a new table. |
||||
* |
||||
* @returns void |
||||
* |
||||
* @remarks |
||||
* This function is triggered when the user initiates the table creation process from the topbar. |
||||
* It emits a tracking event, checks for a valid source, and opens a dialog for table creation. |
||||
* The function also handles the dialog closure and potential scrolling to the newly created table. |
||||
* |
||||
* @see {@link packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue} for a similar implementation |
||||
* of table creation dialog. If this function is updated, consider updating the other implementation as well. |
||||
*/ |
||||
function openTableCreateDialog() { |
||||
$e('c:table:create:topbar') |
||||
|
||||
if (activeTableSourceIndex.value === -1) return |
||||
|
||||
isOpen.value = false |
||||
|
||||
const isCreateTableOpen = ref(true) |
||||
const sourceId = base.value!.sources?.[activeTableSourceIndex.value].id |
||||
|
||||
if (!sourceId || !base.value?.id) return |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgTableCreate'), { |
||||
'modelValue': isCreateTableOpen, |
||||
sourceId, // || sources.value[0].id, |
||||
'baseId': base.value!.id, |
||||
'onCreate': closeDialog, |
||||
'onUpdate:modelValue': () => closeDialog(), |
||||
}) |
||||
|
||||
function closeDialog(table?: TableType) { |
||||
isCreateTableOpen.value = false |
||||
|
||||
if (!table) return |
||||
|
||||
// TODO: Better way to know when the table node dom is available |
||||
setTimeout(() => { |
||||
const newTableDom = document.querySelector(`[data-table-id="${table.id}"]`) |
||||
if (!newTableDom) return |
||||
|
||||
// Scroll to the table node |
||||
newTableDom?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) |
||||
}, 1000) |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<NcDropdown v-model:visible="isOpen"> |
||||
<slot name="default" :is-open="isOpen"></slot> |
||||
<template #overlay> |
||||
<LazyNcList |
||||
v-model:open="isOpen" |
||||
:value="activeTable.id" |
||||
:list="filteredTableList" |
||||
option-value-key="id" |
||||
option-label-key="title" |
||||
search-input-placeholder="Search tables" |
||||
@change="handleNavigateToTable" |
||||
> |
||||
<template #listItem="{ option }"> |
||||
<div> |
||||
<LazyGeneralEmojiPicker :emoji="option?.meta?.icon" readonly size="xsmall"> |
||||
<template #default> |
||||
<GeneralIcon icon="table" class="min-w-4 !text-gray-500" /> |
||||
</template> |
||||
</LazyGeneralEmojiPicker> |
||||
</div> |
||||
<NcTooltip class="truncate flex-1" show-on-truncate-only> |
||||
<template #title> |
||||
{{ option?.title }} |
||||
</template> |
||||
{{ option?.title }} |
||||
</NcTooltip> |
||||
<GeneralIcon |
||||
v-if="option.id === activeTable.id" |
||||
id="nc-selected-item-icon" |
||||
icon="check" |
||||
class="flex-none text-primary w-4 h-4" |
||||
/> |
||||
</template> |
||||
|
||||
<template |
||||
v-if=" |
||||
!isMobileMode && |
||||
isUIAllowed('tableCreate', { |
||||
roles: base?.project_role || base?.workspace_role, |
||||
source: base?.sources?.[activeTableSourceIndex] || {}, |
||||
}) |
||||
" |
||||
#listFooter |
||||
> |
||||
<NcDivider class="!mt-0 !mb-2" /> |
||||
<div class="px-2 mb-2" @click="openTableCreateDialog()"> |
||||
<div |
||||
class="px-2 py-1.5 flex items-center justify-between gap-2 text-sm font-weight-500 !text-brand-500 hover:bg-gray-100 rounded-md cursor-pointer" |
||||
> |
||||
<div class="flex items-center gap-2"> |
||||
<GeneralIcon icon="plus" /> |
||||
<div> |
||||
{{ |
||||
$t('general.createEntity', { |
||||
entity: $t('objects.table'), |
||||
}) |
||||
}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</LazyNcList> |
||||
</template> |
||||
</NcDropdown> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,303 @@
|
||||
<script lang="ts" setup> |
||||
import { type ViewType, ViewTypes } from 'nocodb-sdk' |
||||
|
||||
const { isMobileMode } = useGlobal() |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
const { isUIAllowed } = useRoles() |
||||
|
||||
const { base } = storeToRefs(useBase()) |
||||
|
||||
const { activeTable } = storeToRefs(useTablesStore()) |
||||
|
||||
const viewsStore = useViewsStore() |
||||
|
||||
const { activeView, views } = storeToRefs(viewsStore) |
||||
|
||||
const { loadViews, navigateToView } = viewsStore |
||||
|
||||
const { refreshCommandPalette } = useCommandPalette() |
||||
|
||||
const isOpen = ref<boolean>(false) |
||||
|
||||
const activeSource = computed(() => { |
||||
return base.value.sources?.find((s) => s.id === activeView.value?.source_id) |
||||
}) |
||||
|
||||
/** |
||||
* Handles navigation to a selected view. |
||||
* |
||||
* @param view - The view to navigate to. |
||||
* @returns A Promise that resolves when the navigation is complete. |
||||
* |
||||
* @remarks |
||||
* This function is called when a user selects a view from the dropdown list. |
||||
* It checks if the view has a valid ID and then navigates to the selected view. |
||||
* If the view is a form and it's already active, it performs a hard reload. |
||||
*/ |
||||
const handleNavigateToView = async (view: ViewType) => { |
||||
if (!view?.id) return |
||||
|
||||
await navigateToView({ |
||||
view, |
||||
tableId: activeTable.value.id!, |
||||
baseId: base.value.id!, |
||||
hardReload: view.type === ViewTypes.FORM && activeView.value?.id === view.id, |
||||
doNotSwitchTab: true, |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* Filters the view options based on the input string. |
||||
* |
||||
* @param input - The search input string. |
||||
* @param view - The view object to be filtered. |
||||
* @returns True if the view matches the filter criteria, false otherwise. |
||||
* |
||||
* @remarks |
||||
* This function is used to filter the list of views in the dropdown. |
||||
* It checks if the input string matches either the default view title (translated) or the view's title. |
||||
* The matching is case-insensitive. |
||||
*/ |
||||
const filterOption = (input: string = '', view: ViewType) => { |
||||
if (view.is_default && t('title.defaultView').toLowerCase().includes(input)) { |
||||
return true |
||||
} |
||||
|
||||
return view.title?.toLowerCase()?.includes(input.toLowerCase()) |
||||
} |
||||
|
||||
/** |
||||
* Opens a modal for creating or editing a view. |
||||
* |
||||
* @param options - The options for opening the modal. |
||||
* @param options.title - The title of the modal. Default is an empty string. |
||||
* @param options.type - The type of view to create or edit. |
||||
* @param options.copyViewId - The ID of the view to copy, if creating a copy. |
||||
* @param options.groupingFieldColumnId - The ID of the column to use for grouping, if applicable. |
||||
* @param options.calendarRange - The date range for calendar views. |
||||
* @param options.coverImageColumnId - The ID of the column to use for cover images, if applicable. |
||||
* |
||||
* @returns A Promise that resolves when the modal operation is complete. |
||||
* |
||||
* @remarks |
||||
* This function opens a modal dialog for creating or editing a view. |
||||
* It handles the dialog state, view creation, and navigation to the newly created view. |
||||
* After creating a view, it refreshes the command palette and reloads the views. |
||||
* |
||||
* @see {@link packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue} for a similar implementation of view creation dialog. |
||||
* If this function is updated, consider updating the other implementations as well. |
||||
*/ |
||||
async function onOpenModal({ |
||||
title = '', |
||||
type, |
||||
copyViewId, |
||||
groupingFieldColumnId, |
||||
calendarRange, |
||||
coverImageColumnId, |
||||
}: { |
||||
title?: string |
||||
type: ViewTypes |
||||
copyViewId?: string |
||||
groupingFieldColumnId?: string |
||||
calendarRange?: Array<{ |
||||
fk_from_column_id: string |
||||
fk_to_column_id: string | null |
||||
}> |
||||
coverImageColumnId?: string |
||||
}) { |
||||
isOpen.value = false |
||||
|
||||
const isDlgOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgViewCreate'), { |
||||
'modelValue': isDlgOpen, |
||||
title, |
||||
type, |
||||
'tableId': activeTable.value.id, |
||||
'selectedViewId': copyViewId, |
||||
calendarRange, |
||||
groupingFieldColumnId, |
||||
coverImageColumnId, |
||||
'onUpdate:modelValue': closeDialog, |
||||
'onCreated': async (view: ViewType) => { |
||||
closeDialog() |
||||
|
||||
refreshCommandPalette() |
||||
|
||||
await loadViews({ |
||||
tableId: activeTable.value.id!, |
||||
force: true, |
||||
}) |
||||
|
||||
activeTable.value.meta = { |
||||
...(activeTable.value.meta as object), |
||||
hasNonDefaultViews: true, |
||||
} |
||||
|
||||
navigateToView({ |
||||
view, |
||||
tableId: activeTable.value.id!, |
||||
baseId: base.value.id!, |
||||
doNotSwitchTab: true, |
||||
}) |
||||
|
||||
$e('a:view:create', { view: view.type }) |
||||
}, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
isDlgOpen.value = false |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<NcDropdown v-if="activeView" v-model:visible="isOpen"> |
||||
<slot name="default" :is-open="isOpen"></slot> |
||||
<template #overlay> |
||||
<LazyNcList |
||||
v-model:open="isOpen" |
||||
:value="activeView.id" |
||||
:list="views" |
||||
option-value-key="id" |
||||
option-label-key="title" |
||||
search-input-placeholder="Search views" |
||||
:filter-option="filterOption" |
||||
@change="handleNavigateToView" |
||||
> |
||||
<template #listItem="{ option }"> |
||||
<div> |
||||
<LazyGeneralEmojiPicker :emoji="option?.meta?.icon" readonly size="xsmall"> |
||||
<template #default> |
||||
<GeneralViewIcon :meta="{ type: option?.type }" class="min-w-4 text-lg flex" /> |
||||
</template> |
||||
</LazyGeneralEmojiPicker> |
||||
</div> |
||||
<NcTooltip class="truncate flex-1" show-on-truncate-only> |
||||
<template #title> |
||||
{{ option?.is_default ? $t('title.defaultView') : option?.title }} |
||||
</template> |
||||
{{ option?.is_default ? $t('title.defaultView') : option?.title }} |
||||
</NcTooltip> |
||||
<GeneralIcon |
||||
v-if="option.id === activeView.id" |
||||
id="nc-selected-item-icon" |
||||
icon="check" |
||||
class="flex-none text-primary w-4 h-4" |
||||
/> |
||||
</template> |
||||
|
||||
<template v-if="!isMobileMode && isUIAllowed('viewCreateOrEdit')" #listFooter> |
||||
<NcDivider class="!mt-0 !mb-2" /> |
||||
<div class="overflow-hidden mb-2"> |
||||
<a-menu class="nc-viewlist-menu"> |
||||
<a-sub-menu popup-class-name="nc-viewlist-submenu-popup "> |
||||
<template #title> |
||||
<div class="flex items-center justify-between gap-2 text-sm font-weight-500 !text-brand-500"> |
||||
<div class="flex items-center gap-2"> |
||||
<GeneralIcon icon="plus" /> |
||||
<div> |
||||
{{ |
||||
$t('general.createEntity', { |
||||
entity: $t('objects.view'), |
||||
}) |
||||
}} |
||||
</div> |
||||
</div> |
||||
<GeneralIcon icon="arrowRight" class="text-base text-gray-600 group-hover:text-gray-800" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #expandIcon> </template> |
||||
|
||||
<a-menu-item @click.stop="onOpenModal({ type: ViewTypes.GRID })"> |
||||
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-grid"> |
||||
<GeneralViewIcon :meta="{ type: ViewTypes.GRID }" /> |
||||
Grid |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item v-if="!activeSource?.is_schema_readonly" @click="onOpenModal({ type: ViewTypes.FORM })"> |
||||
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-form"> |
||||
<GeneralViewIcon :meta="{ type: ViewTypes.FORM }" /> |
||||
Form |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item @click="onOpenModal({ type: ViewTypes.GALLERY })"> |
||||
<div class="nc-viewlist-submenu-popup-item" data-testid="topbar-view-create-gallery"> |
||||
<GeneralViewIcon :meta="{ type: ViewTypes.GALLERY }" /> |
||||
Gallery |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item data-testid="topbar-view-create-kanban" @click="onOpenModal({ type: ViewTypes.KANBAN })"> |
||||
<div class="nc-viewlist-submenu-popup-item"> |
||||
<GeneralViewIcon :meta="{ type: ViewTypes.KANBAN }" /> |
||||
Kanban |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item data-testid="topbar-view-create-calendar" @click="onOpenModal({ type: ViewTypes.CALENDAR })"> |
||||
<div class="nc-viewlist-submenu-popup-item"> |
||||
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" class="!w-4 !h-4" /> |
||||
{{ $t('objects.viewType.calendar') }} |
||||
</div> |
||||
</a-menu-item> |
||||
</a-sub-menu> |
||||
</a-menu> |
||||
</div> |
||||
</template> |
||||
</LazyNcList> |
||||
</template> |
||||
</NcDropdown> |
||||
</template> |
||||
|
||||
<style lang="scss"> |
||||
.nc-viewlist-menu { |
||||
@apply !border-r-0; |
||||
|
||||
.ant-menu-submenu { |
||||
@apply !mx-2; |
||||
|
||||
.ant-menu-submenu-title { |
||||
@apply flex items-center gap-2 py-1.5 px-2 my-0 h-auto hover:bg-gray-100 cursor-pointer rounded-md; |
||||
|
||||
.ant-menu-title-content { |
||||
@apply w-full; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.nc-viewlist-submenu-popup { |
||||
@apply !rounded-lg border-1 border-gray-50; |
||||
|
||||
.ant-menu.ant-menu-sub { |
||||
@apply p-2 !rounded-lg !shadow-lg shadow-gray-200; |
||||
} |
||||
|
||||
.ant-menu-item { |
||||
@apply h-auto !my-0 text-sm !leading-5 py-2 px-2 hover:!bg-gray-100 cursor-pointer rounded-md; |
||||
|
||||
.ant-menu-title-content { |
||||
@apply w-full px-0; |
||||
} |
||||
|
||||
.nc-viewlist-submenu-popup-item { |
||||
@apply flex items-center gap-2 !text-gray-800; |
||||
} |
||||
|
||||
&.ant-menu-item-selected { |
||||
@apply bg-transparent; |
||||
} |
||||
} |
||||
} |
||||
.nc-viewlist-submenu-popup .ant-dropdown-menu.ant-dropdown-menu-sub { |
||||
@apply !rounded-lg !shadow-lg shadow-gray-200; |
||||
} |
||||
</style> |
Loading…
Reference in new issue