Files
raven-client/ayanova/src/components/data-table.vue
2021-06-23 23:40:09 +00:00

1336 lines
46 KiB
Vue

<template>
<div>
<gz-error :error-box-message="formState.errorBoxMessage"></gz-error>
<gz-data-table-filter
:data-list-key="dataListKey"
:active-filter-id="activeFilterId"
ref="dataTableFilter"
>
</gz-data-table-filter>
<gz-data-table-filter-manager
:data-list-key="dataListKey"
:active-filter-id="activeFilterId"
ref="dataTableFilterManager"
>
</gz-data-table-filter-manager>
<gz-data-table-mobile-filter-column-selector
:headers="headers"
ref="dataTableMobileFilterColumnSelector"
>
</gz-data-table-mobile-filter-column-selector>
<v-card>
<v-card-title>
<template v-if="preFilterMode">
<div class="my-2">
<v-btn text @click="preFilterNav()">
<v-icon data-cy="clickThru">{{ preFilterMode.icon }}</v-icon>
</v-btn>
<span @click="preFilterNav()" class="text-h5">
{{ preFilterMode.viz }}</span
>
<v-btn
v-if="preFilterMode.clearable"
text
@click="preFilterClear()"
>
<v-icon>$clear</v-icon>
</v-btn>
</div>
<!-- v-if="preFilterMode.ayatype && preFilterMode.id" {icon:null,viz:null,ayatype:null,id:null,clearable:false} -->
</template>
<template v-else>
<v-select
v-model="activeFilterId"
:items="selectLists.savedFilters"
item-text="name"
item-value="id"
:label="$ay.t('Filter')"
@input="selectedFilterChanged"
prepend-icon="$ayiEdit"
@click:prepend="editFilter()"
:append-outer-icon="clearFilterIcon()"
@click:append-outer="clearFilter()"
data-cy="selectSavedFilter"
>
</v-select>
</template>
<v-spacer></v-spacer>
<div class="my-5 my-sm-1">
<v-btn text @click="refresh">
<v-icon data-cy="refresh">$ayiSync</v-icon>
</v-btn>
<v-btn
text
v-if="$vuetify.breakpoint.xs"
class="ml-12"
@click="mobileColumnFilterSelect"
>
<v-icon data-cy="refresh">$ayiFilter</v-icon>
</v-btn>
<v-btn text class="ml-12" @click="editColumnView">
<v-icon
:large="hiddenAffectiveColumns.length > 0"
:color="hiddenAffectiveColumns.length ? 'accent' : null"
data-cy="filter"
>$ayiColumns</v-icon
>
</v-btn>
</div>
</v-card-title>
<div
class="text-h5 ml-3 accent--text"
v-if="!loading && records.length < 1"
>
{{ $ay.t("NoData") }}
</div>
<!--
################################################################################################################################################
WIDE TABLE VIEW
################################################################################################################################################
-->
<template v-if="$vuetify.breakpoint.smAndUp">
<v-data-table
:headers="headers"
:items="records"
v-model="selected"
:options.sync="dataTablePagingOptions"
:server-items-length="totalRecords"
:loading="loading"
multi-sort
:show-select="showSelect"
:single-select="singleSelect"
:footer-props="{
showCurrentPage: true,
showFirstLastPage: true,
itemsPerPageOptions: rowsPerPageItems,
itemsPerPageText: $ay.t('RowsPerPage'),
pageText: $ay.t('PageOfPageText')
}"
:loading-text="$ay.t('Loading')"
:no-data-text="$ay.t('NoData')"
class="elevation-1"
data-cy="datatable"
>
<!-- HEADER FILTERS: https://stackoverflow.com/a/58718975/8939 -->
<template v-for="h in headers" v-slot:[`header.${h.value}`]>
<span :key="h.text">
<v-btn
v-if="h.flt"
icon
@click.stop="filter(h)"
class="ml-n8 mr-1"
><v-icon :color="filterColor(h)">$ayiFilter</v-icon></v-btn
>
{{ h.text }}
</span>
</template>
<template v-slot:body="{ items }">
<tbody>
<tr v-for="item in items" :key="item.id">
<template v-if="showSelect">
<td>
<v-checkbox
v-model="selected"
:value="item"
hide-details
></v-checkbox>
</td>
</template>
<td
v-for="c in item.columns"
:key="c.key"
:style="cellStyle(c)"
>
<template v-if="c.t == 1">
<!-- DATETIME -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 2">
<!-- DATE -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 3">
<!-- TIME -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 4">
<!-- TEXT (also maybe openable)-->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 5">
<!-- INTEGER -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 6">
<!-- BOOL -->
<v-icon v-if="c.v === false" small>$ayiSquare</v-icon>
<v-icon v-else-if="c.v === true" small
>$ayiCheckSquare</v-icon
>
<v-icon v-else small>$ayiMinus-square</v-icon>
</template>
<template v-else-if="c.t == 7">
<!-- DECIMAL -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 8">
<!-- CURRENCY -->
{{ c.v }}
</template>
<template v-else-if="c.t == 9">
<!-- TAGS -->
{{ c.v }}
</template>
<template v-else-if="c.t == 10">
<!-- ENUM (translated to text on getdata) ALSO MAYBE OPENABLE -->
<template v-if="c.i && c.ot">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 11">
<!-- EMAIL -->
<a :href="'mailto:' + c.v">{{ c.v }}</a>
</template>
<template v-else-if="c.t == 12">
<!-- URL / HTTP -->
<!-- Expects full url with protocol etc in c.v so might need to add to record builder -->
<a :href="c.v" target="_blank">{{ c.v }}</a>
</template>
<template v-else-if="c.t == 14">
<!-- File / memory Size -->
{{ c.v }}
</template>
<template v-else-if="c.t == 16">
<!-- PHONE NUMBER -->
<a :href="'tel:' + c.v">{{ c.v }}</a>
</template>
<template v-else>
<!-- UNKNOWN -->
{{ c.v }}
</template>
</td>
</tr>
</tbody>
</template>
</v-data-table>
</template>
<!--
########################################################################
MOBILE TABLE VIEW
########################################################################
-->
<template v-else>
<v-data-table
:headers="headers"
:items="records"
v-model="selected"
:options.sync="dataTablePagingOptions"
:server-items-length="totalRecords"
:loading="loading"
multi-sort
:show-select="showSelect"
:single-select="singleSelect"
:footer-props="{
showCurrentPage: true,
showFirstLastPage: true,
itemsPerPageOptions: rowsPerPageItems,
itemsPerPageText: $ay.t('RowsPerPage'),
pageText: $ay.t('PageOfPageText')
}"
:header-props="{ sortByText: $ay.t('Sort') }"
:loading-text="$ay.t('Loading')"
class="elevation-1"
data-cy="datatable"
:no-data-text="$ay.t('NoData')"
>
<template v-slot:body="{ items }">
<v-row>
<v-col
v-for="item in items"
:key="item.id"
cols="12"
sm="6"
md="4"
lg="3"
>
<v-card elevation="4" tile>
<template v-if="showSelect">
<v-card-title class="subheading font-weight-bold">
<v-checkbox
v-model="selected"
:value="item"
hide-details
></v-checkbox>
</v-card-title>
<v-divider></v-divider>
</template>
<v-list dense>
<v-list-item
two-line
v-for="c in item.columns"
:key="c.key"
>
<v-list-item-content>
<v-list-item-title>
{{ getHeaderText(c.key) }}</v-list-item-title
>
<v-list-item-subtitle :style="cellStyle(c)">
<template v-if="c.t == 1">
<!-- DATETIME -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 2">
<!-- DATE -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 3">
<!-- TIME -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 4">
<!-- TEXT (also maybe openable)-->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 5">
<!-- INTEGER -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 6">
<!-- BOOL -->
<div>
<v-icon v-if="c.v === false" small
>$ayiSquare</v-icon
>
<v-icon v-else-if="c.v === true" small
>$ayiCheckSquare</v-icon
>
<v-icon v-else small>$ayiMinus-square</v-icon>
</div>
</template>
<template v-else-if="c.t == 7">
<!-- DECIMAL -->
<template v-if="c.i && c.i != 0 && !c.nopen">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 8">
<!-- CURRENCY -->
<div>{{ c.v }}</div>
</template>
<template v-else-if="c.t == 9">
<!-- TAGS -->
{{ c.v }}
</template>
<template v-else-if="c.t == 10">
<!-- ENUM (translated to text on getdata) ALSO MAYBE OPENABLE -->
<template v-if="c.i && c.ot">
<!-- openable object with an ID -->
<div
class="subtitle-1"
@click="gridCellButtonClick(c.key, c.i, c.ot)"
>
<a href="javascript:"> {{ c.v }}</a>
</div>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-else-if="c.t == 11">
<!-- EMAIL -->
<a :href="'mailto:' + c.v">{{ c.v }}</a>
</template>
<template v-else-if="c.t == 12">
<!-- URL / HTTP -->
<!-- Expects full url with protocol etc in c.v so might need to add to record builder -->
<a :href="c.v" target="_blank">{{ c.v }}</a>
</template>
<template v-else-if="c.t == 14">
<!-- File / memory Size -->
{{ c.v }}
</template>
<template v-else-if="c.t == 16">
<!-- PHONE NUMBER -->
<a :href="'tel:' + c.v">{{ c.v }}</a>
</template>
<template v-else>
<!-- UNKNOWN -->
{{ c.v }}
</template>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
</v-col>
</v-row>
</template>
</v-data-table>
</template>
</v-card>
</div>
</template>
<script>
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Xeslint-disable */
////////////////////////////////////////////////////////////////////////////////////////////////////////////
const MAX_TEXT_COLUMN_LENGTH = 1024;
export default {
data() {
return {
loading: true,
dataTablePagingOptions: {},
lastDataTablePagingOptions: {},
activeFilterId: 0, //<--0 signifies to select default as it's uninitialized
lastFetchFilterId: 0, //used to track change of filter and reset paging in getdata
selectLists: {
savedFilters: []
},
headers: [],
serverColumns: [],
serverFilter: [],
hiddenAffectiveColumns: [],
totalRecords: 0,
records: [],
rowsPerPageItems: [5, 10, 25, 50, 100],
selected: [],
timeZoneName: null,
formState: {
ready: false,
dirty: false,
valid: true,
readOnly: false,
loading: true,
errorBoxMessage: undefined,
appError: undefined,
serverError: {}
}
};
},
props: {
formKey: { type: String, default: null },
dataListKey: { type: String, default: null },
clientCriteria: {
type: String,
default: undefined
},
preFilterMode: {
//{icon:null,viz:null,ayatype:null,id:null,clearable:false}
type: Object,
default: null
},
showSelect: {
type: Boolean,
default: false
},
singleSelect: {
type: Boolean,
default: false
},
reload: {
type: Boolean,
default: false
},
ridColumnOpenable: {
type: Boolean,
default: true
}
},
watch: {
dataTablePagingOptions: {
async handler() {
//{ "page": 1, "itemsPerPage": 10, "sortBy": [], "sortDesc": [], "groupBy": [], "groupDesc": [], "mustSort": false, "multiSort": false }
//this code works around some weird bug that causes visible items to be selected in grid (only, not in actual selected array, just a visual thing)
// when breakpoint is switched between wide and narrow either way. No idea why it happens but this fixes that issue and also ensures that there are no
//spurious fetches happening just because the view has changed
//See what has changed and record it for processing
let sortHasChanged = !(
window.$gz.util.isEqualArraysOfPrimitives(
this.dataTablePagingOptions.sortBy,
this.lastDataTablePagingOptions.sortBy
) &&
window.$gz.util.isEqualArraysOfPrimitives(
this.dataTablePagingOptions.sortDesc,
this.lastDataTablePagingOptions.sortDesc
)
);
let pagingHaschanged = !(
this.lastDataTablePagingOptions.page ==
this.dataTablePagingOptions.page &&
this.lastDataTablePagingOptions.itemsPerPage ==
this.dataTablePagingOptions.itemsPerPage
);
if (!pagingHaschanged && !sortHasChanged) {
return;
}
//something changed, persist it to last
this.lastDataTablePagingOptions = this.dataTablePagingOptions;
if (sortHasChanged && !this.loading) {
//update sort at server and then allow get data
//no return data is required here
//because getdatafromapi will have the data to refresh the sort of the view anyway
//so it's fire and forget
await window.$gz.api.post("data-list-column-view/sort", {
listKey: this.dataListKey,
sortBy: this.keyArrayFromSortByArray(
this.dataTablePagingOptions.sortBy
),
sortDesc: this.dataTablePagingOptions.sortDesc
});
}
//has changed something important so refetch
await this.getDataFromApi();
},
deep: true
},
selected: function(newValue, oldValue) {
if (!newValue) {
return;
}
if (newValue.length != oldValue.length) {
this.handleSelectChange();
}
},
reload: function(newValue, oldValue) {
if (newValue != oldValue) {
this.getDataFromApi(true);
}
}
},
methods: {
preFilterNav: function() {
window.$gz.eventBus.$emit("openobject", {
type: this.preFilterMode.ayatype,
id: this.preFilterMode.id
});
},
preFilterClear: function() {
this.$emit("clear-pre-filter");
// this.preFilterMode = null;
// this.clientCriteria = null;
// getDataFromApi();
},
//Used by narrow view to get the "header" text for a column based on the column key
getHeaderText(key) {
//key format is row-column e.g."500-2"
let columnIndex = key.split("-")[1];
let header = this.headers[columnIndex];
if (header && header.text) {
return header.text;
}
return "";
},
async filter(item) {
let res = await this.$refs.dataTableFilter.open(item);
if (res && res.refresh == true) {
this.getDataFromApi();
}
},
filterColor(item) {
let clr = "disabled";
if (this.serverFilter.findIndex(z => z.column == item.fk) > -1) {
clr = "primary";
}
return clr;
},
async clearFilter() {
//Reset back to DEFAULT filter
setActiveFilter(this); //will not trigger refresh yet
//Call api method to clear this filter
let res = await window.$gz.api.remove(
`data-list-filter/${this.activeFilterId}`
);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
await fetchSavedFilterList(this);
await this.getDataFromApi();
}
},
clearFilterIcon() {
if (this.serverFilter && this.serverFilter.length > 0) {
return "$clear";
}
return null;
},
async editFilter() {
let res = await this.$refs.dataTableFilterManager.open();
if (res && res.refresh == true) {
if (res.newFilterId) {
//save as new filter, select it
this.activeFilterId = res.newFilterId;
}
//refresh the filter picklist and get data from api
await fetchSavedFilterList(this);
await this.getDataFromApi();
}
},
async mobileColumnFilterSelect() {
let resHeaderItem = await this.$refs.dataTableMobileFilterColumnSelector.open(
this.headers,
this.filterColor
);
if (resHeaderItem) {
await this.filter(resHeaderItem);
}
},
keyArrayFromSortByArray(sortBy) {
return sortBy.map(sortItem => {
let val = this.headers.find(z => z.value == sortItem);
if (val) {
return val.fk;
}
});
},
setSortIndicatorsFromDataListResponse(rsort) {
let sortBy = [];
let sortDesc = [];
if (rsort != null) {
Object.keys(rsort).forEach((key, index) => {
//Pull column header name "value" from "fk"matching "key" here from this.headers columns.c0 etc here from this.headers see above method
let found = this.headers.find(z => z.fk == key);
if (found != null) {
sortBy.push(found.value);
//if not null then push into the sortBy array
let sort = rsort[key];
if (sort == "-") {
sortDesc.push(true);
} else {
sortDesc.push(false);
}
}
});
}
this.dataTablePagingOptions.sortBy = [...sortBy];
this.dataTablePagingOptions.sortDesc = [...sortDesc];
},
refresh() {
this.getDataFromApi();
},
getDataListSelection(ayaType) {
let vm = this;
//called when parent form needs the selected id's or the list view options needed to rehydrate the entire list of id's in the same order and filter
//i.e. for reporting, batch operations etc
let selectedRowIds = [];
//get selected row id's if there are selections else get all row id's
//called into from parent for things like reporting or batch ops
//selected or records both are arrays with an id property that contains the row id conveniently for this method
if (vm.selected.length > 0) {
//return row id's of selected items only
selectedRowIds = vm.selected.map(z => {
return z.id;
});
selectedRowIds.reverse();
}
return {
AType: ayaType,
selectedRowIds: selectedRowIds,
dataListKey: vm.dataListKey,
filterId: vm.activeFilterId,
clientCriteria: vm.clientCriteria,
clientTimeStamp: window.$gz.locale.clientLocalZoneTimeStamp(
vm.timeZoneName
)
};
},
handleSelectChange() {
//SINGLESELECT MODE?
//due to making own template for items need to handle singleselect which only affects if select all checkbox at top is visible when making own item template
if (this.singleSelect) {
this.selected.splice(0, this.selected.length - 1);
}
//emit event to parent form of selected rows
this.$emit(
"selection-change",
this.selected.map(z => z.id)
);
},
editColumnView() {
this.$router.push({
name: "ay-data-list-column-view",
params: {
dataListKey: this.dataListKey,
hiddenAffectiveColumns: this.hiddenAffectiveColumns
// ,
// formKey: this.formKey,
// activeFilterId: this.activeFilterId
}
});
},
selectedFilterChanged: async function() {
await this.getDataFromApi();
},
gridCellButtonClick(key, i, ot) {
let typeToOpen = null;
//if ot is not null or undefined then *that* is the Openable Type otherwise it's in the header data
if (ot) {
typeToOpen = ot;
} else {
//translate key to actual object type from header data
//key format is row-column e.g."500-2"
//get the datatype of the column which matches the server columns array index
typeToOpen = this.serverColumns[key.split("-")[1]].ay;
}
//i is the actual AyaNova index of vm record so we have all we need to open vm object
window.$gz.eventBus.$emit("openobject", { type: typeToOpen, id: i });
},
cellStyle(c) {
//does it have additional styling?
//c.cst is styling added here by data builder in the form of an object with props set for compatibility with Vue
//c.clr is a specific color coming from the server for items that support colors (reminders etc)
if (c.cst || c.clr) {
let clrBit = {};
if (c.clr) {
if (c.clr[0] != "#") {
c.clr = "#" + c.clr;
}
clrBit["border-left"] = `5px solid ${c.clr}`; //warning: don't put a semicolon after any of these styles, it will then ignore the whole bit
}
return { ...c.cst, ...clrBit };
} else {
return null;
}
},
async getDataFromApi(deSelectAll) {
let vm = this;
if (vm.loading) {
return;
}
//start with defaults
let listOptions = {
DataListKey: vm.dataListKey,
Limit: 5,
Offset: 0
};
//has filter changed?
if (vm.lastFetchFilterId != vm.activeFilterId) {
//yes, go back to page one
vm.dataTablePagingOptions.page = 1;
vm.lastFetchFilterId = vm.activeFilterId;
}
//calculate paging based on settings
const { page, itemsPerPage } = vm.dataTablePagingOptions;
if (itemsPerPage && itemsPerPage > 0) {
listOptions.Offset = (page - 1) * itemsPerPage;
listOptions.Limit = itemsPerPage;
}
vm.loading = true;
//Weird bug that causes grid to show all items selected after async method runs below to fetch data
//this puts a pin in it then resets it after the fetch is completed
let preSelected = [];
if (!deSelectAll) {
//preserve selections
preSelected = [...vm.selected];
}
try {
let res = await window.$gz.api.post("data-list", {
offset: listOptions.Offset,
limit: listOptions.Limit,
dataListKey: vm.dataListKey,
filterId: vm.activeFilterId,
clientCriteria: vm.clientCriteria,
clientTimeStamp: window.$gz.locale.clientLocalZoneTimeStamp(
vm.timeZoneName
)
});
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
//Save a copy of the server columns data for handling button clicks etc later
vm.serverColumns = res.columns;
//save a copy of the filter to show the filter symbol icons beside column headers
//and know that the return list *is* filtered
vm.serverFilter = res.filter;
//Hidden columns that affect the query and data displayed
vm.hiddenAffectiveColumns = res.hiddenAffectiveColumns;
//Make sure the translation keys are fetched
await fetchTranslatedHeaderNames(res.columns); //Note can use await here because it's wrapped inside an async function call, it will wait then resume next stuff below
await fetchEnums(res.columns);
//build vm.headers here
vm.headers = buildHeaders(res.columns);
//SET TABLE SORT HERE FROM RESPONSE
vm.setSortIndicatorsFromDataListResponse(res.sortBy);
//Post process data here and then set vm.records
vm.records = buildRecords(
res.data,
res.columns,
vm.ridColumnOpenable
);
vm.totalRecords = res.totalRecordCount;
//Put back selected items
vm.selected = [...preSelected];
//persist the paging options and filter stuff so user sees same page and list on refresh or leave and return scenario
saveFormSettings(vm);
}
} catch (err) {
window.$gz.errorHandler.handleFormError(err, vm);
} finally {
//Note: this needs to be called in NextTick because otherwise it triggers a re-fetch when the sort indicators were set above
vm.$nextTick(() => {
vm.loading = false;
vm.formState.ready = true;
});
}
}
},
async created() {
//get pick lists
let vm = this;
await initForm(vm);
//rehydrate last form settings
//loadFormSettings(vm);
vm.loading = false;
vm.getDataFromApi();
}
};
//Called by getDataFromApi on retrieval of list with columnData
function buildHeaders(columnData) {
//iterate columns, build headers and return
if (!columnData) {
return [];
}
let ret = [];
//iterate the columns
for (let i = 0; i < columnData.length; i++) {
let cm = columnData[i];
let h = {};
h.text = window.$gz.translation.get(cm.cm);
h.fk = cm.fk;
h.value = "columns.c" + i.toString(); //+".v";
if (i == 0) {
h.align = "start";
}
//sort and filter, note server only sends a value if a column is *not* filterable or sortable to save bandwidth since most are
//this just reverses that so that it's easier to check for
//Not filterable?
if (!cm.nf) {
//yes, is filterable
h.flt = true;
}
//Sortable?
if (cm.ns) {
h.sortable = false;
}
ret.push(h);
}
return ret;
}
/////////////////////////////////////////////////////////////////
//Called by getDataFromApi on retrieval of list with columnData
//
function buildRecords(listData, columndefinitions, ridColumnOpenable) {
//iterate data, build each object keyed with index name and display set to correct translated filter and then return
let ret = [];
if (!listData) {
return ret;
}
//cache display format stuff
let timeZoneName = window.$gz.locale.getResolvedTimeZoneName();
let languageName = window.$gz.locale.getResolvedLanguage();
let hour12 = window.$gz.store.state.userOptions.hour12;
let currencyName = window.$gz.locale.getCurrencyName();
//this will cache the first time it's required (if required)
let availableRoles = null;
//comes as an array of arrays, needs to leave as an array of objects representing each row
for (let iRow = 0; iRow < listData.length; iRow++) {
let row = listData[iRow];
//iterate row and build object representing row data keyed to index
//container object for row
//id will be set later when code below encounters the id column which could be in any position (or not at all) but is identified by it's rid property
let o = { id: undefined, columns: {} };
for (let iColumn = 0; iColumn < row.length; iColumn++) {
let column = row[iColumn];
//rowId?
if (column.rid) {
o.id = column.i;
}
let dataType = columndefinitions[iColumn].dt;
let display = column.v;
let cstStyle = null; //custom additional styling here in grid e.g. negative numbers etc
/*
public enum UiFieldDataType : int
{
NoType = 0,
DateTime = 1,
Date = 2,
Time = 3,
Text = 4,
Integer = 5,
Bool = 6,
Decimal = 7,
Currency = 8,
Tags = 9,
Enum = 10,
EmailAddress = 11,
HTTP = 12,
InternalId = 13,
MemorySize=14,
TimeSpan=15,
PhoneNumber=16//this is so client can dial directly,
Roles=17
}
*/
if (display != null) {
switch (dataType) {
case 1: //datetime format to shortdatetime
display = window.$gz.locale.utcDateToShortDateAndTimeLocalized(
display,
timeZoneName,
languageName,
hour12
);
if (!window.$gz.locale.dateIsPast(column.v)) {
cstStyle = { "font-weight": "bold" }; //warning, don't put a semicolon after the style or it will not work
}
break;
case 2: //date only
display = window.$gz.locale.utcDateToShortDateLocalized(
display,
timeZoneName,
languageName
);
if (!window.$gz.locale.dateIsPast(column.v)) {
cstStyle = { "font-weight": "bold" }; //warning, don't put a semicolon after the style or it will not work
}
break;
case 3: //time only
display = window.$gz.locale.utcDateToShortTimeLocalized(
display,
timeZoneName,
languageName,
hour12
);
break;
case 4: //text
// if (display.length > MAX_TEXT_COLUMN_LENGTH) {
// display = display.substring(0, MAX_TEXT_COLUMN_LENGTH) + "...";
// }
display = window.$gz.util.truncateString(
display,
MAX_TEXT_COLUMN_LENGTH
);
break;
case 5: //Integer
//display is already set, just some styling to do
if (window.$gz.util.isNegative(column.v)) {
cstStyle = { color: "#FF0000" }; //warning, don't put a semicolon after the style or it will not work
}
break;
case 7: //decimal
display = window.$gz.locale.decimalLocalized(display, languageName);
if (window.$gz.util.isNegative(column.v)) {
cstStyle = { color: "#FF0000" }; //warning, don't put a semicolon after the style or it will not work
}
break;
case 8: //currency
display = window.$gz.locale.currencyLocalized(
display,
languageName,
currencyName
);
if (window.$gz.util.isNegative(column.v)) {
cstStyle = { color: "#FF0000" }; //warning, don't put a semicolon after the style or it will not work
}
break;
case 9: //tags
if (display && display.length > 0) {
display = display.join(", ");
} else {
display = "";
}
break;
case 10: //enum
display = window.$gz.enums.get(
columndefinitions[iColumn].et,
display
);
break;
case 14: //MemorySize (file size)
display = window.$gz.locale.humanFileSize(
display,
languageName,
false,
2
);
break;
case 17: //Authorization Roles
if (availableRoles == null) {
availableRoles = window.$gz.enums.getSelectionList(
"AuthorizationRoles"
);
}
let roles = display;
let roleNames = [];
if (roles != null && roles != 0) {
for (let i = 0; i < availableRoles.length; i++) {
let role = availableRoles[i];
if (!!(roles & role.id)) {
roleNames.push(role.name);
}
}
}
display = roleNames.join(", ");
break;
default:
//do nothing, allow it to stay as is (checkbox, plain text etc)
}
}
//build the row column object vm will be used by the datatable
let columnObject = {
v: display,
t: dataType,
key: iRow + "-" + iColumn
};
//is the source dtalist field openable? If so it will have an i property set for it's ID and we already know the types to open from the column headers data
//so between the two we can make a clickable button in the grid vm triggers a function with the column index and the id and vm in turn will bubble up the event to open vm
//object
if (column.i) {
columnObject["i"] = column.i;
}
if (!ridColumnOpenable && column.rid) {
columnObject["nopen"] = true;
}
//Is there an overriding openable type already defined?
//(this is used by dynamic columns with different object type and id's that are openable like attachments and event logs and Review list etc)
if (column.ot) {
columnObject["ot"] = column.ot;
}
//rgba color to decorate field with
if (column.clr) {
columnObject["clr"] = column.clr;
}
//additional styling
if (cstStyle) {
columnObject["cst"] = cstStyle;
}
o.columns["c" + iColumn.toString()] = columnObject;
}
ret.push(o);
}
return ret;
}
//////////////////////////////////////////////////////////
//
// Ensures column names are present in translation table
//
async function fetchTranslatedHeaderNames(columnData) {
if (!columnData) {
return;
}
let headerKeys = [];
for (let i = 0; i < columnData.length; i++) {
let cm = columnData[i];
headerKeys.push(cm.cm);
}
//Now fetch all the keys
await window.$gz.translation.cacheTranslations(headerKeys);
}
//////////////////////////////////////////////////////////
//
// Ensures column enums are present in enums list in store
//
async function fetchEnums(columnData) {
if (!columnData) {
return;
}
let headerKeys = [];
for (let i = 0; i < columnData.length; i++) {
let cm = columnData[i];
if (cm.et) {
await window.$gz.enums.fetchEnumList(cm.et);
}
}
}
/////////////////////////////////
//
//
async function initForm(vm) {
vm.timeZoneName = window.$gz.locale.getResolvedTimeZoneName();
await fetchSavedFilterList(vm);
loadFormSettings(vm);
}
////////////////////
//
async function fetchSavedFilterList(vm) {
//http://localhost:7575/api/v8/data-list-filter/list?ListKey=TestWidgetDataList
let res = await window.$gz.api.get(
"data-list-filter/list?ListKey=" + vm.dataListKey
);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
vm.selectLists.savedFilters = res.data;
//confirm we still have the current active filter id
setActiveFilter(vm, vm.activeFilterId);
}
}
////////////////////
//
function saveFormSettings(vm) {
window.$gz.form.setFormSettings(vm.formKey, {
temp: { page: vm.dataTablePagingOptions.page },
saved: {
itemsPerPage: vm.dataTablePagingOptions.itemsPerPage,
dataTable: {
activeFilterId: vm.activeFilterId
}
}
});
}
////////////////////
//
//
function setActiveFilter(vm, desiredId) {
//Handle a change of filter, ensure it exists, if not then try to select default and if not that then just put in a zero
//if desiredId is falsey then try to pick the default
if (desiredId) {
if (vm.selectLists.savedFilters.find(z => z.id == desiredId)) {
vm.activeFilterId = desiredId;
return;
}
}
//no specific id so attempt to set to default
let dflt = vm.selectLists.savedFilters.find(z => z.default == true);
if (dflt) {
vm.activeFilterId = dflt.id;
return;
}
vm.activeFilterId = 0;
}
////////////////////
//
function loadFormSettings(vm) {
let formSettings = window.$gz.form.getFormSettings(vm.formKey);
//process SAVED formsettings
if (formSettings.saved) {
if (formSettings.saved.itemsPerPage) {
vm.dataTablePagingOptions.itemsPerPage = formSettings.saved.itemsPerPage;
}
//have a nonzero saved filter id
if (
formSettings.saved.dataTable.activeFilterId != null &&
formSettings.saved.dataTable.activeFilterId != 0
) {
setActiveFilter(vm, formSettings.saved.dataTable.activeFilterId);
}
}
//process TEMP form settings
if (formSettings.temp && formSettings.temp.page) {
vm.dataTablePagingOptions.page = formSettings.temp.page;
}
}
</script>