CreatBotMainsail/src/components/panels/HistoryListPanel.vue
2022-03-22 10:54:14 +01:00

1080 lines
44 KiB
Vue

<style>
.history-jobs-table th {
white-space: nowrap;
}
.history-jobs-table th.text-start {
padding-right: 0 !important;
}
</style>
<template>
<div>
<panel
:icon="mdiFileDocumentMultipleOutline"
:title="$t('History.PrintHistory')"
card-class="history-list-panel">
<v-card-text>
<v-row>
<v-col class="col-4 d-flex align-center">
<v-text-field
v-model="search"
:append-icon="mdiMagnify"
:label="$t('History.Search')"
single-line
outlined
clearable
hide-details
dense></v-text-field>
</v-col>
<v-col class="offset-4 col-4 d-flex align-center justify-end">
<template v-if="selectedJobs.length">
<v-btn
:title="$t('History.Delete')"
color="warning"
class="px-2 minwidth-0 ml-3"
@click="deleteSelectedDialog = true">
<v-icon>{{ mdiDelete }}</v-icon>
</v-btn>
</template>
<v-btn
:title="$t('History.TitleExportHistory')"
class="px-2 minwidth-0 ml-3"
@click="exportHistory">
<v-icon>{{ mdiDatabaseExportOutline }}</v-icon>
</v-btn>
<v-btn
:title="$t('History.TitleRefreshHistory')"
class="px-2 minwidth-0 ml-3"
@click="refreshHistory">
<v-icon>{{ mdiRefresh }}</v-icon>
</v-btn>
<v-menu :offset-y="true" :close-on-content-click="false" title="Setup current list">
<template #activator="{ on, attrs }">
<v-btn
class="px-2 minwidth-0 ml-3"
:title="$t('History.TitleSettings')"
v-bind="attrs"
v-on="on">
<v-icon>{{ mdiCog }}</v-icon>
</v-btn>
</template>
<v-list>
<template v-if="allPrintStatusArray.length">
<v-list-item
v-for="status of allPrintStatusArray"
:key="status.key"
class="minHeight36">
<v-checkbox
class="mt-0"
hide-details
:input-value="status.showInTable"
:label="`${status.displayName} (${status.value})`"
@change="changeStatusVisible(status)"></v-checkbox>
</v-list-item>
<v-divider></v-divider>
</template>
<v-list-item v-for="header of configHeaders" :key="header.key" class="minHeight36">
<v-checkbox
v-model="header.visible"
class="mt-0"
hide-details
:label="header.text"
@change="changeColumnVisible(header.value)"></v-checkbox>
</v-list-item>
</v-list>
</v-menu>
</v-col>
</v-row>
</v-card-text>
<v-divider class="mb-3"></v-divider>
<v-data-table
v-model="selectedJobs"
:items="jobs"
class="history-jobs-table"
:headers="filteredHeaders"
:options="options"
:custom-sort="sortFiles"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
:items-per-page.sync="countPerPage"
:footer-props="{
itemsPerPageText: $t('History.Jobs'),
itemsPerPageAllText: $t('History.AllJobs'),
itemsPerPageOptions: [10, 25, 50, 100, -1],
}"
item-key="job_id"
:search="search"
:custom-filter="advancedSearch"
mobile-breakpoint="0"
show-select>
<template slot="items" slot-scope="props">
<td v-for="header in filteredHeaders" :key="header.text" class="text-no-wrap">
{{ props.item[header.value] }}
</td>
</template>
<template slot="no-data">
<div class="text-center">{{ $t('History.Empty') }}</div>
</template>
<template #item="{ index, item, isSelected, select }">
<tr
:key="`${index} ${item.filename}`"
v-longpress:600="(e) => showContextMenu(e, item)"
:class="'file-list-cursor user-select-none ' + (item.exists ? '' : 'text--disabled')"
@contextmenu="showContextMenu($event, item)"
@click="clickRow(item)">
<td class="pr-0">
<v-simple-checkbox
v-ripple
:value="isSelected"
class="pa-0 mr-0"
@click.stop="select(!isSelected)"></v-simple-checkbox>
</td>
<td class="px-0 text-center" style="width: 32px">
<template v-if="!item.exists">
<v-icon class="text--disabled">{{ mdiFile }}-cancel</v-icon>
</template>
<template v-else-if="getSmallThumbnail(item) && getBigThumbnail(item)">
<v-tooltip top>
<template #activator="{ on, attrs }">
<vue-load-image>
<img
slot="image"
:src="getSmallThumbnail(item)"
width="32"
height="32"
v-bind="attrs"
v-on="on" />
<v-progress-circular
slot="preloader"
indeterminate
color="primary"></v-progress-circular>
<v-icon slot="error">{{ mdiFile }}</v-icon>
</vue-load-image>
</template>
<span><img :src="getBigThumbnail(item)" width="250" /></span>
</v-tooltip>
</template>
<template v-else-if="getSmallThumbnail(item)">
<vue-load-image>
<img slot="image" :src="getSmallThumbnail(item)" width="32" height="32" />
<v-progress-circular
slot="preloader"
indeterminate
color="primary"></v-progress-circular>
<v-icon slot="error">{{ mdiFile }}</v-icon>
</vue-load-image>
</template>
<template v-else>
<v-icon>{{ mdiFile }}</v-icon>
</template>
</td>
<td class=" ">{{ item.filename }}</td>
<td class="text-right text-no-wrap">
<template v-if="'note' in item && item.note">
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-icon small class="mr-2" v-bind="attrs" v-on="on">
{{ mdiNotebook }}
</v-icon>
</template>
<span v-html="item.note.replaceAll('\n', '<br />')"></span>
</v-tooltip>
</template>
<v-tooltip top>
<template #activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
<v-icon small :color="getStatusColor(item.status)" :disabled="!item.exists">
{{ getStatusIcon(item.status) }}
</v-icon>
</span>
</template>
<span>
{{
$te(`History.StatusValues.${item.status}`, 'en')
? $t(`History.StatusValues.${item.status}`)
: item.status.replace(/_/g, ' ')
}}
</span>
</v-tooltip>
</td>
<td
v-for="col in tableFields"
:key="col.value"
:class="col.outputType !== 'date' ? 'text-no-wrap' : ''">
{{ outputValue(col, item) }}
</td>
<td v-if="headers.find((header) => header.value === 'slicer').visible" class=" ">
{{ 'slicer' in item.metadata && item.metadata.slicer ? item.metadata.slicer : '--' }}
<small v-if="'slicer_version' in item.metadata && item.metadata.slicer_version">
<br />
{{ item.metadata.slicer_version }}
</small>
</td>
</tr>
</template>
</v-data-table>
</panel>
<v-menu v-model="contextMenu.shown" :position-x="contextMenu.x" :position-y="contextMenu.y" absolute offset-y>
<v-list>
<v-list-item @click="clickRow(contextMenu.item)">
<v-icon class="mr-1">{{ mdiTextBoxSearch }}</v-icon>
{{ $t('History.Details') }}
</v-list-item>
<v-list-item
v-if="'note' in contextMenu.item && contextMenu.item.note"
@click="editNote(contextMenu.item)">
<v-icon class="mr-1">{{ mdiNotebookEdit }}</v-icon>
{{ $t('History.EditNote') }}
</v-list-item>
<v-list-item v-else @click="createNote(contextMenu.item)">
<v-icon class="mr-1">{{ mdiNotebookPlus }}</v-icon>
{{ $t('History.AddNote') }}
</v-list-item>
<v-list-item
v-if="contextMenu.item.exists"
:disabled="printerIsPrinting || !klipperReadyForGui"
@click="startPrint(contextMenu.item)">
<v-icon class="mr-1">{{ mdiPrinter }}</v-icon>
{{ $t('History.Reprint') }}
</v-list-item>
<v-list-item @click="deleteJob(contextMenu.item)">
<v-icon class="mr-1">{{ mdiDelete }}</v-icon>
{{ $t('History.Delete') }}
</v-list-item>
</v-list>
</v-menu>
<v-dialog
v-model="detailsDialog.boolShow"
:max-width="600"
persistent
@keydown.esc="detailsDialog.boolShow = false">
<panel
:title="$t('History.JobDetails')"
:icon="mdiUpdate"
card-class="history-detail-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="detailsDialog.boolShow = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text class="px-0">
<overlay-scrollbars style="height: 350px" class="px-6">
<v-row>
<v-col>{{ $t('History.Filename') }}</v-col>
<v-col class="text-right">{{ detailsDialog.item.filename }}</v-col>
</v-row>
<template v-if="'metadata' in detailsDialog.item && 'size' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.Filesize') }}</v-col>
<v-col class="text-right">{{ formatFilesize(detailsDialog.item.metadata.size) }}</v-col>
</v-row>
</template>
<template v-if="'metadata' in detailsDialog.item && 'modified' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.LastModified') }}</v-col>
<v-col class="text-right">{{ formatDate(detailsDialog.item.metadata.modified) }}</v-col>
</v-row>
</template>
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.Status') }}</v-col>
<v-col class="text-right">
{{
$te(`History.StatusValues.${detailsDialog.item.status}`, 'en')
? $t(`History.StatusValues.${detailsDialog.item.status}`)
: detailsDialog.item.status
}}
</v-col>
</v-row>
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.StartTime') }}</v-col>
<v-col class="text-right">{{ formatDate(detailsDialog.item.start_time) }}</v-col>
</v-row>
<template v-if="'end_time' in detailsDialog.item && detailsDialog.item.end_time > 0">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.EndTime') }}</v-col>
<v-col class="text-right">{{ formatDate(detailsDialog.item.end_time) }}</v-col>
</v-row>
</template>
<template
v-if="'metadata' in detailsDialog.item && 'estimated_time' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.EstimatedTime') }}</v-col>
<v-col class="text-right">
{{ formatPrintTime(detailsDialog.item.metadata.estimated_time) }}
</v-col>
</v-row>
</template>
<template v-if="detailsDialog.item.print_duration > 0">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.PrintDuration') }}</v-col>
<v-col class="text-right">
{{ formatPrintTime(detailsDialog.item.print_duration) }}
</v-col>
</v-row>
</template>
<template v-if="detailsDialog.item.total_duration > 0">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.TotalDuration') }}</v-col>
<v-col class="text-right">
{{ formatPrintTime(detailsDialog.item.total_duration) }}
</v-col>
</v-row>
</template>
<template
v-if="'metadata' in detailsDialog.item && 'filament_total' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.EstimatedFilamentWeight') }}</v-col>
<v-col class="text-right">
{{ Math.round(detailsDialog.item.metadata.filament_weight_total * 100) / 100 }} g
</v-col>
</v-row>
</template>
<template
v-if="'metadata' in detailsDialog.item && 'filament_total' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.EstimatedFilament') }}</v-col>
<v-col class="text-right">
{{ Math.round(detailsDialog.item.metadata.filament_total) }} mm
</v-col>
</v-row>
</template>
<template v-if="detailsDialog.item.filament_used > 0">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.FilamentUsed') }}</v-col>
<v-col class="text-right">{{ Math.round(detailsDialog.item.filament_used) }} mm</v-col>
</v-row>
</template>
<template
v-if="
'metadata' in detailsDialog.item &&
'first_layer_extr_temp' in detailsDialog.item.metadata
">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.FirstLayerExtTemp') }}</v-col>
<v-col class="text-right">
{{ detailsDialog.item.metadata.first_layer_extr_temp }} °C
</v-col>
</v-row>
</template>
<template
v-if="
'metadata' in detailsDialog.item &&
'first_layer_bed_temp' in detailsDialog.item.metadata
">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.FirstLayerBedTemp') }}</v-col>
<v-col class="text-right">
{{ detailsDialog.item.metadata.first_layer_bed_temp }} °C
</v-col>
</v-row>
</template>
<template
v-if="
'metadata' in detailsDialog.item && 'first_layer_height' in detailsDialog.item.metadata
">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.FirstLayerHeight') }}</v-col>
<v-col class="text-right">
{{ detailsDialog.item.metadata.first_layer_height }} mm
</v-col>
</v-row>
</template>
<template
v-if="'metadata' in detailsDialog.item && 'layer_height' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.LayerHeight') }}</v-col>
<v-col class="text-right">{{ detailsDialog.item.metadata.layer_height }} mm</v-col>
</v-row>
</template>
<template
v-if="'metadata' in detailsDialog.item && 'object_height' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.ObjectHeight') }}</v-col>
<v-col class="text-right">{{ detailsDialog.item.metadata.object_height }} mm</v-col>
</v-row>
</template>
<template v-if="'metadata' in detailsDialog.item && 'slicer' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.Slicer') }}</v-col>
<v-col class="text-right">{{ detailsDialog.item.metadata.slicer }}</v-col>
</v-row>
</template>
<template
v-if="'metadata' in detailsDialog.item && 'slicer_version' in detailsDialog.item.metadata">
<v-divider class="my-3"></v-divider>
<v-row>
<v-col>{{ $t('History.SlicerVersion') }}</v-col>
<v-col class="text-right">{{ detailsDialog.item.metadata.slicer_version }}</v-col>
</v-row>
</template>
</overlay-scrollbars>
</v-card-text>
</panel>
</v-dialog>
<v-dialog v-model="deleteSelectedDialog" max-width="400">
<panel :title="$t('History.Delete')" card-class="history-delete-selected-dialog" :margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="deleteSelectedDialog = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<p class="mb-0">{{ $t('History.DeleteSelectedQuestion', { count: selectedJobs.length }) }}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="" text @click="deleteSelectedDialog = false">{{ $t('History.Cancel') }}</v-btn>
<v-btn color="error" text @click="deleteSelectedJobs">{{ $t('History.Delete') }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
<v-dialog v-model="noteDialog.boolShow" :max-width="600" persistent @keydown.esc="noteDialog.boolShow = false">
<panel
:title="noteDialog.type === 'create' ? $t('History.CreateNote') : $t('History.EditNote')"
:icon="noteDialog.type === 'create' ? mdiNotebookPlus : mdiNotebookEdit"
card-class="history-note-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="noteDialog.boolShow = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text class="pb-0">
<v-row>
<v-col>
<v-textarea
v-model="noteDialog.note"
outlined
hide-details
:label="$t('History.Note')"></v-textarea>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="" text @click="noteDialog.boolShow = false">{{ $t('History.Cancel') }}</v-btn>
<v-btn color="primary" text @click="saveNote">{{ $t('History.Save') }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</div>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { ServerHistoryStateJob } from '@/store/server/history/types'
import { caseInsensitiveSort, formatFilesize } from '@/plugins/helpers'
import Panel from '@/components/ui/Panel.vue'
import { thumbnailBigMin, thumbnailSmallMax, thumbnailSmallMin } from '@/store/variables'
import {
mdiDatabaseExportOutline,
mdiDelete,
mdiRefresh,
mdiCog,
mdiPrinter,
mdiTextBoxSearch,
mdiFile,
mdiFileDocumentMultipleOutline,
mdiMagnify,
mdiCloseThick,
mdiUpdate,
mdiNotebookEdit,
mdiNotebookPlus,
mdiNotebook,
} from '@mdi/js'
@Component({
components: { Panel },
})
export default class HistoryListPanel extends Mixins(BaseMixin) {
mdiDatabaseExportOutline = mdiDatabaseExportOutline
mdiDelete = mdiDelete
mdiRefresh = mdiRefresh
mdiCog = mdiCog
mdiPrinter = mdiPrinter
mdiFileDocumentMultipleOutline = mdiFileDocumentMultipleOutline
mdiTextBoxSearch = mdiTextBoxSearch
mdiFile = mdiFile
mdiMagnify = mdiMagnify
mdiUpdate = mdiUpdate
mdiCloseThick = mdiCloseThick
mdiNotebookPlus = mdiNotebookPlus
mdiNotebookEdit = mdiNotebookEdit
mdiNotebook = mdiNotebook
formatFilesize = formatFilesize
private search = ''
private sortBy = 'start_time'
private sortDesc = true
private options = {}
private contextMenu = {
shown: false,
touchTimer: undefined,
x: 0,
y: 0,
item: {},
}
private detailsDialog = {
item: {},
boolShow: false,
}
private noteDialog: {
item: ServerHistoryStateJob | null
note: string
boolShow: boolean
type: 'create' | 'edit'
} = {
item: null,
note: '',
boolShow: false,
type: 'create',
}
private deleteSelectedDialog = false
get jobs() {
return this.$store.getters['server/history/getFilterdJobList'] ?? []
}
get selectedJobs() {
return this.$store.state.gui.view.history.selectedJobs ?? []
}
set selectedJobs(newVal) {
this.$store.dispatch('gui/saveSettingWithoutUpload', { name: 'view.history.selectedJobs', value: newVal })
}
get headers() {
const headers = [
{
text: '',
value: '',
align: 'left',
configable: false,
visible: true,
filterable: false,
},
{
text: this.$t('History.Filename'),
value: 'filename',
align: 'left',
configable: false,
visible: true,
},
{
text: '',
value: 'status',
align: 'left',
configable: false,
visible: true,
filterable: false,
},
{
text: this.$t('History.Filesize'),
value: 'size',
align: 'left',
configable: true,
visible: true,
outputType: 'filesize',
},
{
text: this.$t('History.LastModified'),
value: 'modified',
align: 'left',
configable: true,
visible: true,
outputType: 'date',
},
{
text: this.$t('History.StartTime'),
value: 'start_time',
align: 'left',
configable: true,
visible: true,
outputType: 'date',
},
{
text: this.$t('History.EndTime'),
value: 'end_time',
align: 'left',
configable: true,
visible: true,
outputType: 'date',
},
{
text: this.$t('History.EstimatedTime'),
value: 'estimated_time',
align: 'left',
configable: true,
visible: true,
outputType: 'time',
},
{
text: this.$t('History.PrintTime'),
value: 'print_duration',
align: 'left',
configable: true,
visible: true,
outputType: 'time',
},
{
text: this.$t('History.TotalTime'),
value: 'total_duration',
align: 'left',
configable: true,
visible: true,
outputType: 'time',
},
{
text: this.$t('History.FilamentCalc'),
value: 'filament_total',
align: 'left',
configable: true,
visible: true,
outputType: 'length',
},
{
text: this.$t('History.FilamentUsed'),
value: 'filament_used',
align: 'left',
configable: true,
visible: true,
outputType: 'length',
},
{
text: this.$t('History.FirstLayerExtTemp'),
value: 'first_layer_extr_temp',
align: 'left',
configable: true,
visible: true,
outputType: 'temp',
},
{
text: this.$t('History.FirstLayerBedTemp'),
value: 'first_layer_bed_temp',
align: 'left',
configable: true,
visible: true,
outputType: 'temp',
},
{
text: this.$t('History.FirstLayerHeight'),
value: 'first_layer_height',
align: 'left',
configable: true,
visible: true,
outputType: 'length',
},
{
text: this.$t('History.LayerHeight'),
value: 'layer_height',
align: 'left',
configable: true,
visible: true,
outputType: 'length',
},
{
text: this.$t('History.ObjectHeight'),
value: 'object_height',
align: 'left',
configable: true,
visible: true,
outputType: 'length',
},
{
text: this.$t('History.Slicer'),
value: 'slicer',
align: 'left',
configable: true,
visible: true,
},
]
headers.forEach((header) => {
if (header.visible && this.hideColums.includes(header.value)) {
header.visible = false
} else if (!header.visible && !this.hideColums.includes(header.value)) {
header.visible = true
}
})
return headers
}
get tableFields() {
return this.filteredHeaders.filter(
(col: any) => !['filename', 'status', 'slicer'].includes(col.value) && col.value !== ''
)
}
get configHeaders() {
return this.headers.filter((header: any) => header.configable === true)
}
get filteredHeaders() {
return this.headers.filter((header: any) => header.visible === true)
}
get allPrintStatusArray() {
return caseInsensitiveSort(this.$store.getters['server/history/getAllPrintStatusArray'] ?? [], 'name')
}
get countPerPage() {
return this.$store.state.gui.view.historycountPerPage
}
set countPerPage(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.history.countPerPage', value: newVal })
}
get hideColums() {
return this.$store.state.gui.view.history.hideColums
}
set hideColums(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.history.hideColums', value: newVal })
}
get currentLanguage() {
return this.$store.state.gui.general?.language ?? 'en'
}
refreshHistory() {
this.$socket.emit('server.history.list', { start: 0, limit: 50 }, { action: 'server/history/getHistory' })
}
formatDate(date: number) {
const tmp2 = new Date(date * 1000)
return tmp2.toLocaleString().replace(',', '')
}
formatPrintTime(totalSeconds: number) {
if (totalSeconds) {
let output = ''
const days = Math.floor(totalSeconds / (3600 * 24))
if (days) {
totalSeconds %= 3600 * 24
output += days + 'd'
}
const hours = Math.floor(totalSeconds / 3600)
totalSeconds %= 3600
if (hours) output += ' ' + hours + 'h'
const minutes = Math.floor(totalSeconds / 60)
if (minutes) output += ' ' + minutes + 'm'
const seconds = totalSeconds % 60
if (seconds) output += ' ' + seconds.toFixed(0) + 's'
return output
}
return '--'
}
clickRow(item: ServerHistoryStateJob) {
this.detailsDialog.item = item
this.detailsDialog.boolShow = true
}
showContextMenu(e: any, item: ServerHistoryStateJob) {
if (!this.contextMenu.shown) {
e?.preventDefault()
this.contextMenu.shown = true
this.contextMenu.x = e?.clientX || e?.pageX || window.screenX / 2
this.contextMenu.y = e?.clientY || e?.pageY || window.screenY / 2
this.contextMenu.item = item
this.$nextTick(() => {
this.contextMenu.shown = true
})
}
}
sortFiles(items: any[], sortBy: string[], sortDesc: boolean[]) {
const sortByClean = sortBy.length ? sortBy[0] : 'filename'
const sortDescClean = sortDesc[0]
if (items !== undefined) {
// Sort by index
items.sort(function (a, b) {
if (a[sortByClean] === b[sortByClean]) return 0
if (a[sortByClean] === null || a[sortByClean] === undefined) return -1
if (b[sortByClean] === null || b[sortByClean] === undefined) return 1
if (a[sortByClean].constructor === String && b[sortByClean].constructor === String) {
return a[sortByClean].localeCompare(b[sortByClean], undefined, { sensivity: 'base' })
}
if (a[sortByClean] instanceof Array && b[sortByClean] instanceof Array) {
const reducedA = a[sortByClean].length ? a.filament.reduce((a: any, b: any) => a + b) : 0
const reducedB = b[sortByClean].length ? b.filament.reduce((a: any, b: any) => a + b) : 0
return reducedA - reducedB
}
return a[sortByClean] - b[sortByClean]
})
// Deal with descending order
if (sortDescClean) items.reverse()
}
return items
}
advancedSearch(value: string, search: string) {
return value != null && search != null && value.toString().toLowerCase().indexOf(search.toLowerCase()) !== -1
}
getSmallThumbnail(item: ServerHistoryStateJob) {
if ('metadata' in item && 'thumbnails' in item.metadata && item.metadata.thumbnails.length) {
const thumbnail = item.metadata.thumbnails.find(
(thumb: any) =>
thumb.width >= thumbnailSmallMin &&
thumb.width <= thumbnailSmallMax &&
thumb.height >= thumbnailSmallMin &&
thumb.height <= thumbnailSmallMax
)
let relative_url = ''
if (item.filename.lastIndexOf('/') !== -1) {
relative_url = item.filename.substr(0, item.filename.lastIndexOf('/') + 1)
}
if (thumbnail && 'relative_path' in thumbnail) {
return `${this.apiUrl}/server/files/gcodes/${encodeURI(
relative_url + thumbnail.relative_path
)}?timestamp=${item.metadata.modified}`
}
}
return false
}
getBigThumbnail(item: ServerHistoryStateJob) {
if ('metadata' in item && 'thumbnails' in item.metadata && item.metadata.thumbnails.length) {
const thumbnail = item.metadata.thumbnails.find((thumb: any) => thumb.width >= thumbnailBigMin)
let relative_url = ''
if (item.filename.lastIndexOf('/') !== -1) {
relative_url = item.filename.substr(0, item.filename.lastIndexOf('/') + 1)
}
if (thumbnail && 'relative_path' in thumbnail)
return `${this.apiUrl}/server/files/gcodes/${encodeURI(
relative_url + thumbnail.relative_path
)}?timestamp=${item.metadata.modified}`
}
return false
}
getThumbnailWidth(item: ServerHistoryStateJob) {
if (this.getBigThumbnail(item)) {
const thumbnail = item.metadata.thumbnails.find((thumb: any) => thumb.width >= thumbnailBigMin)
if (thumbnail) return thumbnail.width
}
return 400
}
changeColumnVisible(name: string) {
if (this.headers.filter((header) => header.value === name).length) {
let value = this.headers.filter((header) => header.value === name)[0].visible
this.$store.dispatch('gui/setHistoryColumns', { name: name, value: value })
}
}
changeStatusVisible(status: any) {
if (status.showInTable) this.$store.dispatch('gui/hideStatusInHistoryList', status.name)
else this.$store.dispatch('gui/showStatusInHistoryList', status.name)
}
startPrint(item: ServerHistoryStateJob) {
if (item.exists)
this.$socket.emit('printer.print.start', { filename: item.filename }, { action: 'switchToDashboard' })
}
deleteJob(item: ServerHistoryStateJob) {
this.$socket.emit(
'server.history.delete_job',
{ uid: item.job_id },
{ action: 'server/history/getDeletedJobs' }
)
}
deleteSelectedJobs() {
this.selectedJobs.forEach((item: ServerHistoryStateJob) => {
this.$socket.emit(
'server.history.delete_job',
{ uid: item.job_id },
{ action: 'server/history/getDeletedJobs' }
)
})
this.selectedJobs = []
this.deleteSelectedDialog = false
}
exportHistory() {
const checkString = parseFloat('1.23').toLocaleString()
const decimalSeparator = checkString.indexOf(',') >= 0 ? ',' : '.'
const csvSeperator = decimalSeparator === ',' ? ';' : ','
const content: string[][] = []
const row: string[] = []
row.push('filename')
row.push('status')
this.tableFields.forEach((col) => {
row.push(col.value)
})
content.push(row)
if (this.jobs.length) {
this.jobs.forEach((job: ServerHistoryStateJob) => {
const row: string[] = []
let filename = job.filename
if (filename.includes(csvSeperator)) filename = '"' + filename + '"'
row.push(filename)
row.push(job.status)
this.tableFields.forEach((col) => {
row.push(this.outputValue(col, job, false, csvSeperator))
})
if (this.headers.find((header) => header.value === 'slicer')?.visible) {
let slicerString = 'slicer' in job.metadata && job.metadata.slicer ? job.metadata.slicer : '--'
if ('slicer_version' in job.metadata && job.metadata.slicer_version)
slicerString += ' ' + job.metadata.slicer_version
row.push(slicerString)
}
content.push(row)
})
}
const csvContent = 'data:text/csv;charset=utf-8,' + content.map((e) => e.join(csvSeperator)).join('\n')
const link = document.createElement('a')
link.setAttribute('href', encodeURI(csvContent))
link.setAttribute('download', 'print_history.csv')
document.body.appendChild(link)
link.click()
link.remove()
}
getStatusIcon(status: string) {
return this.$store.getters['server/history/getPrintStatusChipIcon'](status)
}
getStatusColor(status: string) {
return this.$store.getters['server/history/getPrintStatusChipColor'](status)
}
outputValue(col: any, item: any, format: boolean = true, escapeChar: string | null = null) {
let value = col.value in item ? item[col.value] : null
if (value === null) value = col.value in item.metadata ? item.metadata[col.value] : null
if (!format) {
switch (col.outputType) {
case 'date':
return this.formatDate(value)
case 'time':
return value.toFixed()
default:
switch (typeof value) {
case 'number':
return value.toLocaleString()
case 'string':
if (escapeChar !== null && value.includes(escapeChar)) value = '"' + value + '"'
return value
default:
return value
}
}
} else if (value > 0) {
switch (col.outputType) {
case 'filesize':
return formatFilesize(value)
case 'date':
return this.formatDate(value)
case 'time':
return this.formatPrintTime(value)
case 'temp':
return value.toFixed() + ' °C'
case 'length':
if (value > 1000) return (value / 1000).toFixed(2) + ' m'
return value.toFixed(2) + ' mm'
default:
return value
}
} else return '--'
}
createNote(item: ServerHistoryStateJob) {
this.noteDialog.item = item
this.noteDialog.note = ''
this.noteDialog.type = 'create'
this.noteDialog.boolShow = true
}
editNote(item: ServerHistoryStateJob) {
this.noteDialog.item = item
this.noteDialog.note = item.note ?? ''
this.noteDialog.type = 'edit'
this.noteDialog.boolShow = true
}
saveNote() {
this.$store.dispatch('server/history/saveHistoryNote', {
job_id: this.noteDialog.item?.job_id,
note: this.noteDialog.note,
})
this.noteDialog.boolShow = false
}
}
</script>