mirror of https://github.com/nocodb/nocodb
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.
1780 lines
57 KiB
1780 lines
57 KiB
<template> |
|
<v-container class="h-100 j-excel-container backgroundColor pa-0 ma-0" fluid> |
|
<v-toolbar |
|
height="32" |
|
dense |
|
class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom mx-1" |
|
style="z-index: 7" |
|
> |
|
<div v-if="!isForm" class="d-flex xc-border align-center search-box" style="min-width: 156px"> |
|
<v-menu bottom offset-y> |
|
<template #activator="{ on }"> |
|
<div style="min-width: 56px" v-on="on"> |
|
<v-icon class="ml-2" small color="grey"> mdi-magnify </v-icon> |
|
|
|
<v-icon color="grey" class="pl-0 pa-1" small> mdi-menu-down </v-icon> |
|
</div> |
|
</template> |
|
<v-list dense> |
|
<v-list-item v-for="col in availableRealColumns" :key="col.column_name" @click="searchField = col.title"> |
|
<v-icon color="grey darken-4" small class="mr-1"> |
|
{{ col.icon }} |
|
</v-icon> |
|
<span class="caption">{{ col.title }}</span> |
|
</v-list-item> |
|
</v-list> |
|
</v-menu> |
|
|
|
<v-divider vertical /> |
|
|
|
<v-text-field |
|
v-model="searchQueryVal" |
|
autocomplete="new-password" |
|
style="min-width: 100px; width: 120px" |
|
flat |
|
dense |
|
solo |
|
hide-details |
|
:placeholder="searchField ? $t('placeholder.searchColumn', { searchField }) : 'Search all columns'" |
|
class="elevation-0 pa-0 flex-grow-1 caption search-field" |
|
@keyup.enter="searchQuery = searchQueryVal" |
|
@blur="searchQuery = searchQueryVal" |
|
/> |
|
</div> |
|
|
|
<span v-if="relationType && false" class="caption grey--text" |
|
>{{ refTable }}({{ relationPrimaryValue }}) -> {{ relationType === 'hm' ? ' Has Many ' : ' Belongs To ' }} -> |
|
{{ table }}</span |
|
> |
|
<div class="d-inline-flex"> |
|
<div> |
|
<fields |
|
v-if="!isForm" |
|
ref="fields" |
|
v-model="showFields" |
|
:field-list="fieldList" |
|
:meta="meta" |
|
:is-locked="isLocked" |
|
:fields-order.sync="fieldsOrder" |
|
:sql-ui="sqlUi" |
|
:show-system-fields.sync="showSystemFields" |
|
:cover-image-field.sync="coverImageField" |
|
:grouping-field.sync="groupingField" |
|
:is-gallery="isGallery" |
|
:is-kanban="isKanban" |
|
:view-id="selectedViewId" |
|
@updated="loadTableData" |
|
/> |
|
<column-filter |
|
v-if="!isForm" |
|
v-model="filters" |
|
:meta="meta" |
|
:is-locked="isLocked" |
|
:field-list="[...realFieldList, ...formulaFieldList]" |
|
dense |
|
:view-id="selectedViewId" |
|
@updated="loadTableData(false)" |
|
/> |
|
<sort-list |
|
v-if="!isForm" |
|
v-model="sortList" |
|
:is-locked="isLocked" |
|
:meta="meta" |
|
:view-id="selectedViewId" |
|
@updated="loadTableData(false)" |
|
/> |
|
</div> |
|
<share-view-menu v-if="!isGallery" @share="$refs.drawer && $refs.drawer.genShareLink()" /> |
|
|
|
<MoreActions |
|
v-if="!isForm" |
|
ref="csvExportImport" |
|
:meta="meta" |
|
:nodes="nodes" |
|
:query-params="{ |
|
fieldsOrder, |
|
fieldFilter, |
|
sortList, |
|
showFields, |
|
}" |
|
:selected-view="selectedView" |
|
:is-view="isView" |
|
@showAdditionalFeatOverlay="showAdditionalFeatOverlay($event)" |
|
@webhook="showAdditionalFeatOverlay('webhooks')" |
|
@reload="reload" |
|
/> |
|
</div> |
|
<v-spacer |
|
class="h-100" |
|
@click=" |
|
clickCount = clickCount + 1; |
|
debug = clickCount >= 4; |
|
" |
|
/> |
|
|
|
<template v-if="!isForm"> |
|
<!-- Export Cache --> |
|
<v-tooltip v-if="debug" bottom> |
|
<template #activator="{ on }"> |
|
<v-icon class="mr-3" small v-on="on" @click="exportCache"> mdi-export </v-icon> |
|
</template> |
|
<span class="caption"> Export Cache </span> |
|
</v-tooltip> |
|
<!-- Delete Cache --> |
|
<v-tooltip v-if="debug" bottom> |
|
<template #activator="{ on }"> |
|
<v-icon class="mr-3" small v-on="on" @click="deleteCache"> mdi-delete </v-icon> |
|
</template> |
|
<span class="caption"> Delete Cache </span> |
|
</v-tooltip> |
|
<debug-metas v-if="debug" class="mr-3" /> |
|
<v-tooltip bottom> |
|
<template #activator="{ on }"> |
|
<v-icon v-if="!isPkAvail && !isForm" color="warning" small class="mr-3" v-on="on"> |
|
mdi-information-outline |
|
</v-icon> |
|
</template> |
|
<span class="caption"> Update & Delete not allowed since the table doesn't have any primary key </span> |
|
</v-tooltip> |
|
<lock-menu v-if="_isUIAllowed('view-type')" v-model="lockType" /> |
|
|
|
<!-- <x-btn |
|
tooltip="Reload view data" |
|
outlined |
|
small |
|
text |
|
btn.class="nc-table-reload-btn px-0" |
|
@click="reload" |
|
>--> |
|
<v-icon small class="mx-n1" color="grey lighten-1"> mdi-circle-small </v-icon> |
|
<!-- tooltip="Reload view data" --> |
|
<x-icon :tooltip="$t('general.reload')" icon.class="nc-table-reload-btn mx-1" small @click="reloadClick"> |
|
mdi-reload |
|
</x-icon> |
|
<v-icon v-if="isEditable && relationType !== 'bt'" small class="mx-n1" color="grey lighten-1"> |
|
mdi-circle-small |
|
</v-icon> |
|
<!-- </x-btn>--> |
|
<!-- <x-btn--> |
|
<!-- v-if="isEditable && relationType !== 'bt'"--> |
|
<!-- tooltip="Add new row"--> |
|
<!-- :disabled="isLocked"--> |
|
<!-- outlined--> |
|
<!-- small--> |
|
<!-- text--> |
|
<!-- btn.class="nc-add-new-row-btn"--> |
|
<!-- @click="insertNewRow(true,true)"--> |
|
<!-- >--> |
|
|
|
<!-- tooltip="Add new row"--> |
|
<x-icon |
|
v-if="!isView && isEditable && relationType !== 'bt'" |
|
icon.class="nc-add-new-row-btn mx-1" |
|
:tooltip="$t('activity.addRow')" |
|
:disabled="isLocked" |
|
small |
|
:color="['success', '']" |
|
@click="clickAddNewIcon" |
|
> |
|
mdi-plus-outline |
|
</x-icon> |
|
<!-- </x-btn>--> |
|
<!-- <x-btn |
|
small |
|
text |
|
btn.class="nc-save-new-row-btn" |
|
outlined |
|
tooltip="Save new rows" |
|
:disabled="!edited || isLocked" |
|
@click="save" |
|
> |
|
<v-icon small class="mr-1" color="grey darken-3"> |
|
save |
|
</v-icon> |
|
Save |
|
</x-btn>--> |
|
<!-- <v-tooltip--> |
|
<!-- bottom--> |
|
<!-- >--> |
|
<!-- <template #activator="{on}">--> |
|
<!-- <v-btn |
|
v-show="_isUIAllowed('table-delete')" |
|
class="nc-table-delete-btn" |
|
:disabled="isLocked" |
|
small |
|
outlined |
|
text |
|
v-on="on" |
|
@click="checkAndDeleteTable" |
|
>--> |
|
<v-icon v-if="_isUIAllowed('table-delete')" small class="mx-n1" color="grey lighten-1"> |
|
mdi-circle-small |
|
</v-icon> |
|
<x-icon |
|
v-if="_isUIAllowed('table-delete')" |
|
icon.class="nc-table-delete-btn mx-1 mr-1" |
|
:disabled="isLocked" |
|
small |
|
:color="['red', '']" |
|
:tooltip="$t('activity.deleteTable')" |
|
@click="checkAndDeleteTable" |
|
> |
|
mdi-delete-outline |
|
</x-icon> |
|
|
|
<v-icon small class="ml-n2" color="grey lighten-1"> mdi-circle-small </v-icon> |
|
<!-- </v-btn>--> |
|
<!-- </template>--> |
|
<!-- <span class="">Delete table</span>--> |
|
<!-- </v-tooltip>--> |
|
</template> |
|
|
|
<!-- Cell height --> |
|
<!-- <v-menu> |
|
<template v-slot:activator="{ on, attrs }"> |
|
<v-icon |
|
v-bind="attrs" |
|
v-on="on" small |
|
class="mx-2" |
|
color="grey darken-3" |
|
> |
|
mdi-arrow-collapse-vertical |
|
</v-icon> |
|
</template> |
|
|
|
<v-list dense class="caption"> |
|
|
|
<v-list-item v-for="h in cellHeights" dense @click.stop="cellHeight = h.size" :key="h.size"> |
|
<v-list-item-icon class="mr-1"> |
|
<v-icon small :color="cellHeight === h.size && 'primary'">{{ h.icon }}</v-icon> |
|
</v-list-item-icon> |
|
<v-list-item-title :class="{'primary--text' : cellHeight === h.size}" style="text-transform: capitalize"> |
|
{{ h.size }} |
|
</v-list-item-title> |
|
</v-list-item> |
|
</v-list> |
|
</v-menu>--> |
|
|
|
<!--tooltip="Toggle navigation drawer"--> |
|
<x-btn |
|
:tooltip="$t('tooltip.toggleNavDraw')" |
|
outlined |
|
small |
|
text |
|
:btn-class="{ 'primary lighten-5 nc-toggle-nav-drawer': !toggleDrawer }" |
|
@click=" |
|
toggleDrawer = !toggleDrawer; |
|
toggleClick(); |
|
" |
|
> |
|
<v-icon small class="mx-0" color="grey darken-3"> |
|
{{ toggleDrawer ? 'mdi-door-closed' : 'mdi-door-open' }} |
|
</v-icon> |
|
</x-btn> |
|
</v-toolbar> |
|
<div |
|
:class="`cell-height-${cellHeight}`" |
|
style="height: calc(100% - 32px); overflow: auto; transition: width 100ms" |
|
class="d-flex" |
|
> |
|
<div class="flex-grow-1 h-100" style="overflow-y: auto"> |
|
<div ref="table" :style="{ height: isForm ? '100%' : 'calc(100% - 36px)' }" style="overflow: auto; width: 100%"> |
|
<div |
|
v-if="loadingData && (isGallery || isGrid)" |
|
class="d-100 h-100 align-center justify-center d-flex flex-column" |
|
> |
|
<v-progress-circular size="40" color="grey" width="2" indeterminate class="mb-4" /> |
|
<span v-if="selectedView" class="caption grey--text">Loading view data... </span> |
|
</div> |
|
|
|
<template v-else-if="selectedViewId && selectedView"> |
|
<!-- <v-skeleton-loader v-if="!dataLoaded && loadingData || !meta" type="table" />--> |
|
<template v-if="selectedView.type === viewTypes.GRID"> |
|
<xc-grid-view |
|
ref="ncgridview" |
|
:loading="loadingData" |
|
:is-view="isView" |
|
droppable |
|
:relation-type="relationType" |
|
:columns-width.sync="columnsWidth" |
|
:is-locked="isLocked" |
|
:table="table" |
|
:available-columns="availableColumns" |
|
:show-fields="showFields" |
|
:sql-ui="sqlUi" |
|
:is-editable="isEditable" |
|
:nodes="nodes" |
|
:primary-value-column="primaryValueColumn" |
|
:belongs-to="belongsTo" |
|
:has-many="hasMany" |
|
:data="data" |
|
:visible-col-length="visibleColLength" |
|
:meta="meta" |
|
:is-virtual="selectedView && selectedView.type === 'vtable'" |
|
:api="api" |
|
:is-pk-avail="isPkAvail" |
|
:view-id="selectedViewId" |
|
@drop="onFileDrop" |
|
@onNewColCreation="onNewColCreation" |
|
@colDelete="onColDelete" |
|
@onCellValueChange="onCellValueChange" |
|
@insertNewRow="insertNewRow" |
|
@showRowContextMenu="showRowContextMenu" |
|
@expandRow="expandRow" |
|
@onRelationDelete="loadMeta" |
|
@loadTableData="loadTableData" |
|
@loadMeta="loadMeta" |
|
/> |
|
</template> |
|
<template v-else-if="selectedView.type === viewTypes.GALLERY"> |
|
<gallery-view |
|
:is-locked="isLocked" |
|
:nodes="nodes" |
|
:table="table" |
|
:show-fields="showFields" |
|
:available-columns="availableColumns" |
|
:meta="meta" |
|
:data="data" |
|
:sql-ui="sqlUi" |
|
:view-id="selectedViewId" |
|
:primary-value-column="primaryValueColumn" |
|
:cover-image-field.sync="coverImageField" |
|
@expandForm="({ rowIndex, rowMeta }) => expandRow(rowIndex, rowMeta)" |
|
/> |
|
</template> |
|
<template v-else-if="isKanban"> |
|
<v-container v-if="kanban.loadingData" fluid> |
|
<v-row> |
|
<v-col v-for="idx in 5" :key="idx"> |
|
<v-skeleton-loader type="image@3" /> |
|
</v-col> |
|
</v-row> |
|
</v-container> |
|
<kanban-view |
|
v-if="!kanban.loadingData && kanban.data.length" |
|
:nodes="nodes" |
|
:table="table" |
|
:show-fields="showFields" |
|
:available-columns="availableColumns" |
|
:meta="meta" |
|
:kanban="kanban" |
|
:sql-ui="sqlUi" |
|
:primary-value-column="primaryValueColumn" |
|
:grouping-field.sync="groupingField" |
|
:api="api" |
|
@expandKanbanForm="({ rowIdx }) => expandKanbanForm(rowIdx)" |
|
@insertNewRow="insertNewRow" |
|
@loadMoreKanbanData="groupingFieldVal => loadMoreKanbanData(groupingFieldVal)" |
|
/> |
|
</template> |
|
<template v-else-if="selectedView && selectedView.show_as === 'calendar'"> |
|
<calendar-view |
|
:nodes="nodes" |
|
:table="table" |
|
:show-fields="showFields" |
|
:available-columns="availableColumns" |
|
:meta="meta" |
|
:data="data" |
|
:primary-value-column="primaryValueColumn" |
|
@expandForm="({ rowIndex, rowMeta }) => expandRow(rowIndex, rowMeta)" |
|
/> |
|
</template> |
|
<template v-else-if="selectedView.type === viewTypes.FORM"> |
|
<form-view |
|
:id="selectedViewId" |
|
ref="formView" |
|
:key="selectedViewId + viewKey" |
|
:view-id="selectedViewId" |
|
:nodes="nodes" |
|
:table="table" |
|
:available-columns="availableColumns" |
|
:meta="meta" |
|
:data="data" |
|
:show-fields.sync="showFields" |
|
:all-columns="allColumns" |
|
:field-list="fieldList" |
|
:is-locked="isLocked" |
|
:db-alias="nodes.dbAlias" |
|
:api="api" |
|
:sql-ui="sqlUi" |
|
:fields-order.sync="fieldsOrder" |
|
:primary-value-column="primaryValueColumn" |
|
:form-params.sync="extraViewParams.formParams" |
|
:view.sync="selectedView" |
|
:view-title="selectedView.title" |
|
@onNewColCreation="loadMeta(false)" |
|
/> |
|
</template> |
|
</template> |
|
</div> |
|
<template v-if="data && (isGrid || isGallery)"> |
|
<pagination v-model="page" :count="count" :size="size" @input="clickPagination" /> |
|
</template> |
|
</div> |
|
<spreadsheet-nav-drawer |
|
v-if="meta" |
|
ref="drawer" |
|
:query-params="listQueryParams" |
|
:is-view="isView" |
|
:current-api-url="currentApiUrl" |
|
:toggle-drawer="toggleDrawer" |
|
:nodes="nodes" |
|
:table="table" |
|
:meta="meta" |
|
:selected-view-id.sync="selectedViewId" |
|
:cover-image-field.sync="coverImageField" |
|
:grouping-field.sync="groupingField" |
|
:selected-view.sync="selectedView" |
|
:primary-value-column="primaryValueColumn" |
|
:concatenated-x-where="concatenatedXWhere" |
|
:sort="sort" |
|
:filters.sync="filters" |
|
:sort-list.sync="sortList" |
|
:show-fields.sync="showFields" |
|
:load="loadViews" |
|
:hide-views="!relation" |
|
:show-advance-options.sync="showAdvanceOptions" |
|
:fields-order.sync="fieldsOrder" |
|
:view-status.sync="viewStatus" |
|
:columns-width.sync="columnsWidth" |
|
:show-system-fields.sync="showSystemFields" |
|
:extra-view-params.sync="extraViewParams" |
|
:views.sync="meta.views" |
|
@rerender="viewKey++" |
|
@generateNewViewKey="generateNewViewKey" |
|
@mapFieldsAndShowFields="mapFieldsAndShowFields" |
|
@loadTableData="loadTableData(false)" |
|
@showAdditionalFeatOverlay="showAdditionalFeatOverlay($event)" |
|
> |
|
<!-- <v-tooltip bottom> |
|
<template #activator="{on}"> |
|
<v-list-item |
|
v-on="on" |
|
@click="showAdditionalFeatOverlay('webhooks')" |
|
> |
|
<v-icon x-small class="mr-2 nc-automations"> |
|
mdi-hook |
|
</v-icon> |
|
<span class="caption"> Automations</span> |
|
</v-list-item> |
|
</template> |
|
Create Automations or API Webhooks |
|
</v-tooltip>--> |
|
<!-- <v-tooltip bottom> |
|
<template #activator="{on}"> |
|
<v-list-item |
|
v-on="on" |
|
@click="showAdditionalFeatOverlay('acl')" |
|
> |
|
<v-icon x-small class="mr-2"> |
|
mdi-shield-edit-outline |
|
</v-icon> |
|
<span class="caption"> API ACL</span> |
|
</v-list-item> |
|
</template> |
|
Create / Edit API Webhooks |
|
</v-tooltip>--> |
|
<v-list-item v-if="showAdvanceOptions" @click="showAdditionalFeatOverlay('validators')"> |
|
<v-icon x-small class="mr-2"> mdi-sticker-check-outline </v-icon> |
|
<span class="caption"> API Validators</span> |
|
</v-list-item> |
|
<v-divider v-if="showAdvanceOptions" class="advance-menu-divider" /> |
|
|
|
<v-list-item v-if="showAdvanceOptions" @click="showAdditionalFeatOverlay('columns')"> |
|
<v-icon x-small class="mr-2"> mdi-view-column </v-icon> |
|
<span class="caption font-weight-light">SQL Columns</span> |
|
</v-list-item> |
|
<v-list-item v-if="showAdvanceOptions" @click="showAdditionalFeatOverlay('indexes')"> |
|
<v-icon x-small class="mr-2"> mdi-blur </v-icon> |
|
<span class="caption font-weight-light">SQL Indexes</span> |
|
</v-list-item> |
|
<v-list-item v-if="showAdvanceOptions" @click="showAdditionalFeatOverlay('triggers')"> |
|
<v-icon x-small class="mr-2"> mdi-shield-edit-outline </v-icon> |
|
<span class="caption font-weight-light">SQL Triggers</span> |
|
</v-list-item> |
|
</spreadsheet-nav-drawer> |
|
</div> |
|
|
|
<v-menu |
|
v-if="rowContextMenu" |
|
value="rowContextMenu" |
|
:position-x="rowContextMenu.x" |
|
:position-y="rowContextMenu.y" |
|
absolute |
|
offset-y |
|
> |
|
<v-list dense> |
|
<template v-if="isEditable && !isLocked"> |
|
<v-list-item v-if="relationType !== 'bt'" v-t="['a:row:insert']" @click="insertNewRow(false)"> |
|
<span class="caption"> |
|
<!-- Insert New Row --> |
|
{{ $t('activity.insertRow') }} |
|
</span> |
|
</v-list-item> |
|
<v-list-item v-t="['a:row:delete']" @click="deleteRow"> |
|
<span class="caption"> |
|
<!-- Delete Row --> |
|
{{ $t('activity.deleteRow') }} |
|
</span> |
|
</v-list-item> |
|
<v-list-item v-t="['a:row:delete-bulk']" @click="deleteSelectedRows"> |
|
<span class="caption"> |
|
<!-- Delete Selected Rows --> |
|
{{ $t('activity.deleteSelectedRow') }} |
|
</span> |
|
</v-list-item> |
|
</template> |
|
|
|
<template |
|
v-if=" |
|
isEditable && |
|
!isLocked && |
|
rowContextMenu.col && |
|
!rowContextMenu.col.rqd && |
|
!rowContextMenu.col.virtual && |
|
rowContextMenu.col.uidt !== 'Formula' |
|
" |
|
> |
|
<v-tooltip bottom> |
|
<template #activator="{ on }"> |
|
<v-list-item v-t="['a:row:clear']" v-on="on" @click="clearCellValue"> |
|
<span class="caption">Clear</span> |
|
</v-list-item> |
|
</template> |
|
|
|
<span class="caption">Set column value to <strong>null</strong></span> |
|
</v-tooltip> |
|
</template> |
|
</v-list> |
|
</v-menu> |
|
<v-dialog |
|
v-if="data" |
|
v-model="showExpandModal" |
|
:overlay-opacity="0.8" |
|
width="1000px" |
|
max-width="100%" |
|
class="mx-auto" |
|
transition="dialog-bottom-transition" |
|
> |
|
<expanded-form |
|
v-if="isKanban && kanban.selectedExpandRow" |
|
:key="kanban.selectedExpandRow.id" |
|
v-model="kanban.selectedExpandRow" |
|
:db-alias="nodes.dbAlias" |
|
:has-many="hasMany" |
|
:belongs-to="belongsTo" |
|
:table="table" |
|
:old-row.sync="kanban.selectedExpandOldRow" |
|
:is-new="kanban.selectedExpandRowMeta.new" |
|
:selected-row-meta="kanban.selectedExpandRowMeta" |
|
:meta="meta" |
|
:sql-ui="sqlUi" |
|
:primary-value-column="primaryValueColumn" |
|
:api="api" |
|
:available-columns="availableColumns" |
|
:show-fields="showFields" |
|
:nodes="nodes" |
|
:query-params="queryParams" |
|
:show-next-prev="false" |
|
:preset-values="presetValues" |
|
:is-locked="isLocked" |
|
@cancel="showExpandModal = false" |
|
@input=" |
|
showExpandModal = false; |
|
kanban.selectedExpandRow && kanban.selectedExpandRow.rowMeta && delete kanban.selectedExpandRow.rowMeta.new; |
|
loadKanbanData(false); |
|
" |
|
@commented="reloadComments" |
|
@next="loadNext" |
|
@prev="loadPrev" |
|
/> |
|
<expanded-form |
|
v-if="!isKanban && selectedExpandRowIndex != null && data[selectedExpandRowIndex]" |
|
:key="selectedExpandRowIndex" |
|
v-model="data[selectedExpandRowIndex].row" |
|
:db-alias="nodes.dbAlias" |
|
:has-many="hasMany" |
|
:belongs-to="belongsTo" |
|
:show-fields="showFields" |
|
:table="table" |
|
:old-row.sync="data[selectedExpandRowIndex].oldRow" |
|
:is-new="data[selectedExpandRowIndex].rowMeta.new" |
|
:selected-row-meta="selectedExpandRowMeta" |
|
:meta="meta" |
|
:sql-ui="sqlUi" |
|
:primary-value-column="primaryValueColumn" |
|
:api="api" |
|
:available-columns="availableColumns" |
|
:nodes="nodes" |
|
:query-params="queryParams" |
|
:show-next-prev="true" |
|
:preset-values="presetValues" |
|
:is-locked="isLocked" |
|
@cancel="showExpandModal = false" |
|
@input=" |
|
showExpandModal = false; |
|
data[selectedExpandRowIndex] && |
|
data[selectedExpandRowIndex].rowMeta && |
|
delete data[selectedExpandRowIndex].rowMeta.new; |
|
loadTableData(); |
|
" |
|
@commented="reloadComments" |
|
@loadTableData="loadTableData" |
|
@next="loadNext" |
|
@prev="loadPrev" |
|
/> |
|
</v-dialog> |
|
<additional-features |
|
v-model="showAddFeatOverlay" |
|
:selected-view="selectedView" |
|
:delete-table="deleteTable" |
|
:nodes="nodes" |
|
:type="featureType" |
|
:table="table" |
|
:meta="meta" |
|
/> |
|
</v-container> |
|
</template> |
|
|
|
<script> |
|
import { mapActions } from 'vuex'; |
|
import debounce from 'debounce'; |
|
import { SqlUiFactory, ViewTypes, UITypes } from 'nocodb-sdk'; |
|
import FileSaver from 'file-saver'; |
|
import FormView from './views/FormView'; |
|
import XcGridView from './views/GridView'; |
|
import spreadsheet from './mixins/spreadsheet'; |
|
import DebugMetas from '~/components/project/spreadsheet/components/DebugMetas'; |
|
|
|
import AdditionalFeatures from '~/components/project/spreadsheet/overlay/AdditinalFeatures'; |
|
import GalleryView from '~/components/project/spreadsheet/views/GalleryView'; |
|
import CalendarView from '~/components/project/spreadsheet/views/CalendarView'; |
|
import KanbanView from '~/components/project/spreadsheet/views/KanbanView'; |
|
import SortList from '~/components/project/spreadsheet/components/SortListMenu'; |
|
import Fields from '~/components/project/spreadsheet/components/FieldsMenu'; |
|
import SpreadsheetNavDrawer from '~/components/project/spreadsheet/components/SpreadsheetNavDrawer'; |
|
import LockMenu from '~/components/project/spreadsheet/components/LockMenu'; |
|
import ExpandedForm from '~/components/project/spreadsheet/components/ExpandedForm'; |
|
import Pagination from '~/components/project/spreadsheet/components/Pagination'; |
|
import ColumnFilter from '~/components/project/spreadsheet/components/ColumnFilterMenu'; |
|
import MoreActions from '~/components/project/spreadsheet/components/MoreActions'; |
|
import ShareViewMenu from '~/components/project/spreadsheet/components/ShareViewMenu'; |
|
import getPlainText from '~/components/project/spreadsheet/helpers/getPlainText'; |
|
|
|
export default { |
|
name: 'RowsXcDataTable', |
|
components: { |
|
ShareViewMenu, |
|
MoreActions, |
|
FormView, |
|
DebugMetas, |
|
Pagination, |
|
ExpandedForm, |
|
LockMenu, |
|
SpreadsheetNavDrawer, |
|
Fields, |
|
SortList, |
|
XcGridView, |
|
KanbanView, |
|
CalendarView, |
|
GalleryView, |
|
ColumnFilter, |
|
AdditionalFeatures, |
|
}, |
|
mixins: [spreadsheet], |
|
props: { |
|
isView: Boolean, |
|
isActive: Boolean, |
|
tabId: String, |
|
env: String, |
|
nodes: Object, |
|
relationType: String, |
|
relation: Object, |
|
relationIdValue: [String, Number], |
|
refTable: String, |
|
relationPrimaryValue: [String, Number], |
|
deleteTable: Function, |
|
showTabs: [Boolean, Number], |
|
}, |
|
data: () => ({ |
|
syncDataDebounce: debounce(async function (self) { |
|
await self.syncData(); |
|
}, 500), |
|
loadTableDataDeb: debounce(async function (self, ignoreLoader) { |
|
await self.loadTableDataFn(ignoreLoader); |
|
}, 200), |
|
viewKey: 0, |
|
extraViewParams: {}, |
|
debug: false, |
|
key: 1, |
|
dataLoaded: false, |
|
searchQueryVal: '', |
|
columnsWidth: null, |
|
viewStatus: { |
|
type: null, |
|
}, |
|
fieldsOrder: [], |
|
coverImageField: null, |
|
groupingField: null, |
|
// showSystemFields: false, |
|
showAdvanceOptions: false, |
|
loadViews: false, |
|
selectedView: {}, |
|
overShieldIcon: false, |
|
progress: false, |
|
createViewType: '', |
|
addNewColModal: false, |
|
showAddFeatOverlay: false, |
|
featureType: null, |
|
addNewColMenu: false, |
|
newColumn: {}, |
|
shareLink: null, |
|
loadingMeta: true, |
|
loadingData: true, |
|
toggleDrawer: false, |
|
selectedViewId: '', |
|
searchField: null, |
|
searchQuery: '', |
|
showExpandModal: false, |
|
selectedExpandRowIndex: null, |
|
selectedExpandRowMeta: null, |
|
navDrawer: true, |
|
selected: { |
|
row: null, |
|
col: null, |
|
}, |
|
editEnabled: { |
|
row: null, |
|
col: null, |
|
}, |
|
page: 1, |
|
count: 0, |
|
// size: 25, |
|
xWhere: '', |
|
sort: '', |
|
|
|
cellHeight: 'small', |
|
|
|
spreadsheet: null, |
|
options: { |
|
allowToolbar: true, |
|
columnSorting: false, |
|
}, |
|
filteredData: [], |
|
|
|
cellHeights: [ |
|
{ |
|
size: 'small', |
|
icon: 'mdi-view-headline', |
|
}, |
|
{ |
|
size: 'medium', |
|
icon: 'mdi-view-sequential', |
|
}, |
|
{ |
|
size: 'large', |
|
icon: 'mdi-view-stream', |
|
}, |
|
{ |
|
size: 'xlarge', |
|
icon: 'mdi-ca rd', |
|
}, |
|
], |
|
rowContextMenu: null, |
|
presetValues: {}, |
|
kanban: { |
|
data: [], |
|
stages: [], |
|
blocks: [], |
|
recordCnt: {}, |
|
recordTotalCnt: {}, |
|
groupingColumnItems: [], |
|
loadingData: true, |
|
selectedExpandRow: null, |
|
selectedExpandOldRow: null, |
|
selectedExpandRowMeta: null, |
|
}, |
|
clickCount: 0, |
|
}), |
|
watch: { |
|
isActive(n, o) { |
|
if (!o && n) { |
|
this.reload(true); |
|
} |
|
}, |
|
page(p) { |
|
this.$store.commit('tabs/MutSetTabState', { |
|
id: this.uniqueId, |
|
key: 'page', |
|
val: p, |
|
}); |
|
}, |
|
selectedViewId(id) { |
|
if (this.tabsState[this.tabId] && this.tabsState[this.tabId].page) { |
|
this.page = this.tabsState[this.tabId].page || 1; |
|
} else { |
|
this.page = 1; |
|
} |
|
// this.$store.commit('tabs/MutSetTabState', { |
|
// id: this.tabId, |
|
// key: 'selectedViewId', |
|
// val: id |
|
// }) |
|
}, |
|
async groupingField(newVal) { |
|
this.groupingField = newVal; |
|
if (this.selectedView && this.selectedView.show_as === 'kanban') { |
|
await this.loadKanbanData(); |
|
} |
|
}, |
|
}, |
|
async mounted() { |
|
try { |
|
if (this.tabsState && this.tabsState[this.uniqueId]) { |
|
if (this.tabsState[this.uniqueId].page) { |
|
this.page = this.tabsState[this.uniqueId].page; |
|
} |
|
} |
|
await this.createTableIfNewTable(); |
|
this.loadingMeta = true; |
|
await this.loadMeta(false); |
|
this.loadingMeta = false; |
|
} catch (e) { |
|
console.log(e); |
|
} |
|
this.searchField = this.primaryValueColumn; |
|
}, |
|
methods: { |
|
clickAddNewIcon() { |
|
this.insertNewRow(true, true); |
|
this.$e('c:row:add:grid-top'); |
|
}, |
|
toggleClick() { |
|
this.$e('c:grid:toggle-navdraw'); |
|
}, |
|
...mapActions({ |
|
loadTablesFromChildTreeNode: 'project/loadTablesFromChildTreeNode', |
|
}), |
|
generateNewViewKey() { |
|
this.viewKey = Math.random(); |
|
}, |
|
loadNext() { |
|
this.selectedExpandRowIndex = ++this.selectedExpandRowIndex % this.data.length; |
|
}, |
|
loadPrev() { |
|
this.selectedExpandRowIndex = |
|
--this.selectedExpandRowIndex === -1 ? this.data.length - 1 : this.selectedExpandRowIndex; |
|
}, |
|
async checkAndDeleteTable() { |
|
// if ( |
|
// !this.meta || ( |
|
// (this.meta.hasMany && this.meta.hasMany.length) || |
|
// (this.meta.manyToMany && this.meta.manyToMany.length) || |
|
// (this.meta.belongsTo && this.meta.belongsTo.length)) |
|
// ) { |
|
// return this.$toast.info('Please delete relations before deleting table.').goAway(3000) |
|
// } |
|
this.deleteTable('showDialog', this.meta.id); |
|
|
|
// if (confirm('Do you want to delete the table?')) { |
|
// await this.$api.meta.tableDelete(this.meta.id) |
|
// } |
|
this.$e('c:table:delete'); |
|
}, |
|
async reloadClick() { |
|
await this.reload(); |
|
this.$e('a:table:reload:navbar'); |
|
}, |
|
async reload(ignoreLoader = false) { |
|
this.$store.dispatch('meta/ActLoadMeta', { |
|
env: this.nodes.env, |
|
dbAlias: this.nodes.dbAlias, |
|
table_name: this.table, |
|
force: true, |
|
}); |
|
if (this.selectedView && this.selectedView.show_as === 'kanban') { |
|
await this.loadKanbanData(); |
|
} else { |
|
await this.loadTableData(ignoreLoader); |
|
} |
|
this.key = Math.random(); |
|
}, |
|
reloadComments() { |
|
if (this.$refs.ncgridview) { |
|
this.$refs.ncgridview.xcAuditModelCommentsCount(); |
|
} |
|
}, |
|
async syncData() {}, |
|
showAdditionalFeatOverlay(feat) { |
|
this.showAddFeatOverlay = true; |
|
this.featureType = feat; |
|
}, |
|
async createTableIfNewTable() { |
|
if (this.nodes.newTable && !this.nodes.tableCreated) { |
|
const columns = this.sqlUi.getNewTableColumns().filter(col => { |
|
if (col.column_name === 'id' && this.nodes.newTable.columns.includes('id_ag')) { |
|
Object.assign(col, this.sqlUi.getDataTypeForUiType({ uidt: UITypes.ID }, 'AG')); |
|
|
|
col.dtxp = this.sqlUi.getDefaultLengthForDatatype(col.dt); |
|
col.dtxs = this.sqlUi.getDefaultScaleForDatatype(col.dt); |
|
|
|
return true; |
|
} |
|
return this.nodes.newTable.columns.includes(col.column_name); |
|
}); |
|
await this.$api.dbTable.create(this.projectId, { |
|
table_name: this.nodes.table_name, |
|
title: this.nodes.title, |
|
columns, |
|
}); |
|
|
|
await this.loadTablesFromChildTreeNode({ |
|
_nodes: { |
|
...this.nodes, |
|
}, |
|
}); |
|
// eslint-disable-next-line vue/no-mutating-props |
|
this.nodes.tableCreated = true; |
|
} |
|
|
|
this.loadViews = true; |
|
}, |
|
|
|
comingSoon() { |
|
this.$toast.info('Coming soon!').goAway(3000); |
|
}, |
|
changed(col, row) { |
|
this.$set(this.data[row].rowMeta, 'changed', this.data[row].rowMeta.changed || {}); |
|
if (this.data[row].rowMeta) { |
|
this.$set(this.data[row].rowMeta.changed, this.availableColumns[col].column_name, true); |
|
} |
|
}, |
|
async save() { |
|
for (let row = 0; row < this.rowLength; row++) { |
|
const { row: rowObj, rowMeta } = this.data[row]; |
|
if (rowMeta.new) { |
|
try { |
|
this.$set(this.data[row], 'saving', true); |
|
const pks = this.meta.columns.filter(col => { |
|
return col.pk; |
|
}); |
|
if ( |
|
this.meta.columns.every(col => { |
|
return !col.ai; |
|
}) && |
|
pks.length && |
|
pks.every(col => !rowObj[col.title] && !(col.columnDefault || col.default) && !(col.meta && col.meta.ag)) |
|
) { |
|
return this.$toast.info('Primary column is empty please provide some value').goAway(3000); |
|
} |
|
if ( |
|
this.meta.columns.some(col => { |
|
return ( |
|
!col.ai && |
|
col.rqd && |
|
!(col.meta && col.meta.ag) && |
|
(rowObj[col.title] === undefined || rowObj[col.title] === null) && |
|
!col.cdf |
|
); |
|
}) |
|
) { |
|
return; |
|
} |
|
|
|
const insertObj = this.meta.columns.reduce((o, col) => { |
|
if (!col.ai && (rowObj && rowObj[col.title]) !== null) { |
|
o[col.title] = rowObj && rowObj[col.title]; |
|
} |
|
return o; |
|
}, {}); |
|
|
|
// const insertedData = await this.api.insert(insertObj) |
|
const insertedData = await this.$api.dbViewRow.create( |
|
'noco', |
|
this.projectName, |
|
this.meta.id, |
|
this.selectedView.id, |
|
insertObj |
|
); |
|
|
|
this.data.splice(row, 1, { |
|
row: insertedData, |
|
rowMeta: {}, |
|
oldRow: { ...insertedData }, |
|
}); |
|
|
|
/* this.$toast.success(`${insertedData[this.primaryValueColumn] ? `${insertedData[this.primaryValueColumn]}'s r` : 'R'}ow saved successfully.`, { |
|
position: 'bottom-center' |
|
}).goAway(3000) */ |
|
} catch (e) { |
|
// if (e.response && e.response.data && e.response.data.msg) { |
|
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000); |
|
// } else { |
|
// this.$toast.error(`Failed to |
|
// |
|
// row : ${e.message}`).goAway(3000) |
|
// } |
|
} finally { |
|
this.$set(this.data[row], 'saving', false); |
|
} |
|
} |
|
} |
|
this.syncCount(); |
|
}, |
|
// // todo: move debounce to cell since this will skip few update api call |
|
// onCellValueChangeDebounce: debounce(async function(col, row, column, self) { |
|
// await self.onCellValueChangeFn(col, row, column) |
|
// }, 100), |
|
// onCellValueChange(col, row, column) { |
|
// this.onCellValueChangeFn(col, row, column) |
|
// }, |
|
async onCellValueChange(col, row, column, saved = true) { |
|
if (!this.data[row]) { |
|
return; |
|
} |
|
const { row: rowObj, rowMeta, oldRow, saving, lastSave } = this.data[row]; |
|
if (!lastSave) { |
|
this.$set(this.data[row], 'lastSave', rowObj[column.title]); |
|
} |
|
if (rowMeta.new) { |
|
// return if there is no change |
|
if ((column && oldRow[column.title] === rowObj[column.title]) || saving) { |
|
return; |
|
} |
|
await this.save(); |
|
} else { |
|
try { |
|
// if (!this.api) { |
|
// return |
|
// } |
|
// return if there is no change |
|
if ( |
|
!column || |
|
saving || |
|
(oldRow[column.title] === rowObj[column.title] && |
|
(lastSave || rowObj[column.title]) === rowObj[column.title]) |
|
) { |
|
return; |
|
} |
|
if (saved) { |
|
this.$set(this.data[row], 'lastSave', oldRow[column.title]); |
|
} |
|
const id = this.meta.columns |
|
.filter(c => c.pk) |
|
.map(c => rowObj[c.title]) |
|
.join('___'); |
|
|
|
if (!id) { |
|
return this.$toast.info("Update not allowed for table which doesn't have primary Key").goAway(3000); |
|
} |
|
this.$set(this.data[row], 'saving', true); |
|
|
|
// eslint-disable-next-line promise/param-names |
|
const newData = await this.$api.dbViewRow.update( |
|
'noco', |
|
this.projectName, |
|
this.meta.id, |
|
this.selectedView.id, |
|
id, |
|
{ |
|
[column.title]: rowObj[column.title], |
|
}, |
|
{ |
|
query: { ignoreWebhook: !saved }, |
|
} |
|
); |
|
|
|
// audit |
|
this.$api.utils |
|
.auditRowUpdate(id, { |
|
fk_model_id: this.meta.id, |
|
column_name: column.title, |
|
row_id: id, |
|
value: getPlainText(rowObj[column.title]), |
|
prev_value: getPlainText(oldRow[column.title]), |
|
}) |
|
.then(() => {}); |
|
|
|
this.$set(this.data[row], 'row', { ...rowObj, ...newData }); |
|
|
|
this.$set(oldRow, column.title, rowObj[column.title]); |
|
/* this.$toast.success(`${rowObj[this.primaryValueColumn] ? `${rowObj[this.primaryValueColumn]}'s c` : 'C'}olumn '${column.column_name}' updated successfully.`, { |
|
position: 'bottom-center' |
|
}).goAway(3000) */ |
|
} catch (e) { |
|
// if (e.response && e.response.data && e.response.data.msg) { |
|
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000); |
|
// } else { |
|
// this.$toast.error(`Failed to update row : ${e.message}`).goAway(3000) |
|
// } |
|
} |
|
|
|
this.$set(this.data[row], 'saving', false); |
|
} |
|
}, |
|
async deleteRow() { |
|
try { |
|
const rowObj = this.rowContextMenu.row; |
|
if (!this.rowContextMenu.rowMeta.new) { |
|
const id = |
|
this.meta && |
|
this.meta.columns && |
|
this.meta.columns |
|
.filter(c => c.pk) |
|
.map(c => rowObj[c.title]) |
|
.join('___'); |
|
|
|
const successfulDeletion = await this.deleteRowById(id); |
|
if (!successfulDeletion) { |
|
return; |
|
} |
|
} |
|
this.data.splice(this.rowContextMenu.index, 1); |
|
this.syncCount(); |
|
// this.$toast.success('Deleted row successfully').goAway(3000) |
|
} catch (e) { |
|
this.$toast.error(`Failed to delete row : ${e.message}`).goAway(3000); |
|
} |
|
}, |
|
async deleteSelectedRows() { |
|
let row = this.rowLength; |
|
while (row--) { |
|
try { |
|
const { row: rowObj, rowMeta } = this.data[row]; |
|
if (!rowMeta.selected) { |
|
continue; |
|
} |
|
if (!rowMeta.new) { |
|
const id = this.meta.columns |
|
.filter(c => c.pk) |
|
.map(c => rowObj[c.title]) |
|
.join('___'); |
|
|
|
const successfulDeletion = await this.deleteRowById(id); |
|
if (!successfulDeletion) { |
|
continue; |
|
} |
|
} |
|
this.data.splice(row, 1); |
|
} catch (e) { |
|
return this.$toast.error(`Failed to delete row : ${e.message}`).goAway(3000); |
|
} |
|
} |
|
this.syncCount(); |
|
}, |
|
|
|
async deleteRowById(id) { |
|
try { |
|
if (!id) { |
|
this.$toast.info("Delete not allowed for table which doesn't have primary Key").goAway(3000); |
|
return false; |
|
} |
|
|
|
const res = await this.$api.dbViewRow.delete('noco', this.projectName, this.meta.id, this.selectedView.id, id); |
|
|
|
if (res?.message) { |
|
this.$toast |
|
.info( |
|
`<div style="padding:10px 4px">Unable to delete row with ID ${id} because of the following: |
|
<br><br>${res.message.join('<br>')}<br><br> |
|
Clear the data first & try again</div>` |
|
) |
|
.goAway(5000); |
|
return false; |
|
} |
|
} catch (e) { |
|
this.$toast.error(`Failed to delete row : ${e.message}`).goAway(3000); |
|
return false; |
|
} |
|
return true; |
|
}, |
|
|
|
async clearCellValue() { |
|
const { col, colIndex, row, index } = this.rowContextMenu; |
|
if (row[col.title] === null) { |
|
return; |
|
} |
|
this.$set(this.data[index].row, col.title, null); |
|
await this.onCellValueChange(colIndex, index, col, true); |
|
}, |
|
async insertNewRow(atEnd = false, expand = false, presetValues = {}) { |
|
const isKanban = this.selectedView && this.selectedView.show_as === 'kanban'; |
|
const data = isKanban ? this.kanban.data : this.data; |
|
const focusRow = isKanban ? data.length : atEnd ? this.rowLength : this.rowContextMenu.index + 1; |
|
const focusCol = this.availableColumns.findIndex(c => !c.ai); |
|
data.splice(focusRow, 0, { |
|
row: |
|
this.relationType === 'hm' |
|
? { |
|
...this.fieldList.reduce( |
|
(o, f) => ({ |
|
...o, |
|
[f]: presetValues[f] ?? null, |
|
}), |
|
{} |
|
), |
|
[this.relation.column_name]: this.relationIdValue, |
|
} |
|
: this.fieldList.reduce( |
|
(o, f) => ({ |
|
...o, |
|
[f]: presetValues[f] ?? null, |
|
}), |
|
{} |
|
), |
|
rowMeta: { |
|
new: true, |
|
}, |
|
oldRow: {}, |
|
}); |
|
if (data[focusRow].row[this.groupingField] === 'Uncategorized') { |
|
data[focusRow].row[this.groupingField] = null; |
|
} |
|
this.selected = { |
|
row: focusRow, |
|
col: focusCol, |
|
}; |
|
this.editEnabled = { |
|
row: focusRow, |
|
col: focusCol, |
|
}; |
|
this.presetValues = presetValues; |
|
|
|
if (expand) { |
|
if (isKanban) { |
|
this.expandKanbanForm(-1, data[focusRow]); |
|
} else { |
|
const { rowMeta } = data[data.length - 1]; |
|
this.expandRow(data.length - 1, rowMeta); |
|
} |
|
} |
|
}, |
|
|
|
async handleKeyDown({ metaKey, key, altKey, shiftKey, ctrlKey }) { |
|
switch ([this._isMac ? metaKey : ctrlKey, key].join('_')) { |
|
case 'true_s': |
|
this.edited && (await this.save()); |
|
break; |
|
case 'true_l': |
|
await this.loadTableData(); |
|
break; |
|
case 'true_n': |
|
this.insertNewRow(true); |
|
break; |
|
} |
|
}, |
|
async loadMeta() { |
|
// load latest table meta |
|
await this.$store.dispatch('meta/ActLoadMeta', { |
|
env: this.nodes.env, |
|
dbAlias: this.nodes.dbAlias, |
|
table_name: this.table, |
|
force: true, |
|
}); |
|
}, |
|
clickPagination() { |
|
this.loadTableData(false); |
|
this.$e('a:grid:pagination'); |
|
}, |
|
loadTableData(ignoreLoader = true) { |
|
this.loadTableDataDeb(this, ignoreLoader); |
|
}, |
|
async loadTableDataFn(ignoreLoader = true) { |
|
if (this.isForm || !this.selectedView || !this.selectedView.title) { |
|
return; |
|
} |
|
this.loadingData = !ignoreLoader; |
|
try { |
|
// if (this.api) { |
|
// const { list, count } = await this.api.paginatedList(this.queryParams) |
|
// const { |
|
// list, |
|
// pageInfo |
|
// } = (await this.$api.data.list( |
|
// this.selectedViewId || this.meta.views[0].id, |
|
// { |
|
// query: { |
|
// ...this.queryParams, |
|
// ...(this._isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(this.sortList) }), |
|
// ...(this._isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(this.filters) }) |
|
// } |
|
// })).data.data |
|
|
|
const { list, pageInfo } = await this.$api.dbViewRow.list( |
|
'noco', |
|
this.projectName, |
|
this.meta.id, |
|
this.selectedView.id, |
|
this.listQueryParams |
|
); |
|
|
|
this.count = pageInfo.totalRows; // count |
|
this.data = list.map(row => ({ |
|
row, |
|
oldRow: { ...row }, |
|
rowMeta: {}, |
|
})); |
|
// } |
|
} catch (e) { |
|
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000); |
|
} |
|
this.loadingData = false; |
|
}, |
|
showRowContextMenu(e, row, rowMeta, index, colIndex, col) { |
|
if (!this.isEditable) { |
|
return; |
|
} |
|
e.preventDefault(); |
|
this.rowContextMenu = false; |
|
this.$nextTick(() => { |
|
this.rowContextMenu = { |
|
x: e.clientX, |
|
y: e.clientY, |
|
row, |
|
index, |
|
rowMeta, |
|
colIndex, |
|
col, |
|
}; |
|
}); |
|
}, |
|
expandRow(row, rowMeta) { |
|
this.showExpandModal = true; |
|
this.selectedExpandRowIndex = row; |
|
this.selectedExpandRowMeta = rowMeta; |
|
}, |
|
async onNewColCreation(col, oldCol) { |
|
// if (this.$refs.drawer) { |
|
// await this.$refs.drawer.loadViews() |
|
// this.$refs.drawer.onViewIdChange(this.selectedViewId) |
|
// } |
|
await this.loadMeta(true, col, oldCol); |
|
this.$nextTick(async () => { |
|
await this.loadTableData(); |
|
}); |
|
this.$refs.fields && this.$refs.fields.loadFields(); |
|
}, |
|
onColDelete() { |
|
this.$refs.fields && this.$refs.fields.loadFields(); |
|
}, |
|
onFileDrop(ev) { |
|
let file; |
|
if (ev.dataTransfer.items) { |
|
// Use DataTransferItemList interface to access the file(s) |
|
if (ev.dataTransfer.items.length && ev.dataTransfer.items[0].kind === 'file') { |
|
file = ev.dataTransfer.items[0].getAsFile(); |
|
} |
|
} else if (ev.dataTransfer.files.length) { |
|
file = ev.dataTransfer.files[0]; |
|
} |
|
|
|
if (file && !file.name.endsWith('.csv')) { |
|
return; |
|
} |
|
|
|
this.$refs.csvExportImport.onCsvFileSelection(file); |
|
}, |
|
// Kanban |
|
async loadKanbanData(initKanbanProps = true) { |
|
try { |
|
const kanban = { |
|
data: [], |
|
stages: [], |
|
blocks: [], |
|
recordCnt: {}, |
|
recordTotalCnt: {}, |
|
groupingColumnItems: [], |
|
loadingData: true, |
|
selectedExpandRow: null, |
|
selectedExpandOldRow: null, |
|
selectedExpandRowMeta: null, |
|
}; |
|
if (initKanbanProps) { |
|
this.kanban = kanban; |
|
} |
|
|
|
if (this.api) { |
|
const groupingColumn = this.meta.columns.find(c => c.title === this.groupingField); |
|
|
|
if (!groupingColumn) { |
|
return; |
|
} |
|
const initialLimit = 10; |
|
const uncategorized = 'Uncategorized'; |
|
|
|
kanban.groupingColumnItems = groupingColumn.dtxp |
|
.split(',') |
|
.map(c => { |
|
const trimCol = c.replace(/'/g, ''); |
|
kanban.recordCnt[trimCol] = 0; |
|
return trimCol; |
|
}) |
|
.sort(); |
|
|
|
kanban.groupingColumnItems.unshift(uncategorized); |
|
kanban.recordCnt[uncategorized] = 0; |
|
for (const groupingColumnItem of kanban.groupingColumnItems) { |
|
{ |
|
// enrich Kanban data |
|
const { data } = await this.api.get( |
|
`/nc/${this.$store.state.project.projectId}/api/v1/${this.$route.query.name}`, |
|
{ |
|
limit: initialLimit, |
|
where: |
|
groupingColumnItem === uncategorized |
|
? `(${this.groupingField},is,null)` |
|
: `(${this.groupingField},eq,${groupingColumnItem})`, |
|
} |
|
); |
|
data.forEach(d => { |
|
// handle composite primary key |
|
d.c_pk = this.meta.columns |
|
.filter(c => c.pk) |
|
.map(c => d[c.title]) |
|
.join('___'); |
|
if (!d.id) { |
|
// id is required for <kanban-board/> |
|
d.id = d.c_pk; |
|
} |
|
kanban.data.push({ |
|
row: d, |
|
oldRow: d, |
|
rowMeta: {}, |
|
}); |
|
kanban.recordCnt[groupingColumnItem] += 1; |
|
kanban.blocks.push({ |
|
status: groupingColumnItem, |
|
...d, |
|
}); |
|
}); |
|
} |
|
{ |
|
// enrich recordTotalCnt |
|
const { data } = await this.api.get( |
|
`/nc/${this.$store.state.project.projectId}/api/v1/${this.$route.query.name}/count`, |
|
{ |
|
where: |
|
groupingColumnItem === uncategorized |
|
? `(${this.groupingField},is,null)` |
|
: `(${this.groupingField},eq,${groupingColumnItem})`, |
|
} |
|
); |
|
kanban.recordTotalCnt[groupingColumnItem] = data.count; |
|
} |
|
} |
|
} |
|
this.kanban = kanban; |
|
} catch (e) { |
|
if (e.response && e.response.data && e.response.data.msg) { |
|
this.$toast |
|
.error(e.response.data.msg, { |
|
position: 'bottom-center', |
|
}) |
|
.goAway(3000); |
|
} else { |
|
this.$toast |
|
.error(`Error occurred : ${e.message}`, { |
|
position: 'bottom-center', |
|
}) |
|
.goAway(3000); |
|
} |
|
} finally { |
|
this.kanban.loadingData = false; |
|
} |
|
}, |
|
async loadMoreKanbanData(groupingFieldVal) { |
|
const uncategorized = 'uncategorized'; |
|
const { data } = await this.api.get( |
|
`/nc/${this.$store.state.project.projectId}/api/v1/${this.$route.query.name}`, |
|
{ |
|
limit: 5, |
|
where: |
|
groupingFieldVal === uncategorized |
|
? `(${this.groupingField},is,null)` |
|
: `(${this.groupingField},eq,${groupingFieldVal})`, |
|
offset: this.kanban.recordCnt[groupingFieldVal], |
|
} |
|
); |
|
data.map(d => { |
|
// handle composite primary key |
|
d.c_pk = this.meta.columns |
|
.filter(c => c.pk) |
|
.map(c => d[c.title]) |
|
.join('___'); |
|
if (!d.id) { |
|
// id is required for <kanban-board/> |
|
d.id = d.c_pk; |
|
} |
|
this.kanban.data.push({ |
|
row: d, |
|
oldRow: d, |
|
rowMeta: {}, |
|
}); |
|
this.kanban.blocks.push({ |
|
status: groupingFieldVal, |
|
...d, |
|
}); |
|
}); |
|
this.kanban.recordCnt[groupingFieldVal] += data.length; |
|
}, |
|
expandKanbanForm(rowIdx, data) { |
|
if (rowIdx != -1) { |
|
// not a new record -> find the target record |
|
data = this.kanban.data.filter(o => o.row.c_pk == rowIdx)[0]; |
|
} |
|
this.showExpandModal = true; |
|
this.kanban.selectedExpandRow = data.row; |
|
this.kanban.selectedExpandOldRow = data.oldRow; |
|
this.kanban.selectedExpandRowMeta = data.rowMeta; |
|
}, |
|
async exportCache() { |
|
try { |
|
const data = await this.$api.utils.cacheGet(); |
|
if (!data) { |
|
this.$toast.info('Cache is empty').goAway(3000); |
|
return; |
|
} |
|
const blob = new Blob([JSON.stringify(data)], { |
|
type: 'text/plain;charset=utf-8', |
|
}); |
|
FileSaver.saveAs(blob, 'cache_exported.json'); |
|
this.$toast.info('Exported Cache Successfully').goAway(3000); |
|
} catch (e) { |
|
console.log(e); |
|
this.$toast.error(e.message).goAway(3000); |
|
} |
|
}, |
|
async deleteCache() { |
|
try { |
|
await this.$api.utils.cacheDelete(); |
|
this.$toast.info('Deleted Cache Successfully').goAway(3000); |
|
} catch (e) { |
|
console.log(e); |
|
this.$toast.error(e.message).goAway(3000); |
|
} |
|
}, |
|
async syncCount() { |
|
const { count } = await this.$api.dbViewRow.count( |
|
'noco', |
|
this.$store.getters['project/GtrProjectName'], |
|
this.meta.id, |
|
this.selectedView.id |
|
); |
|
this.count = count; |
|
}, |
|
}, |
|
computed: { |
|
listQueryParams() { |
|
return { |
|
...this.queryParams, |
|
...(this._isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(this.sortList) }), |
|
...(this._isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(this.filters) }), |
|
// sort: ['-FirstName'], |
|
// where: '(FirstName,like,%ro)~or((FirstName,like,%a)~and(FirstName,like,%e%))' |
|
}; |
|
}, |
|
isLocked() { |
|
return this.lockType === 'locked'; |
|
}, |
|
lockType: { |
|
get() { |
|
return this.selectedView && this.selectedView.lock_type; |
|
}, |
|
set(type) { |
|
this.selectedView.lock_type = type; |
|
this.$api.dbView.update(this.selectedViewId, { |
|
lock_type: type, |
|
}); |
|
}, |
|
}, |
|
showSystemFields: { |
|
get() { |
|
return this.selectedView && this.selectedView.show_system_fields; |
|
}, |
|
set(v) { |
|
if (this.selectedView) { |
|
this.selectedView.show_system_fields = v; |
|
this.$api.dbView |
|
.update(this.selectedViewId, { |
|
show_system_fields: v, |
|
}) |
|
.then(() => { |
|
if (v) { |
|
this.loadTableData(); |
|
} |
|
}); |
|
} |
|
}, |
|
}, |
|
viewTypes() { |
|
return ViewTypes; |
|
}, |
|
tabsState() { |
|
return this.$store.state.tabs.tabsState || {}; |
|
}, |
|
uniqueId() { |
|
return `${this.tabId}_${this.selectedViewId}`; |
|
}, |
|
size() { |
|
return (this.$store.state.project.appInfo && this.$store.state.project.appInfo.defaultLimit) || 25; |
|
}, |
|
isPkAvail() { |
|
return this.meta && this.meta.columns.some(c => c.pk); |
|
}, |
|
isGallery() { |
|
return this.selectedView && this.selectedView.type === ViewTypes.GALLERY; |
|
}, |
|
isForm() { |
|
return this.selectedView && this.selectedView.type === ViewTypes.FORM; |
|
}, |
|
isKanban() { |
|
return this.selectedView && this.selectedView.type === ViewTypes.KANBAN; |
|
}, |
|
isGrid() { |
|
return this.selectedView && this.selectedView.type === ViewTypes.GRID; |
|
}, |
|
meta() { |
|
return this.$store.state.meta.metas[this.table]; |
|
}, |
|
currentApiUrl() { |
|
return ( |
|
this.api && |
|
`${this.api.apiUrl}?` + |
|
Object.entries(this.queryParams) |
|
.filter(p => p[1]) |
|
.map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) |
|
.join('&') |
|
); |
|
}, |
|
isEditable() { |
|
return this._isUIAllowed('xcDatatableEditable'); |
|
}, |
|
sqlUi() { |
|
// return SqlUI.create(this.nodes.dbConnection) |
|
return SqlUiFactory.create(this.nodes.dbConnection); |
|
}, |
|
api() { |
|
return ( |
|
this.meta && |
|
this.$ncApis.get({ |
|
env: this.nodes.env, |
|
dbAlias: this.nodes.dbAlias, |
|
table: this.meta.table_name, |
|
}) |
|
); |
|
// return this.meta && this.meta.title ? ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.meta && this.meta.title, this.meta && this.meta.columns, this, this.meta) : null |
|
}, |
|
}, |
|
}; |
|
</script> |
|
|
|
<style scoped> |
|
/deep/ .v-input__control .v-input__slot .v-input--selection-controls__input { |
|
transform: scale(0.85); |
|
margin-right: 0; |
|
} |
|
|
|
/deep/ .xc-toolbar .v-input__slot, |
|
.navigation .v-input__slot { |
|
box-shadow: none !important; |
|
} |
|
|
|
/deep/ .navigation .v-input__slot input::placeholder { |
|
font-size: 0.8rem; |
|
} |
|
|
|
/deep/ .v-btn { |
|
text-transform: capitalize; |
|
} |
|
|
|
.row-expand-icon, |
|
.row-checkbox { |
|
opacity: 0; |
|
} |
|
|
|
/deep/ .row-checkbox .v-input__control { |
|
height: 24px !important; |
|
} |
|
|
|
.cell-height-medium td, |
|
.cell-height-medium tr { |
|
min-height: 35px !important; |
|
/*height: 35px !important;*/ |
|
/*max-height: 35px !important;*/ |
|
} |
|
|
|
.cell-height-large td, |
|
.cell-height-large tr { |
|
min-height: 40px !important; |
|
/*height: 40px !important;*/ |
|
/*max-height: 40px !important;*/ |
|
} |
|
|
|
.cell-height-xlarge td, |
|
.cell-height-xlarge tr { |
|
min-height: 50px !important; |
|
/*height: 50px !important;*/ |
|
/*max-height: 50px !important;*/ |
|
} |
|
|
|
/deep/ .xc-border.search-box { |
|
overflow: visible; |
|
border-radius: 4px; |
|
} |
|
|
|
/deep/ .xc-border.search-box .v-input { |
|
transition: 0.4s border-color; |
|
} |
|
|
|
/deep/ .xc-border.search-box .v-input--is-focused { |
|
border: 1px solid var(--v-primary-base) !important; |
|
margin: -1px; |
|
} |
|
|
|
/deep/ .search-field.v-text-field.v-text-field--solo.v-input--dense > .v-input__control { |
|
min-height: auto; |
|
} |
|
|
|
.views-navigation-drawer { |
|
transition: 0.4s max-width, 0.4s min-width; |
|
} |
|
|
|
.new-column-header { |
|
text-align: center; |
|
min-width: 70px; |
|
} |
|
|
|
/deep/ .v-input__control label { |
|
font-size: inherit; |
|
} |
|
|
|
/deep/ .nc-table-toolbar > .v-toolbar__content { |
|
padding: 0; |
|
} |
|
</style> |
|
|
|
<!-- |
|
/** |
|
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
|
* |
|
* @author Naveen MR <oof1lab@gmail.com> |
|
* @author Pranav C Balan <pranavxc@gmail.com> |
|
* @author Wing-Kam Wong <wingkwong.code@gmail.com> |
|
* |
|
* @license GNU AGPL version 3 or any later version |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU Affero General Public License as |
|
* published by the Free Software Foundation, either version 3 of the |
|
* License, or (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU Affero General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Affero General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
* |
|
*/ |
|
-->
|
|
|