多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

993 lines
30 KiB

<template>
<v-navigation-drawer
permanent
class="views-navigation-drawer"
:style="{
maxWidth: toggleDrawer ? '0' : '220px',
minWidth: toggleDrawer ? '0' : '220px',
}"
>
<v-container fluid class="h-100 py-0">
<div class="d-flex flex-column h-100">
<div class="flex-grow-1" style="overflow: auto; min-height: 350px">
<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-item-group
v-model="selectedViewIdLocal"
mandatory
color="primary"
>
<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, i) in viewsList"
:key="view.id"
v-t="['a:view:open', { view: view.type }]"
dense
:value="view.id"
active-class="x-active--text"
: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>
<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-item-group>
</v-list>
<template
v-if="hideViews && _isUIAllowed('virtualViewsCreateOrEdit')"
>
<v-divider class="advance-menu-divider" />
<v-list
dense
:class="{
'advanced-border': overShieldIcon,
}"
>
<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="{ on }">
<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="{ on }">
<v-list-item
dense
class="body-2 nc-create-grid-view"
v-on="on"
@click="openCreateViewDlg(viewTypes.GRID)"
>
<v-list-item-icon class="mr-n1">
<v-icon color="blue" x-small>
mdi-grid-large
</v-icon>
</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 />
<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="{ on }">
<v-list-item
dense
class="body-2 nc-create-gallery-view"
v-on="on"
@click="openCreateViewDlg(viewTypes.GALLERY)"
>
<v-list-item-icon class="mr-n1">
<v-icon color="orange" x-small>
mdi-camera-image
</v-icon>
</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 />
<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="{ on }">
<v-list-item
v-if="!isView"
dense
class="body-2 nc-create-form-view"
v-on="on"
@click="openCreateViewDlg(viewTypes.FORM)"
>
<v-list-item-icon class="mr-n1">
<v-icon
x-small
:color="viewIcons[viewTypes.FORM].color"
class="mt-n1"
>
mdi-form-select
</v-icon>
</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 />
<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>
</template>
</div>
<div v-if="!isSharedBase">
<v-btn
v-t="['c:snippet:open']"
color="primary"
class="caption d-100"
@click="codeSnippetModal=true"
>
<v-icon small class="mr-2">
mdi-xml
</v-icon> Get API Snippet
</v-btn>
<code-snippet v-model="codeSnippetModal" :query-params="queryParams" :meta="meta" :view="selectedView" />
</div>
<div v-if="_isUIAllowed('webhook')" class="mb-2">
<v-btn
v-t="['c:actions:webhook']"
color="primary"
outlined
class="caption d-100 mt-2"
@click="webhookSliderModal=true"
>
<v-icon small class="mr-2">
mdi-hook
</v-icon> Webhooks
</v-btn>
<webhook-slider v-model="webhookSliderModal" :meta="meta" />
</div>
<div
v-if="!isSharedBase && time - $store.state.settings.miniSponsorCard > 15 * 60 * 1000"
class="py-2 sponsor-wrapper"
>
<v-icon small class="close-icon" @click="hideMiniSponsorCard">
mdi-close-circle-outline
</v-icon>
<!-- <extras />-->
<v-divider class="mb-2" />
<extras class="pl-1" />
<v-btn
v-t="['e:hiring']"
color="primary"
outlined
class="caption d-100 my-2 "
href="https://angel.co/company/nocodb"
target="_blank"
>
🚀 We are Hiring! 🚀
</v-btn>
<!-- <sponsor-mini nav />-->
</div>
</div>
</v-container>
<create-view-dialog
v-if="showCreateView"
v-model="showCreateView"
:nodes="nodes"
:table="table"
:show_as="createViewType"
:views-count="views.length"
:primary-value-column="primaryValueColumn"
:meta="meta"
:copy-view="copyViewRef"
:alias="meta.title"
:views-list="views"
:selected-view-id="selectedViewId"
@created="onViewCreate"
/>
<v-dialog v-model="showShareModel" max-width="650px">
<v-card class="pa-3 backgroundColor">
<v-container @click.stop>
<h3 class="title mb-3">
<!-- This view is shared via a private link -->
{{ $t("msg.info.privateLink") }}
</h3>
<p class="grey&#45;&#45;text body-2">
<!-- People with private link can only see cells visible in this view -->
</p>
<div
style="border-radius: 4px"
class="share-link-box body-2 pa-2 d-flex align-center"
>
{{ sharedViewUrl }}
<v-spacer />
<a
v-t="['c:view:share:open-url']"
:href="`${sharedViewUrl}`"
style="text-decoration: none"
target="_blank"
>
<v-icon small class="mx-2">mdi-open-in-new</v-icon>
</a>
<v-icon small class="pointer" @click="copyShareUrlToClipboard">
mdi-content-copy
</v-icon>
</div>
<v-switch
v-model="passwordProtect"
dense
@change="onPasswordProtectChange"
>
<template #label>
<!-- Restrict access with a password -->
<span v-show="!passwordProtect" class="caption">
{{ $t("msg.info.beforeEnablePwd") }}
</span>
<!-- Access is password restricted -->
<span v-show="passwordProtect" class="caption">
{{ $t("msg.info.afterEnablePwd") }}
</span>
</template>
</v-switch>
<div
v-if="passwordProtect"
class="d-flex flex-column align-center justify-center"
>
<v-text-field
v-model="shareLink.password"
autocomplete="new-password"
browser-autocomplete="new-password"
class="password-field mr-2 caption"
style="max-width: 230px"
:type="showShareLinkPassword ? 'text' : 'password'"
:hint="$t('placeholder.password.enter')"
persistent-hint
dense
solo
flat
>
<template #append>
<v-icon
small
@click="showShareLinkPassword = !showShareLinkPassword"
>
{{ showShareLinkPassword ? "visibility_off" : "visibility" }}
</v-icon>
</template>
</v-text-field>
<v-btn
color="primary"
class="caption"
small
@click="saveShareLinkPassword"
>
<!-- Save password -->
{{ $t("placeholder.password.save") }}
</v-btn>
</div>
</v-container>
</v-card>
</v-dialog>
</v-navigation-drawer>
</template>
<script>
import draggable from 'vuedraggable'
import { ViewTypes } from 'nocodb-sdk'
import CreateViewDialog from '~/components/project/spreadsheet/dialog/CreateViewDialog'
import Extras from '~/components/project/spreadsheet/components/Extras'
import viewIcons from '~/helpers/viewIcons'
import { copyTextToClipboard } from '~/helpers/xutils'
import SponsorMini from '~/components/SponsorMini'
import CodeSnippet from '~/components/project/spreadsheet/components/CodeSnippet'
import WebhookSlider from '~/components/project/tableTabs/webhook/WebhookSlider'
export default {
name: 'SpreadsheetNavDrawer',
components: { WebhookSlider, CodeSnippet, SponsorMini, Extras, CreateViewDialog, draggable },
props: {
extraViewParams: Object,
showAdvanceOptions: Boolean,
isView: Boolean,
hideViews: Boolean,
primaryValueColumn: [Number, String],
toggleDrawer: {
type: Boolean,
default: false
},
nodes: Object,
table: String,
selectedViewId: [String, Number],
selectedView: Object,
meta: [Object, Array],
concatenatedXWhere: String,
sort: String,
showFields: Object,
filters: [Object, Array],
sortList: [Object, Array],
load: {
default: true,
type: Boolean
},
currentApiUrl: String,
fieldsOrder: Array,
viewStatus: Object,
// columnsWidth: Object,
// coverImageField: String,
groupingField: String,
// showSystemFields: Boolean,
views: Array,
queryParams: Object
},
data: () => ({
webhookSliderModal: false,
codeSnippetModal: false,
drag: false,
dragOptions: {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost'
},
time: Date.now(),
sponsorMiniVisible: true,
enableDummyFeat: false,
searchQueryVal: '',
showShareLinkPassword: false,
passwordProtect: false,
sharedViewPassword: '',
overAdvShieldIcon: false,
overShieldIcon: false,
viewIcons,
copyViewRef: null,
shareLink: {},
showShareModel: false,
showCreateView: false,
loading: false,
viewTypeAlias: {
[ViewTypes.GRID]: 'grid',
[ViewTypes.FORM]: 'form',
[ViewTypes.GALLERY]: 'gallery'
}
}),
computed: {
isSharedBase() {
return this.$route.params && this.$route.params.shared_base_id
},
viewsList: {
set(v) {
this.$emit('update:views', v)
},
get() {
return this.views
}
},
viewTypes() {
return ViewTypes
},
newViewParams() {
if (!this.showFields) {
return {}
}
const showFields = { ...this.showFields }
Object.keys(showFields).forEach((k) => {
showFields[k] = true
})
return { showFields }
},
selectedViewIdLocal: {
set(val) {
const view = (this.views || []).find(v => v.id === val)
this.$router.push({
query: {
...this.$route.query,
view: view && view.id
}
})
},
get() {
let id
if (this.views) {
const view = this.views.find(v => v.id === this.$route.query.view)
id = (view && view.id) || ((this.views && this.views[0]) || {}).id
}
return id
}
},
sharedViewUrl() {
let viewType
switch (this.shareLink.type) {
case this.viewTypes.FORM:
viewType = 'form'
break
case this.viewTypes.KANBAN:
viewType = 'kanban'
break
default:
viewType = 'view'
}
return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.uuid}`
}
},
watch: {
async load(v) {
if (v) {
await this.loadViews()
this.onViewIdChange(this.selectedViewIdLocal)
}
},
selectedViewIdLocal(id) {
this.onViewIdChange(id)
}
},
async created() {
if (this.load) {
await this.loadViews()
}
this.onViewIdChange(this.selectedViewIdLocal)
},
methods: {
async onMove(event) {
if (this.viewsList.length - 1 === event.moved.newIndex) {
this.$set(
this.viewsList[event.moved.newIndex],
'order',
this.viewsList[event.moved.newIndex - 1].order + 1
)
} else if (event.moved.newIndex === 0) {
this.$set(
this.viewsList[event.moved.newIndex],
'order',
this.viewsList[1].order / 2
)
} else {
this.$set(
this.viewsList[event.moved.newIndex],
'order',
(this.viewsList[event.moved.newIndex - 1].order +
this.viewsList[event.moved.newIndex + 1].order) /
2
)
}
await this.$api.dbView.update(this.viewsList[event.moved.newIndex].id, {
title: this.viewsList[event.moved.newIndex].title,
order: this.viewsList[event.moved.newIndex].order
})
this.$e('a:view:reorder')
},
onViewIdChange(id) {
const selectedView = this.views && this.views.find(v => v.id === id)
// const queryParams = {}
this.$emit('update:selectedViewId', id)
this.$emit('update:selectedView', selectedView)
// if (selectedView.type === 'table') {
// return;
// }
// try {
// queryParams = JSON.parse(selectedView.query_params) || {}
// } catch (e) {
// // console.log(e)
// }
// this.$emit('update:filters', queryParams.filters || [])
// this.$emit('update:sortList', queryParams.sortList || [])
// this.$emit('update:fieldsOrder', queryParams.fieldsOrder || [])
// this.$emit('update:viewStatus', queryParams.viewStatus || {})
// this.$emit('update:columnsWidth', queryParams.columnsWidth || {})
// this.$emit('update:extraViewParams', queryParams.extraViewParams || {})
// this.$emit('update:coverImageField', queryParams.coverImageField)
// this.$emit('update:groupingField', queryParams.groupingField)
// this.$emit('update:showSystemFields', queryParams.showSystemFields)
// if (queryParams.showFields) {
// this.$emit('update:showFields', queryParams.showFields)
// } else {
// this.$emit('mapFieldsAndShowFields')
// }
this.$emit('loadTableData')
},
hideMiniSponsorCard() {
this.$store.commit('settings/MutMiniSponsorCard', Date.now())
},
openCreateViewDlg(type) {
const mainView = this.viewsList.find(
v => v.type === 'table' || v.type === 'view'
)
try {
this.copyViewRef = this.copyViewRef || {
query_params: JSON.stringify({
...this.newViewParams,
fieldsOrder: JSON.parse(mainView.query_params).fieldsOrder
})
}
} catch {}
this.createViewType = type
this.showCreateView = true
this.$e('c:view:create', { view: type })
},
isCentrallyAligned(col) {
return ![
'SingleLineText',
'LongText',
'Attachment',
'Date',
'Time',
'Email',
'URL',
'DateTime',
'CreateTime',
'LastModifiedTime',
'Currency'
].includes(col.uidt)
},
onPasswordProtectChange() {
if (!this.passwordProtect) {
this.shareLink.password = null
this.saveShareLinkPassword()
}
},
async saveShareLinkPassword() {
try {
await this.$api.dbViewShare.update(this.shareLink.id, {
password: this.shareLink.password
})
// await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: this.nodes.dbAlias },
// 'updateSharedViewLinkPassword',
// {
// id: this.shareLink.id,
// password: this.shareLink.password
// }
// ])
this.$toast.success('Successfully updated').goAway(3000)
} catch (e) {
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000)
}
this.$e('a:view:share:enable-pwd')
},
async loadViews() {
// this.viewsList = await this.sqlOp(
// {
// dbAlias: this.nodes.dbAlias
// },
// 'xcVirtualTableList',
// {
// tn: this.table
// }
// )
// this.selectedViewIdLocal = this.viewsList && this.viewsList[0] && this.viewsList[0].id
// this.viewsList = []
const views = (await this.$api.dbView.list(this.meta.id)).list
this.$emit('update:views', views)
},
// async onViewChange() {
// let query_params = {}
// try {
// console.log(this.selectedView)
// query_params = JSON.parse(this.selectedView.query_params);
// } catch (e) {
// console.log(e)
// }
// this.$emit('update:filters', query_params.filters || []);
// this.$emit('update:sortList', query_params.sortList || []);
// if (query_params.showFields) {
// this.$emit('update:showFields', query_params.showFields);
// } else {
// this.$emit('mapFieldsAndShowFields');
// }
// this.$emit('loadTableData');
// },
copyapiUrlToClipboard() {
copyTextToClipboard(this.currentApiUrl)
this.clipboardSuccessHandler()
},
async updateViewName(view, index) {
if (!view.edit) {
return
}
// const oldTitle = view.title
this.$set(view, 'edit', false)
if (view.title_temp === view.title) {
return
}
if (
this.viewsList.some(
(v, i) => i !== index && (v.alias || v.title) === view.title_temp
)
) {
this.$toast.info('View name should be unique').goAway(3000)
return
}
try {
// if (this.selectedViewIdLocal === view.id) {
// await this.$router.push({
// query: {
// ...this.$route.query,
// view: view.title_temp
// }
// })
// }
this.$set(view, 'title', view.title_temp)
await this.$api.dbView.update(view.id, {
title: view.title,
order: view.order
})
this.$toast.success('View renamed successfully').goAway(3000)
} catch (e) {
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000)
}
},
showRenameTextBox(view, i) {
this.$set(view, 'edit', true)
this.$set(view, 'title_temp', view.title)
this.$nextTick(() => {
const input = this.$refs[`input${i}`][0]
input.focus()
input.setSelectionRange(0, input.value.length)
})
this.$e('c:view:rename', { view: view.type })
},
async deleteView(view) {
try {
await this.$api.dbView.delete(view.id)
this.$toast.success('View deleted successfully').goAway(3000)
await this.loadViews()
} catch (e) {
this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000)
}
this.$e('a:view:delete', { view: view.type })
},
async genShareLink() {
// const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: this.nodes.dbAlias },
// 'createSharedViewLink',
// {
// model_name: this.table,
// // meta: this.meta,
// query_params: {
// where: this.concatenatedXWhere,
// sort: this.sort,
// fields: Object.keys(this.showFields)
// .filter(f => this.showFields[f])
// .join(','),
// showFields: this.showFields,
// fieldsOrder: this.fieldsOrder,
// extraViewParams: this.extraViewParams,
// selectedViewId: this.selectedViewId,
// columnsWidth: this.columnsWidth
// },
// view_name: this.selectedView.title,
// type: this.selectedView.type,
// show_as: this.selectedView.show_as,
// password: this.sharedViewPassword
// }
// ])
const shared = await this.$api.dbViewShare.create(this.selectedViewId)
// todo: url
this.shareLink = shared
this.showShareModel = true
},
copyView(view, i) {
this.createViewType = view.type
this.showCreateView = true
this.copyViewRef = view
this.$e('c:view:copy', { view: view.type })
},
async onViewCreate(viewMeta) {
this.copyViewRef = null
await this.loadViews()
this.selectedViewIdLocal = viewMeta.id
// await this.onViewChange();
this.$e('a:view:create', { view: viewMeta.type })
},
clipboard(str) {
const el = document.createElement('textarea')
el.addEventListener('focusin', e => e.stopPropagation())
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
},
clipboardSuccessHandler() {
this.$toast.info('Copied to clipboard').goAway(1000)
},
copyShareUrlToClipboard() {
this.clipboard(this.sharedViewUrl)
this.clipboardSuccessHandler()
this.$e('c:view:share:copy-url')
}
}
}
</script>
<style scoped lang="scss">
::v-deep {
.v-list-item--dense .v-list-item__icon,
.v-list--dense .v-list-item .v-list-item__icon {
margin-top: 4px;
margin-bottom: 4px;
}
.v-list-item--dense,
.v-list--dense .v-list-item {
min-height: 32px;
}
.advance-menu-divider {
width: calc(100% - 26px);
margin-left: 13px;
border-style: dashed;
margin-top: 5px;
margin-bottom: 5px;
}
.view .view-icon {
display: none;
transition: 0.3s display;
}
.view:hover .view-icon {
display: inline-block;
}
.view:hover .check-icon {
display: none;
}
.password-field.v-text-field.v-text-field--solo {
.v-text-field__details {
padding: 0 2px !important;
.v-messages__message {
color: grey;
font-size: 0.65rem;
}
}
.v-input__control {
min-height: 28px !important;
}
}
}
.share-link-box {
position: relative;
z-index: 2;
}
.share-link-box::before {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: var(--v-primary-base);
opacity: 0.2;
content: "";
z-index: 1;
pointer-events: none;
}
.views-navigation-drawer {
border-left: 1px solid #80808033;
}
.sponsor-wrapper {
position: relative;
.close-icon {
position: absolute;
right: 10px;
top: 10px;
z-index: 9;
opacity: 0;
transition: 0.4s opacity;
}
&:hover .close-icon {
opacity: 1;
}
}
.nc-draggable-child .nc-child-draggable-icon {
opacity: 0;
transition: 0.3s opacity;
position: absolute;
left: 0;
}
.nc-draggable-child:hover .nc-child-draggable-icon {
opacity: 1;
}
.nc-draggable-child:hover .nc-child-draggable-icon {
opacity: 1;
}
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.ghost {
opacity: 0.5;
background: grey;
}
</style>