Browse Source

refactor(nc-gui): use `useDialog` to open view create modal

pull/4031/head
braks 2 years ago
parent
commit
d8cdc56ef2
  1. 31
      packages/nc-gui/components/dlg/ViewCreate.vue
  2. 36
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  3. 85
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  4. 22
      packages/nc-gui/composables/useDialog/index.ts
  5. 2
      packages/nc-gui/composables/useViews.ts
  6. 1
      packages/nc-gui/context/index.ts
  7. 1
      packages/nc-gui/lang/en.json

31
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
viewList: 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 { viewList = [], meta, selectedViewId, ...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) viewList.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,22 +105,23 @@ watch(
) )
function init() { function init() {
form.title = generateUniqueTitle(capitalize(ViewTypes[props.type].toLowerCase()), viewList?.value || [], 'title') form.title = generateUniqueTitle(capitalize(ViewTypes[props.type].toLowerCase()), viewList, '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 (props.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 = props.groupingFieldColumnId
@ -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">

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

@ -2,11 +2,9 @@
import type { ViewType, ViewTypes } from 'nocodb-sdk' import type { ViewType, 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 { Menu as AntMenu } from 'ant-design-vue'
import type { Ref } from 'vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import { import {
ActiveViewInj, ActiveViewInj,
ViewListInj,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
message, message,
@ -22,9 +20,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 +30,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 +56,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 +75,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 +93,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 +104,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
@ -198,7 +198,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,
}, },
}) })
} }

85
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,
@ -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],
@ -88,31 +71,45 @@ watch(
{ immediate: true }, { immediate: true },
) )
/** Open view creation modal */ /** Open delete modal */
function openModal({ function openCreateDialog({
title,
type, type,
title = '',
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
selectedViewId = copyViewId
kanbanGrpColumnId = groupingFieldColumnId
}
/** Handle view creation */ const { close } = useDialog(resolveComponent('DlgViewCreate'), {
async function onCreate(view: ViewType) { 'modelValue': isOpen,
await loadViews() title,
router.push({ params: { viewTitle: view.title || '' } }) type,
modalOpen = false meta,
$e('a:view:create', { view: view.type }) 'selectedViewId': copyViewId,
groupingFieldColumnId,
'viewList': views,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
await loadViews()
router.push({ params: { viewTitle: view.title || '' } })
$e('a:view:create', { view: view.type })
},
})
function closeDialog() {
isOpen.value = false
close(1000)
}
} }
</script> </script>
@ -132,22 +129,12 @@ async function onCreate(view: ViewType) {
/> />
<div v-if="isOpen" class="flex-1 flex flex-col min-h-0"> <div v-if="isOpen" class="flex-1 flex flex-col min-h-0">
<LazySmartsheetSidebarMenuTop @open-modal="openModal" @deleted="loadViews" /> <LazySmartsheetSidebarMenuTop :views="views" @open-modal="openCreateDialog" @deleted="loadViews" />
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="!my-3 w-full border-b-1" /> <div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="!my-3 w-full border-b-1" />
<LazySmartsheetSidebarMenuBottom @open-modal="openModal" /> <LazySmartsheetSidebarMenuBottom @open-modal="openCreateDialog" />
</div> </div>
<LazyDlgViewCreate
v-if="views"
v-model="modalOpen"
:title="viewCreateTitle"
:type="viewCreateType"
:selected-view-id="selectedViewId"
:grouping-field-column-id="kanbanGrpColumnId"
@created="onCreate"
/>
</a-layout-sider> </a-layout-sider>
</template> </template>

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

@ -1,8 +1,20 @@
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,
getCurrentInstance,
getCurrentScope,
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 +50,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 +65,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(

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

@ -18,7 +18,7 @@ export function useViews(meta: MaybeRef<TableType | undefined>) {
} }
} }
watch(meta, loadViews, { immediate: true }) watch(() => meta, loadViews, { immediate: true })
return { views: $$(views), loadViews } return { views: $$(views), 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