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
4 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> |
<template> |
||||||
<div class="h-full overflow-y-auto scrollbar-thin-dull pt-2 px-5"> |
<div class="flex flex-col"> |
||||||
<div class="text-xl mt-4 mb-8 text-left font-weight-bold">{{ $t('title.appStore') }}</div> |
<NcPageHeader> |
||||||
<div> |
<template #icon> |
||||||
<LazyDashboardSettingsAppStore /> |
<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> |
||||||
</div> |
</div> |
||||||
</template> |
</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"> |
<script setup lang="ts"> |
||||||
const { isMobileMode } = useGlobal() |
const { isMobileMode } = useGlobal() |
||||||
|
|
||||||
const { activeView } = storeToRefs(useViewsStore()) |
const { activeView, openedViewsTab } = storeToRefs(useViewsStore()) |
||||||
|
|
||||||
const { base, isSharedBase } = storeToRefs(useBase()) |
const { base, isSharedBase } = storeToRefs(useBase()) |
||||||
const { baseUrl } = useBase() |
|
||||||
|
|
||||||
const { activeTable } = storeToRefs(useTablesStore()) |
const { activeTable } = storeToRefs(useTablesStore()) |
||||||
const { tableUrl } = useTablesStore() |
|
||||||
|
|
||||||
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) |
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> |
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<div |
<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="{ |
:class="{ |
||||||
'min-w-36/100 max-w-36/100': !isMobileMode && isLeftSidebarOpen, |
'text-base w-[calc(100%_-_52px)]': isMobileMode, |
||||||
'min-w-39/100 max-w-39/100': !isMobileMode && !isLeftSidebarOpen, |
'w-[calc(100%_-_44px)]': !isMobileMode && !isLeftSidebarOpen, |
||||||
'w-2/3 text-base ml-1.5': isMobileMode, |
'w-full': !isMobileMode && isLeftSidebarOpen, |
||||||
'!max-w-3/4': isSharedBase && !isMobileMode, |
|
||||||
}" |
}" |
||||||
> |
> |
||||||
<template v-if="!isMobileMode"> |
<template v-if="!isMobileMode"> |
||||||
<NuxtLink |
<SmartsheetTopbarProjectListDropdown v-if="activeTable"> |
||||||
class="!hover:(text-black underline-gray-600) !underline-transparent ml-0.75 max-w-1/4" |
<template #default="{ isOpen }"> |
||||||
:class="{ |
<div |
||||||
'!max-w-none': isSharedBase && !isMobileMode, |
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" |
||||||
'!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" |
|
||||||
:class="{ |
:class="{ |
||||||
'!text-gray-500': !isMobileMode, |
'!max-w-none': isSharedBase && !isMobileMode, |
||||||
'!text-gray-700': 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> |
</template> |
||||||
</LazyGeneralEmojiPicker> |
</SmartsheetTopbarProjectListDropdown> |
||||||
<div |
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" /> |
||||||
v-if="activeTable" |
</template> |
||||||
:class="{ |
<template v-if="!(isMobileMode && !activeView?.is_default)"> |
||||||
'max-w-1/2': isMobileMode || activeView?.is_default, |
<SmartsheetTopbarTableListDropdown v-if="activeTable"> |
||||||
'max-w-20/100': !isSharedBase && !isMobileMode && !activeView?.is_default, |
<template #default="{ isOpen }"> |
||||||
'max-w-none': isSharedBase && !isMobileMode, |
<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" |
||||||
> |
|
||||||
<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" |
|
||||||
:class="{ |
:class="{ |
||||||
'text-gray-500': !isMobileMode, |
'max-w-full': isMobileMode, |
||||||
'text-gray-800 font-medium': isMobileMode || activeView?.is_default, |
'max-w-1/2': activeView?.is_default, |
||||||
}" |
'max-w-1/4': !isSharedBase && !isMobileMode && !activeView?.is_default, |
||||||
:style="{ |
'max-w-none': isSharedBase && !isMobileMode, |
||||||
wordBreak: 'keep-all', |
|
||||||
whiteSpace: 'nowrap', |
|
||||||
display: 'inline', |
|
||||||
}" |
}" |
||||||
> |
> |
||||||
<template v-if="activeView?.is_default"> |
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall" class="mr-1"> |
||||||
{{ activeTable?.title }} |
<template #default> |
||||||
</template> |
<GeneralIcon |
||||||
<NuxtLink |
icon="table" |
||||||
v-else |
class="min-w-5" |
||||||
class="!text-inherit !underline-transparent !hover:(text-black underline-gray-600)" |
:class="{ |
||||||
:to="tableUrl({ table: activeTable, completeUrl: true, isSharedBase })" |
'!text-gray-500': !isMobileMode, |
||||||
> |
'!text-gray-700': isMobileMode, |
||||||
{{ activeTable?.title }} |
}" |
||||||
</NuxtLink> |
/> |
||||||
</span> |
</template> |
||||||
</NcTooltip> |
</LazyGeneralEmojiPicker> |
||||||
</div> |
|
||||||
|
<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> |
</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)"> |
<template v-if="!(isMobileMode && activeView?.is_default)"> |
||||||
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeView?.meta?.icon" readonly size="xsmall"> |
<!-- <SmartsheetToolbarOpenedViewAction /> --> |
||||||
<template #default> |
|
||||||
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" /> |
<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> |
</template> |
||||||
</LazyGeneralEmojiPicker> |
</SmartsheetTopbarViewListDropdown> |
||||||
|
|
||||||
<SmartsheetToolbarOpenedViewAction /> |
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode" /> |
||||||
</template> |
</template> |
||||||
</div> |
</div> |
||||||
</template> |
</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