mirror of https://github.com/nocodb/nocodb
Braks
2 years ago
committed by
GitHub
47 changed files with 18270 additions and 29426 deletions
After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,66 @@
|
||||
<script lang="ts" setup> |
||||
import { notification } from 'ant-design-vue' |
||||
import { extractSdkResponseErrorMsg } from '~/utils' |
||||
import { onKeyStroke, useApi, useNuxtApp, useVModel } from '#imports' |
||||
|
||||
interface Props { |
||||
modelValue: boolean |
||||
view?: Record<string, any> |
||||
} |
||||
|
||||
interface Emits { |
||||
(event: 'update:modelValue', data: boolean): void |
||||
(event: 'deleted'): void |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emits) |
||||
|
||||
const { api, isLoading } = useApi() |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
onKeyStroke('Escape', () => (vModel.value = false)) |
||||
|
||||
onKeyStroke('Enter', () => onDelete()) |
||||
|
||||
/** Delete a view */ |
||||
async function onDelete() { |
||||
if (!props.view) return |
||||
|
||||
try { |
||||
await api.dbView.delete(props.view.id) |
||||
|
||||
notification.success({ |
||||
message: 'View deleted successfully', |
||||
duration: 3, |
||||
}) |
||||
} catch (e: any) { |
||||
notification.error({ |
||||
message: await extractSdkResponseErrorMsg(e), |
||||
duration: 3, |
||||
}) |
||||
} |
||||
|
||||
emits('deleted') |
||||
|
||||
// telemetry event |
||||
$e('a:view:delete', { view: props.view.type }) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-modal v-model:visible="vModel" class="!top-[35%]" :confirm-loading="isLoading"> |
||||
<template #title> {{ $t('general.delete') }} {{ $t('objects.view') }} </template> |
||||
|
||||
Are you sure you want to delete this view? |
||||
|
||||
<template #footer> |
||||
<a-button key="back" @click="vModel = false">{{ $t('general.cancel') }}</a-button> |
||||
<a-button key="submit" type="danger" :loading="isLoading" @click="onDelete">{{ $t('general.submit') }}</a-button> |
||||
</template> |
||||
</a-modal> |
||||
</template> |
@ -0,0 +1,119 @@
|
||||
<script lang="ts" setup> |
||||
type FlipTrigger = 'hover' | 'click' | { duration: number } |
||||
|
||||
interface Props { |
||||
triggers?: FlipTrigger[] |
||||
duration?: number |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
triggers: () => ['click'] as FlipTrigger[], |
||||
duration: 800, |
||||
}) |
||||
|
||||
let flipped = $ref(false) |
||||
let hovered = $ref(false) |
||||
let flipTimer = $ref<NodeJS.Timer | null>(null) |
||||
|
||||
onMounted(() => { |
||||
const duration = props.triggers.reduce((dur, trigger) => { |
||||
if (typeof trigger !== 'string') { |
||||
dur = trigger.duration |
||||
} |
||||
|
||||
return dur |
||||
}, 0) |
||||
|
||||
if (duration > 0) { |
||||
flipTimer = setInterval(() => { |
||||
if (!hovered) { |
||||
flipped = !flipped |
||||
} |
||||
}, duration) |
||||
} |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
if (flipTimer) { |
||||
clearInterval(flipTimer) |
||||
} |
||||
}) |
||||
|
||||
function onHover(isHovering: boolean) { |
||||
hovered = isHovering |
||||
|
||||
if (props.triggers.find((trigger) => trigger === 'hover')) { |
||||
flipped = isHovering |
||||
} |
||||
} |
||||
|
||||
function onClick() { |
||||
if (props.triggers.find((trigger) => trigger === 'click')) { |
||||
flipped = !flipped |
||||
} |
||||
} |
||||
|
||||
let isFlipping = $ref(false) |
||||
|
||||
watch($$(flipped), () => { |
||||
isFlipping = true |
||||
|
||||
setTimeout(() => { |
||||
isFlipping = false |
||||
}, props.duration / 2) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flip-card" @click="onClick" @mouseover="onHover(true)" @mouseleave="onHover(false)"> |
||||
<div |
||||
class="flipper" |
||||
:style="{ '--flip-duration': `${props.duration || 800}ms`, 'transform': flipped ? 'rotateY(180deg)' : '' }" |
||||
> |
||||
<div |
||||
class="front" |
||||
:style="{ 'pointer-events': flipped ? 'none' : 'auto', 'opacity': !isFlipping ? (flipped ? 0 : 100) : flipped ? 100 : 0 }" |
||||
> |
||||
<slot name="front" /> |
||||
</div> |
||||
<div |
||||
class="back" |
||||
:style="{ 'pointer-events': flipped ? 'auto' : 'none', 'opacity': !isFlipping ? (flipped ? 100 : 0) : flipped ? 0 : 100 }" |
||||
> |
||||
<slot name="back" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.flip-card { |
||||
background-color: transparent; |
||||
perspective: 1000px; |
||||
} |
||||
|
||||
.flipper { |
||||
--flip-duration: 800ms; |
||||
|
||||
position: relative; |
||||
width: 100%; |
||||
height: 100%; |
||||
text-align: center; |
||||
transition: all ease-in-out; |
||||
transition-duration: var(--flip-duration); |
||||
transform-style: preserve-3d; |
||||
} |
||||
|
||||
.front, |
||||
.back { |
||||
position: absolute; |
||||
width: 100%; |
||||
height: 100%; |
||||
-webkit-backface-visibility: hidden; |
||||
backface-visibility: hidden; |
||||
} |
||||
|
||||
.back { |
||||
transform: rotateY(180deg); |
||||
} |
||||
</style> |
@ -1,281 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { ViewTypes } from 'nocodb-sdk' |
||||
import type { TableType } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import { inject, ref } from '#imports' |
||||
import { ActiveViewInj, MetaInj, ViewListInj } from '~/context' |
||||
import useViews from '~/composables/useViews' |
||||
import { viewIcons } from '~/utils/viewUtils' |
||||
import MdiPlusIcon from '~icons/mdi/plus' |
||||
|
||||
const meta = inject(MetaInj) |
||||
const activeView = inject(ActiveViewInj) |
||||
|
||||
const { views, loadViews } = useViews(meta as Ref<TableType>) |
||||
|
||||
provide(ViewListInj, views) |
||||
|
||||
const _isUIAllowed = (view: string) => {} |
||||
|
||||
// todo decide based on route param |
||||
loadViews().then(() => { |
||||
if (activeView) activeView.value = views.value?.[0] |
||||
}) |
||||
|
||||
const toggleDrawer = ref(false) |
||||
// todo: identify based on meta |
||||
const isView = ref(false) |
||||
const viewCreateType = ref<ViewTypes>() |
||||
const viewCreateDlg = ref<boolean>(false) |
||||
|
||||
const openCreateViewDlg = (type: ViewTypes) => { |
||||
viewCreateDlg.value = true |
||||
viewCreateType.value = type |
||||
} |
||||
|
||||
const onViewCreate = (view) => { |
||||
views.value?.push(view) |
||||
activeView.value = view |
||||
viewCreateDlg.value = false |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
class="views-navigation-drawer flex-item-stretch pa-4 elevation-1" |
||||
:style="{ |
||||
maxWidth: toggleDrawer ? '0' : '220px', |
||||
minWidth: toggleDrawer ? '0' : '220px', |
||||
}" |
||||
> |
||||
<div class="d-flex flex-column h-100"> |
||||
<div class="flex-grow-1"> |
||||
<v-list v-if="views && views.length" dense> |
||||
<v-list-item dense> |
||||
<!-- Views --> |
||||
<span class="body-2 font-weight-medium">{{ $t('objects.views') }}</span> |
||||
</v-list-item> |
||||
|
||||
<!-- <v-list-group v-model="selectedViewIdLocal" mandatory color="primary"> --> |
||||
<!-- |
||||
todo: add sortable |
||||
<draggable |
||||
:is="_isUIAllowed('viewlist-drag-n-drop') ? 'draggable' : 'div'" |
||||
v-model="viewsList" |
||||
draggable="div" |
||||
v-bind="dragOptions" |
||||
@change="onMove($event)" |
||||
> --> |
||||
<!-- <transition-group |
||||
type="transition" |
||||
:name="!drag ? 'flip-list' : null" |
||||
> --> |
||||
<v-list-item |
||||
v-for="view in views" |
||||
:key="view.id" |
||||
v-t="['a:view:open', { view: view.type }]" |
||||
dense |
||||
:value="view.id" |
||||
active-class="x-active--text" |
||||
@click="activeView = view" |
||||
> |
||||
<!-- :class="`body-2 view nc-view-item nc-draggable-child nc-${ |
||||
viewTypeAlias[view.type] |
||||
}-view-item`" |
||||
@click="$emit('rerender')" --> |
||||
<!-- <v-icon |
||||
v-if="_isUIAllowed('viewlist-drag-n-drop')" |
||||
small |
||||
:class="`nc-child-draggable-icon nc-child-draggable-icon-${view.title}`" |
||||
@click.stop |
||||
> |
||||
mdi-drag-vertical |
||||
</v-icon> --> |
||||
<!-- <v-list-item-icon class="mr-n1"> |
||||
<v-icon v-if="viewIcons[view.type]" x-small :color="viewIcons[view.type].color"> |
||||
{{ viewIcons[view.type].icon }} |
||||
</v-icon> |
||||
<v-icon v-else color="primary" small> mdi-table </v-icon> |
||||
</v-list-item-icon> --> |
||||
<component :is="viewIcons[view.type].icon" :class="`text-${viewIcons[view.type].color} mr-1`" /> |
||||
<span>{{ view.alias || view.title }}</span> |
||||
|
||||
<!-- <v-list-item-title> |
||||
<v-tooltip bottom> |
||||
<template #activator="{ on }"> |
||||
<div class="font-weight-regular" style="overflow: hidden; text-overflow: ellipsis"> |
||||
<input v-if="view.edit" :ref="`input${i}`" v-model="view.title_temp" /> |
||||
|
||||
<!– @click.stop |
||||
@keydown.enter.stop="updateViewName(view, i)" |
||||
@blur="updateViewName(view, i)" –> |
||||
<template v-else> |
||||
<span v-on="on">{{ view.alias || view.title }}</span> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
{{ view.alias || view.title }} |
||||
</v-tooltip> |
||||
</v-list-item-title> --> |
||||
<v-spacer /> |
||||
<!-- <template v-if="_isUIAllowed('virtualViewsCreateOrEdit')"> |
||||
<!– Copy view –> |
||||
<x-icon |
||||
v-if="!view.edit" |
||||
:tooltip="$t('activity.copyView')" |
||||
x-small |
||||
color="primary" |
||||
icon-class="view-icon nc-view-copy-icon" |
||||
@click.stop="copyView(view, i)" |
||||
> |
||||
mdi-content-copy |
||||
</x-icon> |
||||
<!– Rename view –> |
||||
<x-icon |
||||
v-if="!view.edit" |
||||
:tooltip="$t('activity.renameView')" |
||||
x-small |
||||
color="primary" |
||||
icon-class="view-icon nc-view-edit-icon" |
||||
@click.stop="showRenameTextBox(view, i)" |
||||
> |
||||
mdi-pencil |
||||
</x-icon> |
||||
<!– Delete view" –> |
||||
<x-icon |
||||
v-if="!view.is_default" |
||||
:tooltip="$t('activity.deleteView')" |
||||
small |
||||
color="error" |
||||
icon-class="view-icon nc-view-delete-icon" |
||||
@click.stop="deleteView(view)" |
||||
> |
||||
mdi-delete-outline |
||||
</x-icon> |
||||
</template> |
||||
<v-icon |
||||
v-if="view.id === selectedViewId" |
||||
small |
||||
class="check-icon" |
||||
> |
||||
mdi-check-bold |
||||
</v-icon> --> |
||||
</v-list-item> |
||||
<!-- </transition-group> --> |
||||
<!-- </draggable> --> |
||||
<!-- </v-list-group> --> |
||||
</v-list> |
||||
|
||||
<v-divider class="advance-menu-divider" /> |
||||
|
||||
<v-list dense> |
||||
<v-list-item dense> |
||||
<!-- Create a View --> |
||||
<span class="body-2 font-weight-medium" @dblclick="enableDummyFeat = true"> |
||||
{{ $t('activity.createView') }} |
||||
</span> |
||||
<!-- <v-tooltip top> |
||||
<template #activator="{ props }"> |
||||
<!– <x-icon –> |
||||
<!– color="pink textColor" –> |
||||
<!– icon-class="ml-2" –> |
||||
<!– small –> |
||||
<!– v-on="on" –> |
||||
<!– @mouseenter="overShieldIcon = true" –> |
||||
<!– @mouseleave="overShieldIcon = false" –> |
||||
<!– > –> |
||||
<!– mdi-shield-lock-outline –> |
||||
<!– </x-icon> –> |
||||
</template> |
||||
<!– Only visible to Creator –> |
||||
<span class="caption"> |
||||
{{ $t('msg.info.onlyCreator') }} |
||||
</span> |
||||
</v-tooltip> --> |
||||
</v-list-item> |
||||
<v-tooltip bottom> |
||||
<template #activator="{ props }"> |
||||
<v-list-item dense class="body-2 nc-create-grid-view" v-bind="props" @click="openCreateViewDlg(ViewTypes.GRID)"> |
||||
<!-- <v-list-item-icon class="mr-n1"> --> |
||||
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color} mr-1`" /> |
||||
<!-- </v-list-item-icon> --> |
||||
<v-list-item-title> |
||||
<span class="font-weight-regular"> |
||||
<!-- Grid --> |
||||
{{ $t('objects.viewType.grid') }} |
||||
</span> |
||||
</v-list-item-title> |
||||
<v-spacer /> |
||||
<MdiPlusIcon class="mr-1" /> |
||||
<!-- <v-icon class="mr-1" small> mdi-plus</v-icon> --> |
||||
</v-list-item> |
||||
</template> |
||||
<!-- Add Grid View --> |
||||
{{ $t('msg.info.addView.grid') }} |
||||
</v-tooltip> |
||||
<v-tooltip bottom> |
||||
<template #activator="{ props }"> |
||||
<v-list-item |
||||
dense |
||||
class="body-2 nc-create-gallery-view" |
||||
v-bind="props" |
||||
@click="openCreateViewDlg(ViewTypes.GALLERY)" |
||||
> |
||||
<!-- <v-list-item-icon class="mr-n1"> --> |
||||
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color} mr-1`" /> |
||||
<!-- </v-list-item-icon> --> |
||||
<v-list-item-title> |
||||
<span class="font-weight-regular"> |
||||
<!-- Gallery --> |
||||
{{ $t('objects.viewType.gallery') }} |
||||
</span> |
||||
</v-list-item-title> |
||||
|
||||
<v-spacer /> |
||||
|
||||
<MdiPlusIcon class="mr-1" /> |
||||
<!-- <v-icon class="mr-1" small> mdi-plus</v-icon> --> |
||||
</v-list-item> |
||||
</template> |
||||
<!-- Add Gallery View --> |
||||
{{ $t('msg.info.addView.gallery') }} |
||||
</v-tooltip> |
||||
|
||||
<v-tooltip bottom> |
||||
<template #activator="{ props }"> |
||||
<v-list-item |
||||
v-if="!isView" |
||||
dense |
||||
class="body-2 nc-create-form-view" |
||||
v-bind="props" |
||||
@click="openCreateViewDlg(ViewTypes.FORM)" |
||||
> |
||||
<!-- <v-list-item-icon class="mr-n1"> --> |
||||
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color} mr-1`" /> |
||||
<!-- </v-list-item-icon> --> |
||||
<v-list-item-title> |
||||
<span class="font-weight-regular"> |
||||
<!-- Form --> |
||||
|
||||
{{ $t('objects.viewType.form') }} |
||||
</span> |
||||
</v-list-item-title> |
||||
|
||||
<v-spacer /> |
||||
|
||||
<MdiPlusIcon class="mr-1" /> |
||||
<!-- <v-icon class="mr-1" small> mdi-plus</v-icon> --> |
||||
</v-list-item> |
||||
</template> |
||||
<!-- Add Form View --> |
||||
{{ $t('msg.info.addView.form') }} |
||||
</v-tooltip> |
||||
</v-list> |
||||
</div> |
||||
</div> |
||||
|
||||
<DlgViewCreate v-if="views" v-model="viewCreateDlg" :type="viewCreateType" @created="onViewCreate" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,148 @@
|
||||
<script lang="ts" setup> |
||||
import { ViewTypes } from 'nocodb-sdk' |
||||
import { ref, useNuxtApp } from '#imports' |
||||
import { viewIcons } from '~/utils' |
||||
import MdiPlusIcon from '~icons/mdi/plus' |
||||
import MdiXml from '~icons/mdi/xml' |
||||
import MdiHook from '~icons/mdi/hook' |
||||
import MdiHeartsCard from '~icons/mdi/cards-heart' |
||||
import MdiShieldLockOutline from '~icons/mdi/shield-lock-outline' |
||||
|
||||
interface Emits { |
||||
(event: 'openModal', data: { type: ViewTypes; title?: string }): void |
||||
} |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
const isView = ref(false) |
||||
|
||||
function onApiSnippet() { |
||||
// get API snippet |
||||
$e('a:view:api-snippet') |
||||
} |
||||
|
||||
function onOpenModal(type: ViewTypes, title = '') { |
||||
emits('openModal', { type, title }) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-menu :selected-keys="[]" class="flex-1 flex flex-col"> |
||||
<a-divider class="my-2" /> |
||||
|
||||
<h3 class="px-3 text-xs font-semibold flex items-center gap-4"> |
||||
{{ $t('activity.createView') }} |
||||
<a-tooltip> |
||||
<template #title> |
||||
{{ $t('msg.info.onlyCreator') }} |
||||
</template> |
||||
<MdiShieldLockOutline class="text-pink-500" /> |
||||
</a-tooltip> |
||||
</h3> |
||||
|
||||
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)"> |
||||
<a-tooltip placement="left"> |
||||
<template #title> |
||||
{{ $t('msg.info.addView.grid') }} |
||||
</template> |
||||
|
||||
<div class="text-xs flex items-center h-full w-full gap-2"> |
||||
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" /> |
||||
|
||||
<div>{{ $t('objects.viewType.grid') }}</div> |
||||
|
||||
<div class="flex-1" /> |
||||
|
||||
<MdiPlusIcon class="group-hover:text-primary" /> |
||||
</div> |
||||
</a-tooltip> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)"> |
||||
<a-tooltip placement="left"> |
||||
<template #title> |
||||
{{ $t('msg.info.addView.gallery') }} |
||||
</template> |
||||
|
||||
<div class="text-xs flex items-center h-full w-full gap-2"> |
||||
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" /> |
||||
|
||||
<div>{{ $t('objects.viewType.gallery') }}</div> |
||||
|
||||
<div class="flex-1" /> |
||||
|
||||
<MdiPlusIcon class="group-hover:text-primary" /> |
||||
</div> |
||||
</a-tooltip> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item v-if="!isView" key="form" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.FORM)"> |
||||
<a-tooltip placement="left"> |
||||
<template #title> |
||||
{{ $t('msg.info.addView.form') }} |
||||
</template> |
||||
|
||||
<div class="text-xs flex items-center h-full w-full gap-2"> |
||||
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" /> |
||||
|
||||
<div>{{ $t('objects.viewType.form') }}</div> |
||||
|
||||
<div class="flex-1" /> |
||||
|
||||
<MdiPlusIcon class="group-hover:text-primary" /> |
||||
</div> |
||||
</a-tooltip> |
||||
</a-menu-item> |
||||
|
||||
<div class="flex-auto justify-end flex flex-col md:gap-4 lg:gap-8 mt-4"> |
||||
<button |
||||
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease" |
||||
@click="onApiSnippet" |
||||
> |
||||
<MdiXml />Get API Snippet |
||||
</button> |
||||
|
||||
<button |
||||
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease" |
||||
@click="onApiSnippet" |
||||
> |
||||
<MdiHook />{{ $t('objects.webhooks') }} |
||||
</button> |
||||
</div> |
||||
|
||||
<general-flipping-card class="my-4 lg:my-6 min-h-[100px] w-[250px]" :triggers="['click', { duration: 15000 }]"> |
||||
<template #front> |
||||
<div class="flex h-full w-full gap-6 flex-col"> |
||||
<general-social /> |
||||
|
||||
<div> |
||||
<a |
||||
v-t="['e:hiring']" |
||||
class="px-4 py-3 !bg-primary rounded shadow text-white" |
||||
href="https://angel.co/company/nocodb" |
||||
target="_blank" |
||||
@click.stop |
||||
> |
||||
🚀 We are Hiring! 🚀 |
||||
</a> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #back> |
||||
<!-- todo: add project cost --> |
||||
<a |
||||
href="https://github.com/sponsors/nocodb" |
||||
target="_blank" |
||||
class="group flex items-center gap-2 w-full mx-3 px-4 py-2 rounded-l !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg !opacity-100) transition duration-150 ease" |
||||
@click.stop |
||||
> |
||||
<MdiHeartsCard class="text-red-500" /> |
||||
{{ $t('activity.sponsorUs') }} |
||||
</a> |
||||
</template> |
||||
</general-flipping-card> |
||||
</a-menu> |
||||
</template> |
@ -0,0 +1,244 @@
|
||||
<script lang="ts" setup> |
||||
import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'nocodb-sdk' |
||||
import type { SortableEvent } from 'sortablejs' |
||||
import type { Menu as AntMenu } from 'ant-design-vue' |
||||
import { notification } from 'ant-design-vue' |
||||
import type { Ref } from 'vue' |
||||
import Sortable from 'sortablejs' |
||||
import RenameableMenuItem from './RenameableMenuItem.vue' |
||||
import { inject, onMounted, ref, useApi, useTabs, watch } from '#imports' |
||||
import { extractSdkResponseErrorMsg } from '~/utils' |
||||
import type { TabItem } from '~/composables/useTabs' |
||||
import { TabType } from '~/composables/useTabs' |
||||
import { ActiveViewInj, ViewListInj } from '~/context' |
||||
|
||||
interface Emits { |
||||
(event: 'openModal', data: { type: ViewTypes; title?: string }): void |
||||
(event: 'deleted'): void |
||||
(event: 'sorted'): void |
||||
} |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const activeView = inject(ActiveViewInj, ref()) |
||||
|
||||
const views = inject<Ref<any[]>>(ViewListInj, ref([])) |
||||
|
||||
const { addTab } = useTabs() |
||||
|
||||
const { api } = useApi() |
||||
|
||||
/** Selected view(s) for menu */ |
||||
const selected = ref<string[]>([]) |
||||
|
||||
/** dragging renamable view items */ |
||||
let dragging = $ref(false) |
||||
|
||||
let deleteModalVisible = $ref(false) |
||||
|
||||
/** view to delete for modal */ |
||||
let toDelete = $ref<Record<string, any> | undefined>() |
||||
|
||||
const menuRef = $ref<typeof AntMenu>() |
||||
|
||||
let isMarked = $ref<string | false>(false) |
||||
|
||||
/** Watch currently active view, so we can mark it in the menu */ |
||||
watch(activeView, (nextActiveView) => { |
||||
const _nextActiveView = nextActiveView as GridType | FormType | KanbanType |
||||
|
||||
if (_nextActiveView && _nextActiveView.id) { |
||||
selected.value = [_nextActiveView.id] |
||||
} |
||||
}) |
||||
|
||||
/** shortly mark an item after sorting */ |
||||
function markItem(id: string) { |
||||
isMarked = id |
||||
setTimeout(() => { |
||||
isMarked = false |
||||
}, 300) |
||||
} |
||||
|
||||
/** validate view title */ |
||||
function validate(value?: string) { |
||||
if (!value || value.trim().length < 0) { |
||||
return 'View name is required' |
||||
} |
||||
|
||||
if (views.value.every((v1) => ((v1 as GridType | KanbanType | GalleryType).alias || v1.title) !== value)) { |
||||
return 'View name should be unique' |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
function onSortStart(evt: SortableEvent) { |
||||
evt.stopImmediatePropagation() |
||||
evt.preventDefault() |
||||
dragging = true |
||||
} |
||||
|
||||
async function onSortEnd(evt: SortableEvent) { |
||||
evt.stopImmediatePropagation() |
||||
evt.preventDefault() |
||||
dragging = false |
||||
|
||||
if (views.value.length < 2) return |
||||
|
||||
const { newIndex = 0, oldIndex = 0 } = evt |
||||
|
||||
if (newIndex === oldIndex) return |
||||
|
||||
const children = evt.to.children as unknown as HTMLLIElement[] |
||||
|
||||
const previousEl = children[newIndex - 1] |
||||
const nextEl = children[newIndex + 1] |
||||
|
||||
const currentItem: Record<string, any> = views.value.find((v) => v.id === evt.item.id) |
||||
const previousItem: Record<string, any> = previousEl ? views.value.find((v) => v.id === previousEl.id) : {} |
||||
const nextItem: Record<string, any> = nextEl ? views.value.find((v) => v.id === nextEl.id) : {} |
||||
|
||||
let nextOrder: number |
||||
|
||||
// set new order value based on the new order of the items |
||||
if (views.value.length - 1 === newIndex) { |
||||
nextOrder = parseFloat(previousItem.order) + 1 |
||||
} else if (newIndex === 0) { |
||||
nextOrder = parseFloat(nextItem.order) / 2 |
||||
} else { |
||||
nextOrder = (parseFloat(previousItem.order) + parseFloat(nextItem.order)) / 2 |
||||
} |
||||
|
||||
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder.toString() : oldIndex.toString() |
||||
|
||||
currentItem.order = _nextOrder |
||||
|
||||
await api.dbView.update(currentItem.id, { order: _nextOrder }) |
||||
|
||||
markItem(currentItem.id) |
||||
} |
||||
|
||||
let sortable: Sortable |
||||
|
||||
// todo: replace with vuedraggable |
||||
const initSortable = (el: HTMLElement) => { |
||||
if (sortable) sortable.destroy() |
||||
|
||||
sortable = new Sortable(el, { |
||||
handle: '.nc-drag-icon', |
||||
ghostClass: 'ghost', |
||||
onStart: onSortStart, |
||||
onEnd: onSortEnd, |
||||
}) |
||||
} |
||||
|
||||
onMounted(() => menuRef && initSortable(menuRef.$el)) |
||||
|
||||
// todo: fix view type, alias is missing for some reason? |
||||
/** Navigate to view and add new tab if necessary */ |
||||
function changeView(view: { id: string; alias?: string; title?: string; type: ViewTypes }) { |
||||
activeView.value = view |
||||
|
||||
const tabProps: TabItem = { |
||||
id: view.id, |
||||
title: (view.alias ?? view.title) || '', |
||||
type: TabType.VIEW, |
||||
} |
||||
|
||||
addTab(tabProps) |
||||
} |
||||
|
||||
/** Rename a view */ |
||||
async function onRename(view: Record<string, any>) { |
||||
const valid = validate(view.title) |
||||
|
||||
if (valid !== true) { |
||||
notification.error({ |
||||
message: valid, |
||||
duration: 2, |
||||
}) |
||||
} |
||||
|
||||
try { |
||||
// todo typing issues, order and id do not exist on all members of ViewTypes (Kanban, Gallery, Form, Grid) |
||||
await api.dbView.update(view.id, { |
||||
title: view.title, |
||||
order: view.order, |
||||
}) |
||||
|
||||
notification.success({ |
||||
message: 'View renamed successfully', |
||||
duration: 3, |
||||
}) |
||||
} catch (e: any) { |
||||
notification.error({ |
||||
message: await extractSdkResponseErrorMsg(e), |
||||
duration: 3, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/** Open delete modal */ |
||||
async function onDelete(view: Record<string, any>) { |
||||
toDelete = view |
||||
deleteModalVisible = true |
||||
} |
||||
|
||||
/** View was deleted, trigger reload */ |
||||
function onDeleted() { |
||||
emits('deleted') |
||||
toDelete = undefined |
||||
deleteModalVisible = false |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<h3 class="nc-headline pt-3 px-3 text-xs font-semibold">{{ $t('objects.views') }}</h3> |
||||
|
||||
<a-menu ref="menuRef" :class="{ dragging }" class="nc-views-menu" :selected-keys="selected"> |
||||
<RenameableMenuItem |
||||
v-for="view of views" |
||||
:id="view.id" |
||||
:key="view.id" |
||||
:view="view" |
||||
class="transition-all ease-in duration-300" |
||||
:class="[isMarked === view.id ? 'bg-gray-200' : '']" |
||||
@change-view="changeView" |
||||
@open-modal="$emit('openModal', $event)" |
||||
@delete="onDelete" |
||||
@rename="onRename" |
||||
/> |
||||
</a-menu> |
||||
|
||||
<dlg-view-delete v-model="deleteModalVisible" :view="toDelete" @deleted="onDeleted" /> |
||||
</template> |
||||
|
||||
<style lang="scss"> |
||||
.nc-views-menu { |
||||
@apply flex-1 max-h-[50vh] md:max-h-[200px] lg:max-h-[400px] xl:max-h-[600px] overflow-y-scroll scrollbar-thin-primary; |
||||
|
||||
.ghost, |
||||
.ghost > * { |
||||
@apply !pointer-events-none; |
||||
} |
||||
|
||||
&.dragging { |
||||
.nc-icon { |
||||
@apply !hidden; |
||||
} |
||||
|
||||
.nc-view-icon { |
||||
@apply !block; |
||||
} |
||||
} |
||||
|
||||
.ant-menu-item:not(.sortable-chosen) { |
||||
@apply color-transition hover:!bg-transparent; |
||||
} |
||||
|
||||
.sortable-chosen { |
||||
@apply !bg-primary/25 text-primary; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,179 @@
|
||||
<script lang="ts" setup> |
||||
import type { ViewTypes } from 'nocodb-sdk' |
||||
import { viewIcons } from '~/utils' |
||||
import { useDebounceFn, useNuxtApp, useVModel } from '#imports' |
||||
import MdiTrashCan from '~icons/mdi/trash-can' |
||||
import MdiContentCopy from '~icons/mdi/content-copy' |
||||
import MdiDrag from '~icons/mdi/drag-vertical' |
||||
|
||||
interface Props { |
||||
view: Record<string, any> |
||||
} |
||||
|
||||
interface Emits { |
||||
(event: 'openModal', data: { type: ViewTypes; title?: string }): void |
||||
(event: 'update:view', data: Record<string, any>): void |
||||
(event: 'changeView', view: Record<string, any>): void |
||||
(event: 'rename', view: Record<string, any>): void |
||||
(event: 'delete', view: Record<string, any>): void |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const vModel = useVModel(props, 'view', emits) |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
/** Is editing the view name enabled */ |
||||
let isEditing = $ref<boolean>(false) |
||||
|
||||
/** Helper to check if editing was disabled before the view navigation timeout triggers */ |
||||
let isStopped = $ref(false) |
||||
|
||||
/** Original view title when editing the view name */ |
||||
let originalTitle = $ref<string | undefined>() |
||||
|
||||
/** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */ |
||||
const onClick = useDebounceFn(() => { |
||||
if (isEditing || isStopped) return |
||||
|
||||
emits('changeView', vModel.value) |
||||
}, 250) |
||||
|
||||
/** Enable editing view name on dbl click */ |
||||
function onDblClick() { |
||||
if (!isEditing) { |
||||
isEditing = true |
||||
originalTitle = vModel.value.title |
||||
} |
||||
} |
||||
|
||||
/** Handle keydown on input field */ |
||||
function onKeyDown(event: KeyboardEvent) { |
||||
if (event.key === 'Escape') { |
||||
onKeyEsc(event) |
||||
} else if (event.key === 'Enter') { |
||||
onKeyEnter(event) |
||||
} |
||||
} |
||||
|
||||
/** Rename view when enter is pressed */ |
||||
function onKeyEnter(event: KeyboardEvent) { |
||||
event.stopImmediatePropagation() |
||||
event.preventDefault() |
||||
|
||||
onRename() |
||||
} |
||||
|
||||
/** Disable renaming view when escape is pressed */ |
||||
function onKeyEsc(event: KeyboardEvent) { |
||||
event.stopImmediatePropagation() |
||||
event.preventDefault() |
||||
|
||||
onCancel() |
||||
} |
||||
|
||||
onKeyStroke('Enter', (event) => { |
||||
if (isEditing) { |
||||
onKeyEnter(event) |
||||
} |
||||
}) |
||||
|
||||
function focusInput(el: HTMLInputElement) { |
||||
if (el) el.focus() |
||||
} |
||||
|
||||
/** Duplicate a view */ |
||||
// todo: This is not really a duplication, maybe we need to implement a true duplication? |
||||
function onDuplicate() { |
||||
emits('openModal', { type: vModel.value.type, title: vModel.value.title }) |
||||
|
||||
$e('c:view:copy', { view: vModel.value.type }) |
||||
} |
||||
|
||||
/** Delete a view */ |
||||
async function onDelete() { |
||||
emits('delete', vModel.value) |
||||
} |
||||
|
||||
/** Rename a view */ |
||||
async function onRename() { |
||||
if (!isEditing) return |
||||
|
||||
if (vModel.value.title === '' || vModel.value.title === originalTitle) { |
||||
onCancel() |
||||
return |
||||
} |
||||
|
||||
emits('rename', vModel.value) |
||||
|
||||
onStopEdit() |
||||
} |
||||
|
||||
/** Cancel renaming view */ |
||||
function onCancel() { |
||||
if (!isEditing) return |
||||
|
||||
vModel.value.title = originalTitle |
||||
onStopEdit() |
||||
} |
||||
|
||||
/** Stop editing view name, timeout makes sure that view navigation (click trigger) does not pick up before stop is done */ |
||||
function onStopEdit() { |
||||
isStopped = true |
||||
isEditing = false |
||||
originalTitle = '' |
||||
|
||||
setTimeout(() => { |
||||
isStopped = false |
||||
}, 250) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-menu-item class="select-none group !flex !items-center !my-0" @dblclick.stop="onDblClick" @click.stop="onClick"> |
||||
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2"> |
||||
<div class="flex w-auto"> |
||||
<MdiDrag |
||||
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move" |
||||
@click.stop.prevent |
||||
/> |
||||
|
||||
<component |
||||
:is="viewIcons[vModel.type].icon" |
||||
class="nc-view-icon group-hover:hidden" |
||||
:class="`text-${viewIcons[vModel.type].color}`" |
||||
/> |
||||
</div> |
||||
|
||||
<a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" /> |
||||
<div v-else>{{ vModel.alias || vModel.title }}</div> |
||||
|
||||
<div class="flex-1" /> |
||||
|
||||
<template v-if="!isEditing"> |
||||
<div class="flex items-center gap-1"> |
||||
<a-tooltip placement="left"> |
||||
<template #title> |
||||
{{ $t('activity.copyView') }} |
||||
</template> |
||||
|
||||
<MdiContentCopy class="hidden group-hover:block text-gray-500" @click.stop="onDuplicate" /> |
||||
</a-tooltip> |
||||
|
||||
<template v-if="!vModel.is_default"> |
||||
<a-tooltip placement="left"> |
||||
<template #title> |
||||
{{ $t('activity.deleteView') }} |
||||
</template> |
||||
|
||||
<MdiTrashCan class="hidden group-hover:block text-red-500" @click.stop="onDelete" /> |
||||
</a-tooltip> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</a-menu-item> |
||||
</template> |
@ -0,0 +1,71 @@
|
||||
<script setup lang="ts"> |
||||
import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'nocodb-sdk' |
||||
import MenuTop from './MenuTop.vue' |
||||
import MenuBottom from './MenuBottom.vue' |
||||
import { inject, provide, ref, useApi, useViews, watch } from '#imports' |
||||
import { ActiveViewInj, MetaInj, ViewListInj } from '~/context' |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
const activeView = inject(ActiveViewInj, ref()) |
||||
|
||||
const { views, loadViews } = useViews(meta) |
||||
|
||||
const { api } = useApi() |
||||
|
||||
provide(ViewListInj, views) |
||||
|
||||
/** Sidebar visible */ |
||||
const drawerOpen = inject('navDrawerOpen', ref(false)) |
||||
|
||||
/** View type to create from modal */ |
||||
let viewCreateType = $ref<ViewTypes>() |
||||
|
||||
/** View title to create from modal (when duplicating) */ |
||||
let viewCreateTitle = $ref('') |
||||
|
||||
/** is view creation modal open */ |
||||
let modalOpen = $ref(false) |
||||
|
||||
/** Watch current views and on change set the next active view */ |
||||
watch( |
||||
views, |
||||
(nextViews) => { |
||||
if (nextViews.length) { |
||||
activeView.value = nextViews[0] |
||||
} |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
|
||||
/** Open view creation modal */ |
||||
function openModal({ type, title = '' }: { type: ViewTypes; title: string }) { |
||||
modalOpen = true |
||||
viewCreateType = type |
||||
viewCreateTitle = title |
||||
} |
||||
|
||||
/** Handle view creation */ |
||||
function onCreate(view: GridType | FormType | KanbanType | GalleryType) { |
||||
views.value.push(view) |
||||
activeView.value = view |
||||
modalOpen = false |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-layout-sider theme="light" class="shadow" :width="drawerOpen ? 0 : 250"> |
||||
<div class="flex flex-col h-full"> |
||||
<MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" /> |
||||
<MenuBottom @open-modal="openModal" /> |
||||
</div> |
||||
|
||||
<dlg-view-create v-if="views" v-model="modalOpen" :title="viewCreateTitle" :type="viewCreateType" @created="onCreate" /> |
||||
</a-layout-sider> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
:deep(.ant-menu-title-content) { |
||||
@apply w-full; |
||||
} |
||||
</style> |
@ -0,0 +1,87 @@
|
||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' |
||||
import { Api } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import type { EventHook, MaybeRef } from '@vueuse/core' |
||||
import { addAxiosInterceptors } from './interceptors' |
||||
import { createEventHook, ref, unref, useNuxtApp } from '#imports' |
||||
import type { NuxtApp } from '#app' |
||||
|
||||
interface UseApiReturn<D = any, R = any> { |
||||
api: Api<any> |
||||
isLoading: Ref<boolean> |
||||
error: Ref<AxiosError<D, R> | null> |
||||
response: Ref<AxiosResponse<D, R> | null> |
||||
onError: EventHook<AxiosError<D, R>>['on'] |
||||
onResponse: EventHook<AxiosResponse<D, R>>['on'] |
||||
} |
||||
|
||||
export function createApiInstance(app: NuxtApp, baseURL = 'http://localhost:8080') { |
||||
const api = new Api({ |
||||
baseURL, |
||||
}) |
||||
|
||||
addAxiosInterceptors(api, app) |
||||
|
||||
return api |
||||
} |
||||
|
||||
/** todo: add props? */ |
||||
interface UseApiProps<D = any> { |
||||
axiosConfig?: MaybeRef<AxiosRequestConfig<D>> |
||||
useGlobalInstance?: MaybeRef<boolean> |
||||
} |
||||
|
||||
export function useApi<Data = any, RequestConfig = any>(props: UseApiProps<Data> = {}): UseApiReturn<Data, RequestConfig> { |
||||
const isLoading = ref(false) |
||||
|
||||
const error = ref(null) |
||||
|
||||
const response = ref<any>(null) |
||||
|
||||
const errorHook = createEventHook<AxiosError<Data, RequestConfig>>() |
||||
|
||||
const responseHook = createEventHook<AxiosResponse<Data, RequestConfig>>() |
||||
|
||||
const api = unref(props.useGlobalInstance) ? useNuxtApp().$api : createApiInstance(useNuxtApp()) |
||||
|
||||
api.instance.interceptors.request.use( |
||||
(config) => { |
||||
error.value = null |
||||
response.value = null |
||||
isLoading.value = true |
||||
|
||||
return { |
||||
...config, |
||||
...unref(props), |
||||
} |
||||
}, |
||||
(requestError) => { |
||||
errorHook.trigger(requestError) |
||||
error.value = requestError |
||||
response.value = null |
||||
isLoading.value = false |
||||
|
||||
return requestError |
||||
}, |
||||
) |
||||
|
||||
api.instance.interceptors.response.use( |
||||
(apiResponse) => { |
||||
responseHook.trigger(apiResponse as AxiosResponse<Data, RequestConfig>) |
||||
// can't properly typecast
|
||||
response.value = apiResponse |
||||
isLoading.value = false |
||||
|
||||
return apiResponse |
||||
}, |
||||
(apiError) => { |
||||
errorHook.trigger(apiError) |
||||
error.value = apiError |
||||
isLoading.value = false |
||||
|
||||
return apiError |
||||
}, |
||||
) |
||||
|
||||
return { api, isLoading, response, error, onError: errorHook.on, onResponse: responseHook.on } |
||||
} |
@ -0,0 +1,78 @@
|
||||
import type { Api } from 'nocodb-sdk' |
||||
import { navigateTo, useRoute, useRouter } from '#imports' |
||||
import type { NuxtApp } from '#app' |
||||
|
||||
const DbNotFoundMsg = 'Database config not found' |
||||
|
||||
export function addAxiosInterceptors(api: Api<any>, app: NuxtApp) { |
||||
const router = useRouter() |
||||
const route = useRoute() |
||||
|
||||
api.instance.interceptors.request.use((config) => { |
||||
config.headers['xc-gui'] = 'true' |
||||
|
||||
if (app.$state.token.value) config.headers['xc-auth'] = app.$state.token.value |
||||
|
||||
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) { |
||||
// config.headers['xc-preview'] = store.state.users.previewAs
|
||||
} |
||||
|
||||
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) { |
||||
if (route && route.params && route.params.shared_base_id) config.headers['xc-shared-base-id'] = route.params.shared_base_id |
||||
} |
||||
|
||||
return config |
||||
}) |
||||
|
||||
// Return a successful response back to the calling service
|
||||
api.instance.interceptors.response.use( |
||||
(response) => response, |
||||
// Handle Error
|
||||
(error) => { |
||||
if (error.response && error.response.data && error.response.data.msg === DbNotFoundMsg) return router.replace('/project/0') |
||||
|
||||
// Return any error which is not due to authentication back to the calling service
|
||||
if (!error.response || error.response.status !== 401) { |
||||
return Promise.reject(error) |
||||
} |
||||
|
||||
// Logout user if token refresh didn't work or user is disabled
|
||||
if (error.config.url === '/auth/refresh-token') { |
||||
app.$state.signOut() |
||||
|
||||
return Promise.reject(error) |
||||
} |
||||
|
||||
// Try request again with new token
|
||||
return api.instance |
||||
.post('/auth/refresh-token', null, { |
||||
withCredentials: true, |
||||
}) |
||||
.then((token) => { |
||||
// New request with new token
|
||||
const config = error.config |
||||
config.headers['xc-auth'] = token.data.token |
||||
app.$state.signIn(token.data.token) |
||||
|
||||
return new Promise((resolve, reject) => { |
||||
api.instance |
||||
.request(config) |
||||
.then((response) => { |
||||
resolve(response) |
||||
}) |
||||
.catch((error) => { |
||||
reject(error) |
||||
}) |
||||
}) |
||||
}) |
||||
.catch(async (error) => { |
||||
app.$state.signOut() |
||||
// todo: handle new user
|
||||
|
||||
navigateTo('/signIn') |
||||
|
||||
return Promise.reject(error) |
||||
}) |
||||
}, |
||||
) |
||||
} |
@ -1,73 +0,0 @@
|
||||
import type { TableType, ViewType } from 'nocodb-sdk' |
||||
import { ViewTypes } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import { useToast } from 'vue-toastification' |
||||
import { useNuxtApp } from '#app' |
||||
// import useMetas from '~/composables/useMetas'
|
||||
|
||||
export default (meta: Ref<TableType>, onViewCreate?: (viewMeta: any) => void) => { |
||||
const view = reactive<{ title: string; type?: ViewTypes }>({ |
||||
title: '', |
||||
}) |
||||
|
||||
const loading = ref(false) |
||||
|
||||
const { $api } = useNuxtApp() |
||||
const toast = useToast() |
||||
// unused
|
||||
// const { metas } = useMetas()
|
||||
|
||||
const createView = async (viewType: ViewTypes, selectedViewId = null) => { |
||||
loading.value = true |
||||
|
||||
try { |
||||
let data |
||||
switch (viewType) { |
||||
case ViewTypes.GRID: |
||||
// todo: update swagger
|
||||
data = await $api.dbView.gridCreate( |
||||
meta?.value?.id as string, |
||||
{ |
||||
title: view?.title, |
||||
copy_from_id: selectedViewId, |
||||
} as any, |
||||
) |
||||
break |
||||
case ViewTypes.GALLERY: |
||||
data = await $api.dbView.galleryCreate( |
||||
meta?.value?.id as string, |
||||
{ |
||||
title: view?.title, |
||||
copy_from_id: selectedViewId, |
||||
} as any, |
||||
) |
||||
break |
||||
case ViewTypes.FORM: |
||||
data = await $api.dbView.formCreate( |
||||
meta?.value?.id as string, |
||||
{ |
||||
title: view?.title, |
||||
copy_from_id: selectedViewId, |
||||
} as any, |
||||
) |
||||
break |
||||
} |
||||
toast.success('View created successfully') |
||||
onViewCreate?.(data) |
||||
} catch (e: any) { |
||||
toast.error(e.message) |
||||
} |
||||
|
||||
loading.value = false |
||||
} |
||||
|
||||
const generateUniqueTitle = (views: ViewType[]) => { |
||||
let c = 1 |
||||
while (views?.some((t) => t.title === `${meta?.value?.title}${c}`)) { |
||||
c++ |
||||
} |
||||
view.title = `${meta?.value?.title}${c}` |
||||
} |
||||
|
||||
return { view, createView, generateUniqueTitle, loading } |
||||
} |
@ -1,15 +1,27 @@
|
||||
import type { FormType, GalleryType, GridType, KanbanType, TableType } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import type { MaybeRef } from '@vueuse/core' |
||||
import type { WatchOptions } from '@vue/runtime-core' |
||||
import { useNuxtApp } from '#app' |
||||
|
||||
export default function (meta: Ref<TableType>) { |
||||
const views = ref<(GridType | FormType | KanbanType | GalleryType)[]>() |
||||
export default function (meta: MaybeRef<TableType | undefined>, watchOptions: WatchOptions = {}) { |
||||
let views = $ref<(GridType | FormType | KanbanType | GalleryType)[]>([]) |
||||
const { $api } = useNuxtApp() |
||||
|
||||
const loadViews = async () => { |
||||
if (meta.value?.id) |
||||
views.value = (await $api.dbView.list(meta.value?.id)).list as (GridType | FormType | KanbanType | GalleryType)[] |
||||
const _meta = unref(meta) |
||||
|
||||
if (_meta && _meta.id) { |
||||
const response = (await $api.dbView.list(_meta.id)).list as any[] |
||||
if (response) { |
||||
views = response.sort((a, b) => a.order - b.order) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return { views, loadViews } |
||||
watch(() => meta, loadViews, { |
||||
immediate: true, |
||||
...watchOptions, |
||||
}) |
||||
|
||||
return { views: $$(views), loadViews } |
||||
} |
||||
|
@ -0,0 +1,3 @@
|
||||
export * from './constants' |
||||
export * from './enums' |
||||
export * from './types' |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
||||
import { Menu as AntMenu } from 'ant-design-vue' |
||||
import { defineNuxtPlugin } from '#imports' |
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => { |
||||
nuxtApp.vueApp.component(AntMenu.name, AntMenu) |
||||
}) |
@ -0,0 +1,22 @@
|
||||
import { adjectives, animals, starWars, uniqueNamesGenerator } from 'unique-names-generator' |
||||
|
||||
export const generateUniqueName = () => { |
||||
return uniqueNamesGenerator({ |
||||
dictionaries: [[starWars], [adjectives, animals]][Math.floor(Math.random() * 2)], |
||||
}) |
||||
.toLowerCase() |
||||
.replace(/[ -]/g, '_') |
||||
} |
||||
|
||||
export const generateUniqueTitle = <T extends Record<string, any> = Record<string, any>>( |
||||
title: string, |
||||
arr: T[], |
||||
predicate: keyof T, |
||||
) => { |
||||
let c = 1 |
||||
while (arr.some((item) => item[predicate] === (`${title}-${c}` as keyof T))) { |
||||
c++ |
||||
} |
||||
|
||||
return `${title}-${c}` |
||||
} |
@ -0,0 +1,11 @@
|
||||
export * from './colorsUtils' |
||||
export * from './dateTimeUtils' |
||||
export * from './durationHelper' |
||||
export * from './errorUtils' |
||||
export * from './fileUtils' |
||||
export * from './filterUtils' |
||||
export * from './generateName' |
||||
export * from './projectCreateUtils' |
||||
export * from './urlUtils' |
||||
export * from './validation' |
||||
export * from './viewUtils' |
Loading…
Reference in new issue