Browse Source

Merge pull request #4031 from nocodb/fix/sidebar-sort

pull/4064/head
Braks 2 years ago committed by GitHub
parent
commit
16e8c65de7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      packages/nc-gui/components/dlg/ViewCreate.vue
  2. 24
      packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue
  3. 59
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  4. 6
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  5. 103
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  6. 3
      packages/nc-gui/components/tabs/Smartsheet.vue
  7. 13
      packages/nc-gui/composables/useDialog/index.ts
  8. 42
      packages/nc-gui/composables/useKanbanViewStore.ts
  9. 14
      packages/nc-gui/composables/useViews.ts
  10. 1
      packages/nc-gui/context/index.ts
  11. 1
      packages/nc-gui/lang/en.json

37
packages/nc-gui/components/dlg/ViewCreate.vue

@ -2,17 +2,16 @@
import type { ComponentPublicInstance } from '@vue/runtime-core' import type { ComponentPublicInstance } from '@vue/runtime-core'
import type { Form as AntForm, SelectProps } from 'ant-design-vue' import type { Form as AntForm, SelectProps } from 'ant-design-vue'
import { capitalize } from '@vue/runtime-core' import { capitalize } from '@vue/runtime-core'
import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk' import type { FormType, GalleryType, GridType, KanbanType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes } from 'nocodb-sdk' import { UITypes, ViewTypes } from 'nocodb-sdk'
import { import {
MetaInj,
ViewListInj,
computed, computed,
generateUniqueTitle, generateUniqueTitle,
inject,
message, message,
nextTick, nextTick,
onBeforeMount,
reactive, reactive,
ref,
unref, unref,
useApi, useApi,
useI18n, useI18n,
@ -26,6 +25,8 @@ interface Props {
title?: string title?: string
selectedViewId?: string selectedViewId?: string
groupingFieldColumnId?: string groupingFieldColumnId?: string
views: ViewType[]
meta: TableType
} }
interface Emits { interface Emits {
@ -41,7 +42,7 @@ interface Form {
fk_grp_col_id: string | null fk_grp_col_id: string | null
} }
const props = defineProps<Props>() const { views = [], meta, selectedViewId, groupingFieldColumnId, ...props } = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
@ -55,10 +56,6 @@ const { t } = useI18n()
const { isLoading: loading, api } = useApi() const { isLoading: loading, api } = useApi()
const meta = inject(MetaInj, ref())
const viewList = inject(ViewListInj)
const form = reactive<Form>({ const form = reactive<Form>({
title: props.title || '', title: props.title || '',
type: props.type, type: props.type,
@ -75,7 +72,7 @@ const viewNameRules = [
{ {
validator: (_: unknown, v: string) => validator: (_: unknown, v: string) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
;(unref(viewList) || []).every((v1) => ((v1 as GridType | KanbanType | GalleryType).alias || v1.title) !== v) views.every((v1) => ((v1 as GridType | KanbanType | GalleryType).alias || v1.title) !== v)
? resolve(true) ? resolve(true)
: reject(new Error(`View name should be unique`)) : reject(new Error(`View name should be unique`))
}), }),
@ -98,7 +95,7 @@ const typeAlias = computed(
}[props.type]), }[props.type]),
) )
watch(vModel, (value) => value && init()) onBeforeMount(init)
watch( watch(
() => props.type, () => props.type,
@ -108,25 +105,26 @@ watch(
) )
function init() { function init() {
form.title = generateUniqueTitle(capitalize(ViewTypes[props.type].toLowerCase()), viewList?.value || [], 'title') form.title = generateUniqueTitle(capitalize(ViewTypes[props.type].toLowerCase()), views, 'title')
if (props.selectedViewId) { if (selectedViewId) {
form.copy_from_id = props.selectedViewId form.copy_from_id = selectedViewId
} }
// preset the grouping field column // preset the grouping field column
if (props.type === ViewTypes.KANBAN) { if (props.type === ViewTypes.KANBAN) {
singleSelectFieldOptions.value = meta singleSelectFieldOptions.value = meta
.value!.columns!.filter((el) => el.uidt === UITypes.SingleSelect) .columns!.filter((el) => el.uidt === UITypes.SingleSelect)
.map((field) => { .map((field) => {
return { return {
value: field.id, value: field.id,
label: field.title, label: field.title,
} }
}) })
if (props.groupingFieldColumnId) {
if (groupingFieldColumnId) {
// take from the one from copy view // take from the one from copy view
form.fk_grp_col_id = props.groupingFieldColumnId form.fk_grp_col_id = groupingFieldColumnId
} else { } else {
// take the first option // take the first option
form.fk_grp_col_id = singleSelectFieldOptions.value?.[0]?.value as string form.fk_grp_col_id = singleSelectFieldOptions.value?.[0]?.value as string
@ -186,7 +184,8 @@ async function onSubmit() {
<template> <template>
<a-modal v-model:visible="vModel" class="!top-[35%]" :confirm-loading="loading" wrap-class-name="nc-modal-view-create"> <a-modal v-model:visible="vModel" class="!top-[35%]" :confirm-loading="loading" wrap-class-name="nc-modal-view-create">
<template #title> <template #title>
{{ $t('general.create') }} <span class="text-capitalize">{{ typeAlias }}</span> {{ $t('objects.view') }} {{ $t(`general.${selectedViewId ? 'duplicate' : 'create'}`) }} <span class="text-capitalize">{{ typeAlias }}</span>
{{ $t('objects.view') }}
</template> </template>
<a-form ref="formValidator" layout="vertical" :model="form"> <a-form ref="formValidator" layout="vertical" :model="form">
@ -203,7 +202,7 @@ async function onSubmit() {
v-model:value="form.fk_grp_col_id" v-model:value="form.fk_grp_col_id"
class="w-full nc-kanban-grouping-field-select" class="w-full nc-kanban-grouping-field-select"
:options="singleSelectFieldOptions" :options="singleSelectFieldOptions"
:disabled="props.groupingFieldColumnId" :disabled="groupingFieldColumnId"
placeholder="Select a Grouping Field" placeholder="Select a Grouping Field"
not-found-content="No Single Select Field can be found. Please create one first." not-found-content="No Single Select Field can be found. Please create one first."
/> />

24
packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { useNuxtApp, useSmartsheetStoreOrThrow, useUIPermission, viewIcons } from '#imports' import { useNuxtApp, useSmartsheetStoreOrThrow, viewIcons } from '#imports'
interface Emits { interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void (event: 'openModal', data: { type: ViewTypes; title?: string }): void
@ -10,8 +10,6 @@ const emits = defineEmits<Emits>()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const { isSqlView } = useSmartsheetStoreOrThrow() const { isSqlView } = useSmartsheetStoreOrThrow()
function onOpenModal(type: ViewTypes, title = '') { function onOpenModal(type: ViewTypes, title = '') {
@ -22,14 +20,14 @@ function onOpenModal(type: ViewTypes, title = '') {
<template> <template>
<a-menu :selected-keys="[]" class="flex flex-col"> <a-menu :selected-keys="[]" class="flex flex-col">
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')"> <div>
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500"> <h3 class="px-3 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }} {{ $t('activity.createView') }}
</h3> </h3>
<a-menu-item <a-menu-item
key="grid" key="grid"
class="group !flex !items-center !my-0 !h-[30px] nc-create-grid-view" class="group !flex !items-center !my-0 !h-2.5rem nc-create-grid-view"
@click="onOpenModal(ViewTypes.GRID)" @click="onOpenModal(ViewTypes.GRID)"
> >
<a-tooltip :mouse-enter-delay="1" placement="left"> <a-tooltip :mouse-enter-delay="1" placement="left">
@ -37,7 +35,7 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('msg.info.addView.grid') }} {{ $t('msg.info.addView.grid') }}
</template> </template>
<div class="nc-project-menu-item text-xs flex items-center h-full w-full gap-2"> <div class="nc-project-menu-item !py-0 text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :style="{ color: viewIcons[ViewTypes.GRID].color }" /> <component :is="viewIcons[ViewTypes.GRID].icon" :style="{ color: viewIcons[ViewTypes.GRID].color }" />
<div>{{ $t('objects.viewType.grid') }}</div> <div>{{ $t('objects.viewType.grid') }}</div>
@ -51,7 +49,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<a-menu-item <a-menu-item
key="gallery" key="gallery"
class="group !flex !items-center !my-0 nc-create-gallery-view" class="group !flex !items-center !my-0 !h-2.5rem nc-create-gallery-view"
@click="onOpenModal(ViewTypes.GALLERY)" @click="onOpenModal(ViewTypes.GALLERY)"
> >
<a-tooltip :mouse-enter-delay="1" placement="left"> <a-tooltip :mouse-enter-delay="1" placement="left">
@ -59,7 +57,7 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('msg.info.addView.gallery') }} {{ $t('msg.info.addView.gallery') }}
</template> </template>
<div class="nc-project-menu-item text-xs flex items-center h-full w-full gap-2"> <div class="nc-project-menu-item !py-0 text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :style="{ color: viewIcons[ViewTypes.GALLERY].color }" /> <component :is="viewIcons[ViewTypes.GALLERY].icon" :style="{ color: viewIcons[ViewTypes.GALLERY].color }" />
<div>{{ $t('objects.viewType.gallery') }}</div> <div>{{ $t('objects.viewType.gallery') }}</div>
@ -74,7 +72,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<a-menu-item <a-menu-item
v-if="!isSqlView" v-if="!isSqlView"
key="form" key="form"
class="group !flex !items-center !my-0 !h-[30px] nc-create-form-view" class="group !flex !items-center !my-0 !h-2.5rem nc-create-form-view"
@click="onOpenModal(ViewTypes.FORM)" @click="onOpenModal(ViewTypes.FORM)"
> >
<a-tooltip :mouse-enter-delay="1" placement="left"> <a-tooltip :mouse-enter-delay="1" placement="left">
@ -82,7 +80,7 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('msg.info.addView.form') }} {{ $t('msg.info.addView.form') }}
</template> </template>
<div class="nc-project-menu-item text-xs flex items-center h-full w-full gap-2"> <div class="nc-project-menu-item !py-0 text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :style="{ color: viewIcons[ViewTypes.FORM].color }" /> <component :is="viewIcons[ViewTypes.FORM].icon" :style="{ color: viewIcons[ViewTypes.FORM].color }" />
<div>{{ $t('objects.viewType.form') }}</div> <div>{{ $t('objects.viewType.form') }}</div>
@ -96,7 +94,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<a-menu-item <a-menu-item
key="kanban" key="kanban"
class="group !flex !items-center !my-0 nc-create-kanban-view" class="group !flex !items-center !my-0 !h-2.5rem nc-create-kanban-view"
@click="onOpenModal(ViewTypes.KANBAN)" @click="onOpenModal(ViewTypes.KANBAN)"
> >
<a-tooltip :mouse-enter-delay="1" placement="left"> <a-tooltip :mouse-enter-delay="1" placement="left">
@ -104,7 +102,7 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('msg.info.addView.kanban') }} {{ $t('msg.info.addView.kanban') }}
</template> </template>
<div class="nc-project-menu-item text-xs flex items-center h-full w-full gap-2"> <div class="nc-project-menu-item !py-0 text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.KANBAN].icon" :style="{ color: viewIcons[ViewTypes.KANBAN].color }" /> <component :is="viewIcons[ViewTypes.KANBAN].icon" :style="{ color: viewIcons[ViewTypes.KANBAN].color }" />
<div>{{ $t('objects.viewType.kanban') }}</div> <div>{{ $t('objects.viewType.kanban') }}</div>

59
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -1,12 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ViewType, ViewTypes } from 'nocodb-sdk' import type { ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs' import type { SortableEvent } from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import type { Ref } from 'vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import { import {
ActiveViewInj, ActiveViewInj,
ViewListInj,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
message, message,
@ -22,9 +21,9 @@ import {
watch, watch,
} from '#imports' } from '#imports'
const emits = defineEmits<Emits>() interface Props {
views: ViewType[]
const { t } = useI18n() }
interface Emits { interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void (event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
@ -32,12 +31,16 @@ interface Emits {
(event: 'deleted'): void (event: 'deleted'): void
} }
const { views = [] } = defineProps<Props>()
const emits = defineEmits<Emits>()
const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const views = inject<Ref<ViewType[]>>(ViewListInj, ref([]))
const { api } = useApi() const { api } = useApi()
const router = useRouter() const router = useRouter()
@ -54,10 +57,8 @@ let isMarked = $ref<string | false>(false)
/** Watch currently active view, so we can mark it in the menu */ /** Watch currently active view, so we can mark it in the menu */
watch(activeView, (nextActiveView) => { watch(activeView, (nextActiveView) => {
const _nextActiveView = nextActiveView as ViewType if (nextActiveView && nextActiveView.id) {
selected.value = [nextActiveView.id]
if (_nextActiveView && _nextActiveView.id) {
selected.value = [_nextActiveView.id]
} }
}) })
@ -75,7 +76,7 @@ function validate(view: ViewType) {
return 'View name is required' return 'View name is required'
} }
if (views.value.some((v) => v.title === view.title && v.id !== view.id)) { if (views.some((v) => v.title === view.title && v.id !== view.id)) {
return 'View name should be unique' return 'View name should be unique'
} }
@ -93,7 +94,7 @@ async function onSortEnd(evt: SortableEvent) {
evt.preventDefault() evt.preventDefault()
dragging = false dragging = false
if (views.value.length < 2) return if (views.length < 2) return
const { newIndex = 0, oldIndex = 0 } = evt const { newIndex = 0, oldIndex = 0 } = evt
@ -104,17 +105,17 @@ async function onSortEnd(evt: SortableEvent) {
const previousEl = children[newIndex - 1] const previousEl = children[newIndex - 1]
const nextEl = children[newIndex + 1] const nextEl = children[newIndex + 1]
const currentItem = views.value.find((v) => v.id === evt.item.id) const currentItem = views.find((v) => v.id === evt.item.id)
if (!currentItem || !currentItem.id) return if (!currentItem || !currentItem.id) return
const previousItem = (previousEl ? views.value.find((v) => v.id === previousEl.id) : {}) as ViewType const previousItem = (previousEl ? views.find((v) => v.id === previousEl.id) : {}) as ViewType
const nextItem = (nextEl ? views.value.find((v) => v.id === nextEl.id) : {}) as ViewType const nextItem = (nextEl ? views.find((v) => v.id === nextEl.id) : {}) as ViewType
let nextOrder: number let nextOrder: number
// set new order value based on the new order of the items // set new order value based on the new order of the items
if (views.value.length - 1 === newIndex) { if (views.length - 1 === newIndex) {
nextOrder = parseFloat(String(previousItem.order)) + 1 nextOrder = parseFloat(String(previousItem.order)) + 1
} else if (newIndex === 0) { } else if (newIndex === 0) {
nextOrder = parseFloat(String(nextItem.order)) / 2 nextOrder = parseFloat(String(nextItem.order)) / 2
@ -149,9 +150,10 @@ const initSortable = (el: HTMLElement) => {
onMounted(() => menuRef && initSortable(menuRef.$el)) onMounted(() => menuRef && initSortable(menuRef.$el))
/** Navigate to view by changing url param */ /** Navigate to view by changing url param */
function changeView(view: { id: string; alias?: string; title?: string; type: ViewTypes }) { function changeView(view: ViewType) {
router.push({ params: { viewTitle: view.title || '' } }) router.push({ params: { viewTitle: view.title || '' } })
if (view.type === 1 && selected.value[0] === view.id) {
if (view.type === ViewTypes.FORM && selected.value[0] === view.id) {
// reload the page if the same form view is clicked // reload the page if the same form view is clicked
// router.go(0) // router.go(0)
// fix me: router.go(0) reloads entire page. need to reload only the form view // fix me: router.go(0) reloads entire page. need to reload only the form view
@ -183,7 +185,7 @@ async function onRename(view: ViewType) {
} }
/** Open delete modal */ /** Open delete modal */
function openDeleteDialog(view: Record<string, any>) { function openDeleteDialog(view: ViewType) {
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgViewDelete'), { const { close } = useDialog(resolveComponent('DlgViewDelete'), {
@ -198,7 +200,7 @@ function openDeleteDialog(view: Record<string, any>) {
// return to the default view // return to the default view
router.replace({ router.replace({
params: { params: {
viewTitle: views.value[0].title, viewTitle: views[0].title,
}, },
}) })
} }
@ -211,17 +213,12 @@ function openDeleteDialog(view: Record<string, any>) {
close(1000) close(1000)
} }
} }
watch(views, (nextViews) => {
if (nextViews?.length && (!activeView.value || !nextViews.includes(activeView.value))) {
activeView.value = nextViews[0]
}
})
</script> </script>
<template> <template>
<a-menu ref="menuRef" :class="{ dragging }" class="nc-views-menu flex-1" :selected-keys="selected"> <a-menu ref="menuRef" :class="{ dragging }" class="nc-views-menu flex-1" :selected-keys="selected">
<LazySmartsheetSidebarRenameableMenuItem <!-- Lazy load breaks menu item active styles, i.e. styles never change even when active item changes -->
<SmartsheetSidebarRenameableMenuItem
v-for="view of views" v-for="view of views"
:id="view.id" :id="view.id"
:key="view.id" :key="view.id"
@ -235,7 +232,7 @@ watch(views, (nextViews) => {
}" }"
@change-view="changeView" @change-view="changeView"
@open-modal="$emit('openModal', $event)" @open-modal="$emit('openModal', $event)"
@delete="openDeleteDialog(view)" @delete="openDeleteDialog"
@rename="onRename" @rename="onRename"
/> />
</a-menu> </a-menu>

6
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -30,7 +30,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'view', emits) as WritableComputedRef<ViewType & { is_default: boolean }> const vModel = useVModel(props, 'view', emits) as WritableComputedRef<ViewType & { alias?: string; is_default: boolean }>
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -56,6 +56,8 @@ const onClick = useDebounceFn(() => {
/** Enable editing view name on dbl click */ /** Enable editing view name on dbl click */
function onDblClick() { function onDblClick() {
if (!isUIAllowed('virtualViewsCreateOrEdit')) return
if (!isEditing) { if (!isEditing) {
isEditing = true isEditing = true
originalTitle = vModel.value.title originalTitle = vModel.value.title
@ -162,7 +164,7 @@ function onStopEdit() {
<template> <template>
<a-menu-item <a-menu-item
class="select-none group !flex !items-center !my-0 hover:(bg-primary !bg-opacity-5)" class="select-none group !flex !items-center !my-0 hover:(bg-primary !bg-opacity-5)"
@dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick()" @dblclick.stop="onDblClick"
@click.stop="onClick" @click.stop="onClick"
> >
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2"> <div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">

103
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -3,11 +3,11 @@ import type { ViewType, ViewTypes } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
MetaInj, MetaInj,
ViewListInj,
computed, computed,
inject, inject,
provide,
ref, ref,
resolveComponent,
useDialog,
useNuxtApp, useNuxtApp,
useRoute, useRoute,
useRouter, useRouter,
@ -21,7 +21,7 @@ const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const { views, loadViews } = useViews(meta) const { views, loadViews, isLoading } = useViews(meta)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -31,8 +31,6 @@ const route = useRoute()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
provide(ViewListInj, views)
/** Sidebar visible */ /** Sidebar visible */
const { isOpen } = useSidebar('nc-right-sidebar') const { isOpen } = useSidebar('nc-right-sidebar')
@ -41,21 +39,6 @@ const sidebarCollapsed = computed(() => !isOpen.value)
/** Sidebar ref */ /** Sidebar ref */
const sidebar = ref() const sidebar = ref()
/** View type to create from modal */
let viewCreateType = $ref<ViewTypes>()
/** View title to create from modal (when duplicating) */
let viewCreateTitle = $ref('')
/** selected view id for copying view meta */
let selectedViewId = $ref('')
/** Kanban Grouping Column Id for copying view meta */
let kanbanGrpColumnId = $ref('')
/** is view creation modal open */
let modalOpen = $ref(false)
/** Watch route param and change active view based on `viewTitle` */ /** Watch route param and change active view based on `viewTitle` */
watch( watch(
[views, () => route.params.viewTitle], [views, () => route.params.viewTitle],
@ -80,39 +63,54 @@ watch(
activeView.value = nextViews[0] activeView.value = nextViews[0]
} }
} }
/** if active view is not found, set it to first view */ /** if active view is not found, set it to first view */
if (!activeView.value && nextViews.length) { if (nextViews?.length && (!activeView.value || !nextViews.includes(activeView.value))) {
activeView.value = nextViews[0] activeView.value = nextViews[0]
} }
}, },
{ immediate: true }, { immediate: true },
) )
/** Open view creation modal */ /** Open delete modal */
function openModal({ function onOpenModal({
type,
title = '', title = '',
type,
copyViewId, copyViewId,
groupingFieldColumnId, groupingFieldColumnId,
}: { }: {
title?: string
type: ViewTypes type: ViewTypes
title: string copyViewId?: string
copyViewId: string groupingFieldColumnId?: string
groupingFieldColumnId: string
}) { }) {
modalOpen = true const isOpen = ref(true)
viewCreateType = type
viewCreateTitle = title const { close } = useDialog(resolveComponent('DlgViewCreate'), {
selectedViewId = copyViewId 'modelValue': isOpen,
kanbanGrpColumnId = groupingFieldColumnId title,
} type,
meta,
'selectedViewId': copyViewId,
groupingFieldColumnId,
'views': views,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
await loadViews()
router.push({ params: { viewTitle: view.title || '' } })
$e('a:view:create', { view: view.type })
},
})
/** Handle view creation */ function closeDialog() {
async function onCreate(view: ViewType) { isOpen.value = false
await loadViews()
router.push({ params: { viewTitle: view.title || '' } }) close(1000)
modalOpen = false }
$e('a:view:create', { view: view.type })
} }
</script> </script>
@ -127,27 +125,24 @@ async function onCreate(view: ViewType) {
theme="light" theme="light"
> >
<LazySmartsheetSidebarToolbar <LazySmartsheetSidebarToolbar
v-if="isOpen"
class="min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)] flex items-center py-3 px-3 justify-between border-b-1" class="min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)] flex items-center py-3 px-3 justify-between border-b-1"
/> />
<div v-if="isOpen" class="flex-1 flex flex-col min-h-0"> <div class="flex-1 flex flex-col min-h-0">
<LazySmartsheetSidebarMenuTop @open-modal="openModal" @deleted="loadViews" /> <GeneralOverlay v-if="!views.length" :model-value="isLoading" inline class="bg-gray-300/50">
<div class="w-full h-full flex items-center justify-center">
<a-spin />
</div>
</GeneralOverlay>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="!my-3 w-full border-b-1" /> <LazySmartsheetSidebarMenuTop :views="views" @open-modal="onOpenModal" @deleted="loadViews" />
<LazySmartsheetSidebarMenuBottom @open-modal="openModal" /> <template v-if="isUIAllowed('virtualViewsCreateOrEdit')">
</div> <div class="!my-3 w-full border-b-1" />
<LazyDlgViewCreate <LazySmartsheetSidebarMenuBottom @open-modal="onOpenModal" />
v-if="views" </template>
v-model="modalOpen" </div>
:title="viewCreateTitle"
:type="viewCreateType"
:selected-view-id="selectedViewId"
:grouping-field-column-id="kanbanGrpColumnId"
@created="onCreate"
/>
</a-layout-sider> </a-layout-sider>
</template> </template>

3
packages/nc-gui/components/tabs/Smartsheet.vue

@ -79,7 +79,8 @@ provide(TabMetaInj, activeTab)
</Transition> </Transition>
</div> </div>
<LazySmartsheetSidebar v-if="meta" class="nc-right-sidebar" /> <!-- Lazy loading the sidebar causes issues when deleting elements, i.e. it appears as if multiple elements are removed when they are not -->
<SmartsheetSidebar v-if="meta" class="nc-right-sidebar" />
</div> </div>
</template> </template>

13
packages/nc-gui/composables/useDialog/index.ts

@ -1,8 +1,9 @@
import type { VNode } from '@vue/runtime-dom' import type { VNode } from '@vue/runtime-dom'
import { isVNode, render } from '@vue/runtime-dom' import { isVNode, render } from '@vue/runtime-dom'
import type { ComponentPublicInstance } from '@vue/runtime-core' import type { ComponentPublicInstance } from '@vue/runtime-core'
import type { MaybeRef } from '@vueuse/core'
import { isClient } from '@vueuse/core' import { isClient } from '@vueuse/core'
import { createEventHook, h, ref, toReactive, tryOnScopeDispose, useNuxtApp, watch } from '#imports' import { createEventHook, h, ref, toReactive, tryOnScopeDispose, unref, useNuxtApp, watch } from '#imports'
/** /**
* Programmatically create a component and attach it to the body (or a specific mount target), like a dialog or modal. * Programmatically create a component and attach it to the body (or a specific mount target), like a dialog or modal.
@ -38,7 +39,7 @@ import { createEventHook, h, ref, toReactive, tryOnScopeDispose, useNuxtApp, wat
export function useDialog( export function useDialog(
componentOrVNode: any, componentOrVNode: any,
props: NonNullable<Parameters<typeof h>[1]> = {}, props: NonNullable<Parameters<typeof h>[1]> = {},
mountTarget?: Element | ComponentPublicInstance, mountTarget?: MaybeRef<Element | ComponentPublicInstance>,
) { ) {
if (typeof document === 'undefined' || !isClient) { if (typeof document === 'undefined' || !isClient) {
console.warn('[useDialog]: Cannot use outside of browser!') console.warn('[useDialog]: Cannot use outside of browser!')
@ -53,10 +54,12 @@ export function useDialog(
const vNodeRef = ref<VNode>() const vNodeRef = ref<VNode>()
mountTarget = mountTarget ? ('$el' in mountTarget ? (mountTarget.$el as HTMLElement) : mountTarget) : document.body let _mountTarget = unref(mountTarget)
_mountTarget = _mountTarget ? ('$el' in _mountTarget ? (_mountTarget.$el as HTMLElement) : _mountTarget) : document.body
/** if specified, append vnode to mount target instead of document.body */ /** if specified, append vnode to mount target instead of document.body */
mountTarget.appendChild(domNode) _mountTarget.appendChild(domNode)
/** When props change, we want to re-render the element with the new prop values */ /** When props change, we want to re-render the element with the new prop values */
const stop = watch( const stop = watch(
@ -87,7 +90,7 @@ export function useDialog(
setTimeout(() => { setTimeout(() => {
try { try {
;(mountTarget as HTMLElement)?.removeChild(domNode) ;(_mountTarget as HTMLElement)?.removeChild(domNode)
} catch (e) {} } catch (e) {}
}, 100) }, 100)

42
packages/nc-gui/composables/useKanbanViewStore.ts

@ -1,9 +1,27 @@
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import type { Api, ColumnType, KanbanType, SelectOptionType, SelectOptionsType, TableType, ViewType } from 'nocodb-sdk' import type { Api, ColumnType, KanbanType, SelectOptionType, SelectOptionsType, TableType, ViewType } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import { message } from 'ant-design-vue'
import type { Row } from '~/lib' import type { Row } from '~/lib'
import { SharedViewPasswordInj, deepCompare, enumColor, extractPkFromRow, useInjectionState, useNuxtApp } from '#imports' import {
IsPublicInj,
SharedViewPasswordInj,
deepCompare,
enumColor,
extractPkFromRow,
extractSdkResponseErrorMsg,
getHTMLEncodedText,
inject,
message,
provide,
ref,
useApi,
useI18n,
useInjectionState,
useNuxtApp,
useProject,
useSharedView,
useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports'
type GroupingFieldColOptionsType = SelectOptionType & { collapsed: boolean } type GroupingFieldColOptionsType = SelectOptionType & { collapsed: boolean }
@ -223,7 +241,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
groupingFieldColOptions.value = [ groupingFieldColOptions.value = [
...((groupingFieldColumn.value?.colOptions as SelectOptionsType & { collapsed: boolean })?.options ?? []), ...((groupingFieldColumn.value?.colOptions as SelectOptionsType & { collapsed: boolean })?.options ?? []),
// enrich uncategorized stack // enrich uncategorized stack
{ id: 'uncategorized', title: null, order: 0, color: enumColor.light[2] }, { id: 'uncategorized', title: null, order: 0, color: enumColor.light[2] } as any,
] ]
// sort by initial order // sort by initial order
.sort((a, b) => a.order! - b.order!) .sort((a, b) => a.order! - b.order!)
@ -302,15 +320,13 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
// } // }
) )
// audit // audit
$api.utils $api.utils.auditRowUpdate(id, {
.auditRowUpdate(id, { fk_model_id: meta.value?.id as string,
fk_model_id: meta.value?.id as string, column_name: property,
column_name: property, row_id: id,
row_id: id, value: getHTMLEncodedText(toUpdate.row[property]),
value: getHTMLEncodedText(toUpdate.row[property]), prev_value: getHTMLEncodedText(toUpdate.oldRow[property]),
prev_value: getHTMLEncodedText(toUpdate.oldRow[property]), })
})
.then(() => {})
/** update row data(to sync formula and other related columns) */ /** update row data(to sync formula and other related columns) */
Object.assign(toUpdate.row, updatedRowData) Object.assign(toUpdate.row, updatedRowData)

14
packages/nc-gui/composables/useViews.ts

@ -1,24 +1,28 @@
import type { TableType, ViewType } from 'nocodb-sdk' import type { TableType, ViewType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import { unref, useNuxtApp, watch } from '#imports' import { ref, unref, useNuxtApp, watch } from '#imports'
export function useViews(meta: MaybeRef<TableType | undefined>) { export function useViews(meta: MaybeRef<TableType | undefined>) {
let views = $ref<ViewType[]>([]) const views = ref<ViewType[]>([])
const isLoading = ref(false)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const loadViews = async () => { const loadViews = async () => {
isLoading.value = true
const _meta = unref(meta) const _meta = unref(meta)
if (_meta && _meta.id) { if (_meta && _meta.id) {
const response = (await $api.dbView.list(_meta.id)).list as ViewType[] const response = (await $api.dbView.list(_meta.id)).list as ViewType[]
if (response) { if (response) {
views = response.sort((a, b) => a.order! - b.order!) views.value = response.sort((a, b) => a.order! - b.order!)
} }
} }
isLoading.value = false
} }
watch(meta, loadViews, { immediate: true }) watch(() => unref(meta), loadViews, { immediate: true })
return { views: $$(views), loadViews } return { views, isLoading, loadViews }
} }

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

@ -27,7 +27,6 @@ export const ReloadViewMetaHookInj: InjectionKey<EventHook<boolean | void>> = Sy
export const ReloadRowDataHookInj: InjectionKey<EventHook<boolean | void>> = Symbol('reload-row-data-injection') export const ReloadRowDataHookInj: InjectionKey<EventHook<boolean | void>> = Symbol('reload-row-data-injection')
export const OpenNewRecordFormHookInj: InjectionKey<EventHook<void>> = Symbol('open-new-record-form-injection') export const OpenNewRecordFormHookInj: InjectionKey<EventHook<void>> = Symbol('open-new-record-form-injection')
export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection') export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')
export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection') export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection')
export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection') export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection')
export const CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url') export const CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url')

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

@ -16,6 +16,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"submit": "Submit", "submit": "Submit",
"create": "Create", "create": "Create",
"duplicate": "Duplicate",
"insert": "Insert", "insert": "Insert",
"delete": "Delete", "delete": "Delete",
"update": "Update", "update": "Update",

Loading…
Cancel
Save