多维表格
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.

883 lines
25 KiB

<template>
<div>
<table v-if="data" class="xc-row-table"
style=" ">
<thead>
<tr class="text-left">
<th
class="grey-border caption"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
style="width: 65px">#
</th>
<th
class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
v-xc-ver-resize
v-for="(col,i) in availableColumns"
:key="col.cn"
v-show="showFields[col._cn]"
@xcresize="onresize(col._cn,$event)"
@xcresizing="resizingCol = col._cn"
@xcresized="resizingCol = null"
:data-col="col._cn"
>
<!-- :style="columnsWidth[col._cn] ? `min-width:${columnsWidth[col._cn]}; max-width:${columnsWidth[col._cn]}` : ''"
-->
<header-cell
:isPublicView="isPublicView"
@onRelationDelete="$emit('onRelationDelete')"
:nodes="nodes"
:value="col._cn"
:sql-ui="sqlUi"
:meta="meta"
:column-index="meta && meta.columns && meta.columns.indexOf(col)"
:is-foreign-key="col._cn in belongsTo || col._cn in hasMany"
:column="col"
:isVirtual="isVirtual"
@saved="onNewColCreation"
></header-cell>
<!--{{ col.cn }}-->
</th>
<th v-for="(bt,i) in meta.belongsTo"
class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
:key="i"
>
{{ bt._rtn }} (Belongs To)
</th>
<th v-for="(hm,i) in meta.hasMany"
class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
:key="i"
>
{{ hm._tn }} (Has Many)
</th>
<th v-for="(mm,i) in meta.manyToMany"
class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
:key="i"
>
{{ mm.rtn }} (Many To Many)
</th>
<th
v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')"
@click="addNewColMenu = true"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
class="grey-border new-column-header pointer"
>
<v-menu offset-y z-index="99" left v-model="addNewColMenu"
content-class="elevation-0"
>
<template v-slot:activator="{on}">
<v-icon small v-on="on">mdi-plus</v-icon>
</template>
<edit-column
v-if="addNewColMenu"
:meta="meta"
:nodes="nodes"
@close="addNewColMenu = false"
@saved="onNewColCreation"
:sql-ui="sqlUi"
></edit-column>
</v-menu>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="({row:rowObj, rowMeta, oldRow},row) in data"
:key="row"
@contextmenu="showRowContextMenu($event,rowObj,rowMeta,row)"
>
<td
style="width: 65px" class="caption">
<div class="d-flex align-center">
<span v-show="!rowMeta || !rowMeta.selected" class="ml-2 grey--text"
:class="{ 'row-no' : !isPublicView }"
>{{ row + 1 }}</span>
<template v-if="!isPublicView">
<v-checkbox v-if="rowMeta" class="row-checkbox pt-0 align-self-center my-auto"
:class="{active : rowMeta.selected}"
v-model="rowMeta.selected"
dense></v-checkbox>
<v-spacer></v-spacer>
<v-icon
v-if="!groupedAggCount[ids[row]]"
color="pink"
small
class="row-expand-icon mr-1 pointer"
@click="expandRow(row,rowMeta)">
mdi-arrow-expand
</v-icon>
</template>
<v-chip
@click="expandRow(row,rowMeta)"
x-small v-if="groupedAggCount[ids[row]]"
:color="colors[ groupedAggCount[ids[row]] % colors.length]">
{{ groupedAggCount[ids[row]] }}
</v-chip>
</div>
</td>
<td
class="cell pointer"
v-for="(columnObj,col) in availableColumns"
:key="columnObj._cn"
:class="{
active : !isPublicView && selected.col === col && selected.row === row && isEditable ,
'primary-column' : primaryValueColumn === columnObj._cn,
'text-center': isCentrallyAligned(columnObj)
}"
@dblclick="makeEditable(col,row,columnObj.ai)"
@click="makeSelected(col,row);"
v-show="showFields[columnObj._cn]"
:data-col="columnObj._cn"
>
<editable-cell
v-if="!isLocked
&& !isPublicView
&& (
editEnabled.col === col
&& editEnabled.row === row
)
|| enableEditable(columnObj)"
:column="columnObj"
:meta="meta"
:active="(selected.col === col && selected.row === row)"
v-model="rowObj[columnObj._cn]"
@save="editEnabled = {}"
@cancel="editEnabled = {}"
@update="onCellValueChange(col, row, columnObj)"
@change="onCellValueChange(col, row, columnObj)"
:sql-ui="sqlUi"
:db-alias="nodes.dbAlias"
/>
<!-- @change="changed(col,row)"-->
<!-- />-->
<div v-else-if="columnObj.cn in hasMany" class="hasmany-col d-flex ">
{{ rowObj[columnObj._cn] }}
<v-spacer></v-spacer>
<v-menu open-on-hover>
<template v-slot:activator="{on}">
<v-icon v-on="on" class=" hasmany-col-menu-icon">mdi-menu-down</v-icon>
</template>
<v-list dense>
<v-list-item v-for="(rel,i) in hasMany[columnObj.cn]"
@click="addNewRelationTab(
rel,
table,
meta._tn,
rel.tn,
rel._tn,
rowObj[columnObj._cn],
'hm',
rowObj,
rowObj[primaryValueColumn]
)"
:key="i"
>
<v-chip small :color="colors[i % colors.length]">
<span class="caption text-capitalize"> {{ rel._tn }}</span>
</v-chip>
</v-list-item>
</v-list>
</v-menu>
</div>
<span v-else-if="columnObj._cn in belongsTo"
@click="addNewRelationTab(
belongsTo[columnObj._cn],
table,
meta._tn,
belongsTo[columnObj._cn].rtn,
belongsTo[columnObj._cn]._rtn,
rowObj[columnObj._cn],
'bt',
rowObj,
rowObj[primaryValueColumn]
)"
class="belongsto-col">{{ rowObj[columnObj._cn] }}</span>
<template v-else-if="primaryValueColumn === columnObj._cn">
<v-menu open-on-hover offset-y bottom>
<template v-slot:activator="{on}">
<!-- <v-chip v-on="on"
small
class="caption xc-bt-chip"
outlined
color="success">
{{ rowObj[columnObj.cn] }}
<v-icon v-on="on" class="hasmany-col-menu-icon pv">mdi-menu-down</v-icon>
</v-chip> -->
<span v-on="on"
class="caption xc-bt-chip primary--text">
{{ rowObj[columnObj._cn] }}
<v-icon v-on="on" class="hasmany-col-menu-icon pv">mdi-menu-down</v-icon>
</span>
</template>
<v-list dense>
<v-list-item dense v-if="haveHasManyrelation"><span class="grey--text caption text-center mt-n2">Has Many</span>
</v-list-item>
<template v-if="haveHasManyrelation">
<template v-for="(hm,idCol) in hasMany">
<template v-for="(rel,i) in hm">
<v-divider
:key="i + idCol + '_div'"></v-divider>
<v-list-item
class="py-1"
@click="addNewRelationTab(
rel,
table,
meta._tn,
rel.tn,
rel._tn,
rowObj[idCol],
'hm',
rowObj,
rowObj[primaryValueColumn]
)"
:key="i + idCol"
dense
>
<v-list-item-icon class="mx-1">
<v-icon class="has-many-icon mr-1" small :color="textColors[i % colors.length]">
mdi-source-fork
</v-icon>
</v-list-item-icon>
<!-- <v-chip small >-->
<!-- <v-list-item-title> -->
<span class="caption text-capitalize"> {{ rel._tn }}</span>
<!-- </v-list-item-title>-->
<!-- </v-chip>-->
</v-list-item>
</template>
</template>
</template>
<v-list-item v-else>
<span class="caption text-capitalize grey--text font-weight-light"> No relation found</span>
</v-list-item>
</v-list>
</v-menu>
</template>
<table-cell v-else
:selected="selected.col === col && selected.row === row"
:isLocked="isLocked"
@enableedit="makeSelected(col,row);makeEditable(col,row,columnObj.ai)"
:column="columnObj"
:meta="meta"
:db-alias="nodes.dbAlias"
:value="rowObj[columnObj._cn]"
:sql-ui="sqlUi"
></table-cell>
</td>
<td v-for="(bt,i) in meta.belongsTo" class="caption" :key="i">
<belongs-to-cell
:row="rowObj"
:value="rowObj[bt._rtn]"
:meta="meta"
:bt="bt"
:nodes="nodes"
@loadTableData="$emit('loadTableData')"
:api="api"
/>
</td>
<td v-for="(hm,i) in meta.hasMany" class="caption" :key="i">
<template v-if="rowObj[hm._tn]">
<has-many-cell
:row="rowObj"
:value="rowObj[hm._tn]"
:meta="meta"
:hm="hm"
:nodes="nodes"
@loadTableData="$emit('loadTableData')"
/>
</template>
</td>
<td v-for="(mm,i) in meta.manyToMany" class="caption" :key="i">
<many-to-many
:row="rowObj"
:value="rowObj[mm._rtn]"
:meta="meta"
:mm="mm"
:nodes="nodes"
@loadTableData="$emit('loadTableData')"
/>
</td>
</tr>
<tr v-if="!isLocked && !isPublicView && isEditable && relationType !== 'bt'">
<td :colspan="visibleColLength + 1" class="text-left pointer" @click="insertNewRow(true)">
<v-tooltip top>
<template v-slot:activator="{on}">
<v-icon v-on="on" small color="pink">mdi-plus</v-icon>
<span class="ml-1 caption grey--text ">New Row</span>
</template>
<span class="caption"> Add new row</span>
</v-tooltip>
</td>
</tr>
</tbody>
</table>
<div is="style" v-html="style"></div>
</div>
</template>
<script>
import HeaderCell from "@/components/project/spreadsheet/components/headerCell";
import EditableCell from "@/components/project/spreadsheet/components/editableCell";
import EditColumn from "@/components/project/spreadsheet/components/editColumn";
import TableCell from "@/components/project/spreadsheet/components/tableCell";
import colors from "@/mixins/colors";
import columnStyling from "@/components/project/spreadsheet/helpers/columnStyling";
import HasManyCell from "@/components/project/spreadsheet/components/editableCell/hasManyCell";
import BelongsToCell from "@/components/project/spreadsheet/components/editableCell/belogsToCell";
import ManyToMany from "@/components/project/spreadsheet/components/editableCell/manyToMany";
export default {
components: {ManyToMany, BelongsToCell, HasManyCell, TableCell, EditColumn, EditableCell, HeaderCell},
mixins: [colors],
props: {
relationType: String,
availableColumns: [Object, Array],
showFields: Object,
sqlUi: [Object, Function],
api: [Object, Function],
isEditable: Boolean,
nodes: Object,
primaryValueColumn: String,
belongsTo: [Object, Array],
hasMany: [Object, Array],
data: [Array, Object],
meta: Object,
visibleColLength: [Number, String],
isPublicView: Boolean,
table: String,
isVirtual: Boolean,
isLocked: Boolean,
columnsWidth: {type: Object}
},
mounted() {
this.calculateColumnWidth();
},
methods: {
calculateColumnWidth() {
setTimeout(() => {
const obj = {};
this.meta && this.meta.columns && this.meta.columns.forEach(c => {
obj[c._cn] = columnStyling[c.uidt] && columnStyling[c.uidt].w || undefined;
})
Array.from(this.$el.querySelectorAll('th')).forEach(el => {
const width = el.getBoundingClientRect().width;
obj[el.dataset.col] = obj[el.dataset.col] || ((width < 100 ? 100 : width) + 'px');
});
this.$emit('update:columnsWidth', {...obj, ...(this.columnWidth || {})})
}, 500)
},
isCentrallyAligned(col) {
return !['SingleLineText',
'LongText',
'Attachment',
'Date',
'Time',
'Email',
'URL',
'DateTime',
'CreateTime',
'LastModifiedTime'].includes(col.uidt)
},
async xcAuditModelCommentsCount() {
if (this.isPublicView || !this.data || !this.data.length) return;
const aggCount = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
dbAlias: this.nodes.dbAlias
}, 'xcAuditModelCommentsCount', {
model_name: this.meta._tn,
ids: this.data.map(({row: r}) => {
return this.meta.columns.filter((c) => c.pk).map(c => r[c._cn]).join('___')
})
}])
this.aggCount = aggCount;
},
onKeyDown(e) {
if (this.selected.col === null || this.selected.row === null) return;
switch (e.keyCode) {
// left
case 37:
if (this.selected.col > 0) this.selected.col--;
break;
// right
case 39:
if (this.selected.col < this.colLength - 1) this.selected.col++;
break;
// up
case 38:
if (this.selected.row > 0) this.selected.row--;
break;
// down
case 40:
if (this.selected.row < this.rowLength - 1) this.selected.row++;
break;
// enter
case 13:
this.makeEditable(this.selected.col, this.selected.row)
break;
}
},
onNewColCreation() {
this.addNewColMenu = false;
this.addNewColModal = false;
this.$emit('onNewColCreation')
},
expandRow(...args) {
this.$emit('expandRow', ...args)
},
showRowContextMenu($event, rowObj, rowMeta, row) {
this.$emit('showRowContextMenu', $event, rowObj, rowMeta, row)
},
onCellValueChange(col, row, column) {
this.$emit('onCellValueChange', col, row, column)
},
addNewRelationTab(...args) {
this.$emit('addNewRelationTab', ...args)
},
makeSelected(col, row) {
if (this.selected.col !== col || this.selected.row !== row) {
this.selected = {col, row};
this.editEnabled = {};
}
},
makeEditable(col, row) {
if (this.isPublicView || !this.isEditable) return;
if (this.availableColumns[col].ai) {
return this.$toast.info('Auto Increment field is not editable').goAway(3000)
}
if (this.availableColumns[col].pk && !this.data[row].rowMeta.new) {
return this.$toast.info('Editing primary key not supported').goAway(3000)
}
if (this.editEnabled.col !== col || this.editEnabled.row !== row) {
this.editEnabled = {col, row};
}
},
enableEditable(column) {
return (column && column.uidt === 'Attachment') ||
(column && column.uidt === 'SingleSelect') ||
(column && column.uidt === 'MultiSelect') ||
(column && column.uidt === 'DateTime') ||
(column && column.uidt === 'Date') ||
(column && column.uidt === 'Time') ||
(this.sqlUi && this.sqlUi.getAbstractType(column) === 'boolean');
},
insertNewRow(atEnd = false, expand = false) {
this.$emit('insertNewRow', atEnd, expand)
},
onresize(col, size) {
this.$emit('update:columnsWidth', {...this.columnsWidth, [col]: size});
}
},
computed: {
ids() {
return this.data.map(({oldRow}) => this.meta.columns.filter((c) => c.pk).map(c => oldRow[c._cn]).join('___'))
},
haveHasManyrelation() {
return !!Object.keys(this.hasMany).length;
},
colLength() {
return (this.availableColumns && this.availableColumns.length) || 0
},
// visibleColLength() {
// return (this.availableColumns && this.availableColumns.length) || 0
// },
rowLength() {
return (this.data && this.data.length) || 0
},
availColNames() {
return (this.availableColumns && this.availableColumns.map(c => c._cn)) || [];
},
groupedAggCount() {
return this.aggCount ? this.aggCount.reduce((o, {model_id, count}) => ({...o, [model_id]: count}), {}) : {};
},
style() {
let style = '';
for (const [key, val] of Object.entries(this.columnsWidth || {})) {
if (val && key !== this.resizingCol)
style += `
[data-col="${key}"]{
min-width: ${val};
max-width: ${val};
width: ${val};
}
`;
}
return style;
}
},
data: () => ({
resizingCol: null,
selectedExpandRowIndex: null,
selectedExpandRowMeta: null,
addNewColMenu: false,
selected: {
row: null,
col: null
},
editEnabled: {
row: null,
col: null
},
aggCount: []
}),
created() {
document.addEventListener('keydown', this.onKeyDown);
this.xcAuditModelCommentsCount();
},
beforeDestroy() {
document.removeEventListener('keydown', this.onKeyDown);
},
name: "xcGridView",
watch: {
data() {
this.xcAuditModelCommentsCount();
}
}
}
</script>
<style scoped lang="scss">
::v-deep {
.resizer:hover, .resizer:active, .resizer:focus {
background: var(--v-primary-base);
cursor: col-resize;
}
.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;
}
.v-input__control .v-input__slot .v-input--selection-controls__input {
transform: scale(.85);
margin-right: 0;
}
.xc-toolbar .v-input__slot, .navigation .v-input__slot {
box-shadow: none !important;
}
.navigation .v-input__slot input::placeholder {
font-size: .8rem;
}
.v-btn {
text-transform: capitalize;
}
.row-checkbox .v-input__control {
height: 24px !important;
}
tr:hover .xc-bt-chip {
margin-right: 0;
padding-right: 0 !important;
}
.xc-bt-chip {
margin-right: 24px;
transition: .4s margin-right, .4s padding-right;
}
.theme--light tbody tr:hover {
background: #eeeeee22;
}
.theme--dark tbody tr:hover {
background: #33333322;
}
.xc-border.search-box {
overflow: visible;
border-radius: 4px;
}
.xc-border.search-box .v-input {
transition: .4s border-color;
}
.xc-border.search-box .v-input--is-focused {
border: 1px solid var(--v-primary-base) !important;
margin: -1px;
}
.search-field.v-text-field.v-text-field--solo.v-input--dense > .v-input__control {
min-height: auto;
}
.view .view-icon {
display: none;
transition: .3s display;
}
.view:hover .view-icon {
display: inline-block;
}
.view:hover .check-icon {
display: none;
}
.v-input__control label {
font-size: inherit;
}
}
table, td, th.grey-border, th.grey.darken-3, th.grey.lighten-4 {
border-right: 1px solid #7f828b33 !important;
border-left: 1px solid #7f828b33 !important;
border-bottom: 2px solid #7f828b33 !important;
border-top: 2px solid #7f828b33 !important;
border-collapse: collapse;
}
td, th {
position: relative;
padding: 0 5px !important;
}
td.active::after, td.active::before {
content: '';
position: absolute;
z-index: 3;
height: calc(100% + 2px);
width: calc(100% + 2px);
left: -1px;
top: -1px;
pointer-events: none;
}
td.active::after {
border: 2px solid var(--v-primary-lighten1);
}
td.active::before {
background: var(--v-primary-base);
opacity: .1;
}
.xc-row-table thead tr {
background: transparent;
}
.xc-row-table thead th {
/*background: ;*/
position: sticky;
top: 0;
//left: 0;
z-index: 6;
}
.row-expand-icon, .row-checkbox {
opacity: 0;
}
tr:hover .row-expand-icon, tr:hover .row-checkbox, tr .row-checkbox.active {
opacity: 1;
}
tr:hover .row-no {
display: none;
}
td, tr {
min-height: 31px !important;
/*height: 31px !important;*/
/*max-height: 31px !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;*/
}
.belongsto-col {
text-decoration: underline;
}
.belongsto-col:hover {
color: var(--v-primary-base);
}
.hasmany-col-menu-icon {
opacity: 0;
transition: .4s opacity, .4s max-width;
}
.hasmany-col-menu-icon.pv {
/*display: none ;*/
max-width: 0;
overflow: hidden;
}
tr:hover .hasmany-col-menu-icon {
opacity: 1;
/*display: inline-block;*/
max-width: 24px;
}
tbody tr:nth-of-type(odd) {
background-color: transparent;
}
tbody tr:hover {
background-color: #dddddd33 !important;
}
.views-navigation-drawer {
transition: .4s max-width, .4s min-width;
}
.cell {
font-size: 13px;
}
th::before {
content: '';
position: absolute;
width: 100%;
background: inherit;
top: -2px;
height: 3px;
left: 0;
}
td.primary-column {
border-right-width: 2px !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: .2;
content: "";
z-index: 1;
pointer-events: none;
}
.new-column-header:hover {
background: linear-gradient(#ffffff33, #ffffff33);
}
.new-column-header {
text-align: center;
min-width: 70px;
}
.advance-menu-divider {
width: calc(100% - 26px);
margin-left: 13px;
border-style: dashed;
margin-top: 5px;
margin-bottom: 5px;
}
.sort-grid {
display: grid;
grid-template-columns:22px auto 100px;
column-gap: 6px;
row-gap: 6px;
}
.xc-row-table {
table-layout: fixed
}
td {
overflow: hidden;
}
th:first-child, td:first-child {
min-width: 100px;
}
.has-many-icon {
transform: rotate(90deg);
}
th {
min-width: 100px;
}
</style>