2
0
mirror of https://github.com/frappe/books.git synced 2025-01-10 18:24:40 +00:00

incr(ui): export wizard

This commit is contained in:
18alantom 2022-10-17 14:58:33 +05:30
parent ece213d69f
commit fb2b026e96
4 changed files with 292 additions and 103 deletions

View File

@ -1,7 +1,10 @@
<template> <template>
<div :class="[inputClasses, containerClasses]"> <div :class="[inputClasses, containerClasses]">
<label class="flex items-center"> <label
<div class="mr-3 text-gray-600 text-sm" v-if="showLabel && !labelRight"> class="flex items-center"
:class="spaceBetween ? 'justify-between' : ''"
>
<div class="mr-3" :class="labelClasses" v-if="showLabel && !labelRight">
{{ df.label }} {{ df.label }}
</div> </div>
<div <div
@ -63,7 +66,7 @@
@focus="(e) => $emit('focus', e)" @focus="(e) => $emit('focus', e)"
/> />
</div> </div>
<div class="ml-3 text-gray-600 text-sm" v-if="showLabel && labelRight"> <div class="ml-3" :class="labelClasses" v-if="showLabel && labelRight">
{{ df.label }} {{ df.label }}
</div> </div>
</label> </label>
@ -77,10 +80,15 @@ export default {
extends: Base, extends: Base,
emits: ['focus'], emits: ['focus'],
props: { props: {
spaceBetween: {
default: false,
type: Boolean,
},
labelRight: { labelRight: {
default: true, default: true,
type: Boolean, type: Boolean,
}, },
labelClass: String,
}, },
data() { data() {
return { return {
@ -90,11 +98,13 @@ export default {
}; };
}, },
computed: { computed: {
/* labelClasses() {
inputClasses() { if (this.labelClass) {
return this.getInputClassesFromProp([]); return this.labelClass;
}
return 'text-gray-600 text-sm';
}, },
*/
checked() { checked() {
return this.value; return this.value;
}, },

View File

@ -1,111 +1,280 @@
<template> <template>
<div id="exportWizard" class="modal-body"> <div>
<div class="ml-4 col-6 text-left"> <!-- Export Wizard Header -->
<input <FormHeader :form-title="label" :form-sub-title="t`Export Wizard`" />
id="select-cbox" <hr />
@change="toggleSelect"
class="form-check-input" <!-- Export Config -->
type="checkbox" <div class="grid grid-cols-3 p-4 gap-4">
v-model="selectAllFlag" <Check
v-if="configFields.useListFilters"
:df="configFields.useListFilters"
:space-between="true"
:show-label="true"
:label-right="false"
:value="useListFilters"
:border="true"
@change="(value: boolean) => (useListFilters = value)"
/>
<Select
v-if="configFields.exportFormat"
:df="configFields.exportFormat"
:value="exportFormat"
:border="true"
@change="(value: ExportFormat) => (exportFormat = value)"
/>
</div>
<hr />
<!-- Fields Selection -->
<div>
<!-- Field Selection Header -->
<button
class="flex justify-between items-center text-gray-600 p-4 w-full"
@click="showFieldSelection = !showFieldSelection"
> >
<label class="form-check-label bold ml-2" for="select-cbox">{{ "Select/Clear All" }}</label> <p class="text-sm">
</div> {{ t`${numSelected} fields selected` }}
<hr width="93%"> </p>
<div class="row ml-4 mb-4"> <feather-icon
<div v-for="column in columns" :key="column.id" class="form-check mt-2 col-6"> :name="showFieldSelection ? 'chevron-down' : 'chevron-up'"
<input :id="column.id" class="form-check-input" type="checkbox" v-model="column.checked"> class="w-4 h-4"
<label class="form-check-label" :for="column.id">{{ column.content }}</label> />
</button>
<!-- Field Selection Body -->
<hr v-if="showFieldSelection" />
<div
v-if="showFieldSelection"
class="max-h-96 overflow-auto custom-scroll"
>
<!-- Main Fields -->
<div class="p-4">
<h2 class="text-sm font-semibold text-gray-800">
{{ fyo.schemaMap[schemaName]?.label ?? schemaName }}
</h2>
<div class="grid grid-cols-3 border rounded-md mt-1">
<Check
v-for="ef of fields"
:label-class="
ef.fieldtype === 'Table'
? 'text-sm text-gray-600 font-semibold'
: 'text-sm text-gray-600'
"
:key="ef.fieldname"
:df="getField(ef)"
:show-label="true"
:value="ef.export"
@change="(value: boolean) => setExportFieldValue(ef, value)"
/>
</div>
</div>
<!-- Table Fields -->
<div
class="p-4"
v-for="efs of filteredTableFields"
:key="efs.fieldname"
>
<h2 class="text-sm font-semibold text-gray-800">
{{ fyo.schemaMap[efs.target]?.label ?? schemaName }}
</h2>
<div class="grid grid-cols-3 border rounded-md mt-1">
<Check
v-for="ef of efs.fields"
:key="ef.fieldname"
:df="getField(ef)"
:show-label="true"
:value="ef.export"
@change="(value: boolean) => setExportFieldValue(ef, value, efs.target)"
/>
</div>
</div>
</div> </div>
</div> </div>
<div class="row footer-divider mb-3">
<div class="col-12" style="border-bottom:1px solid #e9ecef"></div> <!-- Export Button -->
</div> <hr />
<div class="row"> <div class="p-4 flex justify-between items-center">
<div class="col-12 text-right"> <p class="text-gray-600 text-sm">{{ t`${numEntries} entries` }}</p>
<f-button primary @click="save">{{ 'Download CSV' }}</f-button> <Button type="primary" @click="exportData">{{ t`Export` }}</Button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script>
import FileSaver from 'file-saver'
export default { <script lang="ts">
props: ['title', 'rows', 'columnData'], import { t } from 'fyo';
import { Field, FieldTypeEnum, TargetField } from 'schemas/types';
import { fyo } from 'src/initFyo';
import { defineComponent } from 'vue';
import Button from './Button.vue';
import Check from './Controls/Check.vue';
import Select from './Controls/Select.vue';
import FormHeader from './FormHeader.vue';
interface ExportField {
fieldname: string;
fieldtype: FieldTypeEnum;
label: string;
export: boolean;
}
interface ExportTableField {
fieldname: string;
label: string;
target: string;
fields: ExportField[];
}
type ExportFormat = 'csv' | 'json';
interface ExportWizardData {
numEntries: number;
useListFilters: boolean;
exportFormat: ExportFormat;
showFieldSelection: boolean;
fields: ExportField[];
tableFields: ExportTableField[];
}
const excludedFieldTypes = [
FieldTypeEnum.AttachImage,
FieldTypeEnum.Attachment,
];
export default defineComponent({
props: {
schemaName: { type: String, required: true },
pageTitle: String,
},
data() { data() {
const fields = fyo.schemaMap[this.schemaName]?.fields ?? [];
const exportFields = getExportFields(fields);
const exportTableFields = getExportTableFields(fields);
return { return {
selectAllFlag: true, numEntries: 0,
columns: this.columnData useListFilters: true,
}; exportFormat: 'csv',
fields: exportFields,
tableFields: exportTableFields,
showFieldSelection: !false,
} as ExportWizardData;
}, },
methods: { methods: {
toggleSelect() { getField(ef: ExportField): Field {
this.columns = this.columns.map(column => { return {
return { fieldtype: 'Check',
id: column.id, label: ef.label,
content: column.content, fieldname: ef.fieldname,
checked: this.selectAllFlag };
}; },
getExportField(
fieldname: string,
target?: string
): ExportField | undefined {
let fields: ExportField[] | undefined;
if (!target) {
fields = this.fields;
} else {
fields = this.tableFields.find((f) => f.target === target)?.fields;
}
if (!fields) {
return undefined;
}
return fields.find((f) => f.fieldname === fieldname);
},
setExportFieldValue(ef: ExportField, value: boolean, target?: string) {
const field = this.getExportField(ef.fieldname, target);
if (!field) {
return;
}
field.export = value;
},
exportData() {
console.log('export clicked');
},
},
computed: {
label() {
if (this.pageTitle) {
return this.pageTitle;
}
return fyo.schemaMap?.[this.schemaName]?.label ?? '';
},
filteredTableFields() {
return this.tableFields.filter((f) => {
const ef = this.getExportField(f.fieldname);
return !!ef?.export;
}); });
}, },
close() { numSelected() {
this.$modal.hide(); return (
this.filteredTableFields.reduce(
(acc, f) => f.fields.filter((f) => f.export).length + acc,
0
) +
this.fields.filter(
(f) => f.fieldtype !== FieldTypeEnum.Table && f.export
).length
);
}, },
checkNoneSelected(columns) { configFields() {
for (let column of columns) { return {
if (column.checked) return false; useListFilters: {
} fieldtype: 'Check',
return true; label: t`Use List Filters`,
fieldname: 'useListFilters',
},
exportFormat: {
fieldtype: 'Select',
label: t`Export Format`,
fieldname: 'exportFormat',
options: [
{ value: 'json', label: 'JSON' },
{ value: 'csv', label: 'CSV' },
],
},
};
}, },
async save() { },
if (this.checkNoneSelected(this.columns)) { components: { FormHeader, Check, Select, Button },
alert( });
`No columns have been selected.\n` +
`Please select at least one column to perform export.` function getExportFields(fields: Field[]): ExportField[] {
); return fields
} else { .filter((f) => !f.computed && f.label)
let selectedColumnIds = this.columns.map(column => { .map((field) => {
if (column.checked) return column.id; const { fieldname, label } = field;
}); const fieldtype = field.fieldtype as FieldTypeEnum;
let selectedColumns = this.columnData.filter( return {
column => selectedColumnIds.indexOf(column.id) != -1 fieldname,
); fieldtype,
await this.exportData(this.rows, selectedColumns); label,
this.$modal.hide(); export: !excludedFieldTypes.includes(fieldtype),
} };
}, });
async exportData(rows = this.rows, columns = this.columnData) { }
let title = this.title;
let columnNames = columns.map(column => column.content).toString(); function getExportTableFields(fields: Field[]): ExportTableField[] {
let columnIDs = columns.map(column => column.id); return fields
let rowData = rows.map(row => columnIDs.map(id => row[id]).toString()); .filter((f) => f.fieldtype === FieldTypeEnum.Table)
let csvDataArray = [columnNames, ...rowData]; .map((f) => {
let csvData = csvDataArray.join('\n'); const target = (f as TargetField).target;
let d = new Date(); const tableFields = fyo.schemaMap[target]?.fields ?? [];
let fileName = [ const exportTableFields = getExportFields(tableFields);
title.replace(/\s/g, '-'),
[d.getDate(), d.getMonth(), d.getFullYear()].join('-'), return {
`${d.getTime()}.csv` fieldname: f.fieldname,
].join('_'); label: f.label,
var blob = new Blob([csvData], { type: 'text/plain;charset=utf-8' }); target,
await FileSaver.saveAs(blob, fileName); fields: exportTableFields,
} };
} })
}; .filter((f) => !!f.fields.length);
}
</script> </script>
<style scoped>
.fixed-btn-width {
width: 5vw !important;
}
.bold {
font-size: 1.1rem;
font-weight: 600;
}
#select-cbox {
width: 15px;
height: 15px;
}
#exportWizard {
overflow: hidden;
}
</style>

View File

@ -16,7 +16,7 @@
v-if="openModal" v-if="openModal"
> >
<div <div
class="bg-white rounded-lg shadow-2xl w-form" class="bg-white rounded-lg shadow-2xl w-form border overflow-hidden"
v-bind="$attrs" v-bind="$attrs"
@click.stop @click.stop
> >

View File

@ -1,6 +1,9 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<PageHeader :title="title"> <PageHeader :title="title">
<Button :icon="false" @click="openExportModal = true">
{{ t`Export` }}
</Button>
<FilterDropdown <FilterDropdown
ref="filterDropdown" ref="filterDropdown"
@change="applyFilter" @change="applyFilter"
@ -24,11 +27,16 @@
class="flex-1 flex h-full" class="flex-1 flex h-full"
@makeNewDoc="makeNewDoc" @makeNewDoc="makeNewDoc"
/> />
<Modal :open-modal="openExportModal" @closemodal="openExportModal = false">
<ExportWizard :schema-name="schemaName" :title="pageTitle" />
</Modal>
</div> </div>
</template> </template>
<script> <script>
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import ExportWizard from 'src/components/ExportWizard.vue';
import FilterDropdown from 'src/components/FilterDropdown.vue'; import FilterDropdown from 'src/components/FilterDropdown.vue';
import Modal from 'src/components/Modal.vue';
import PageHeader from 'src/components/PageHeader.vue'; import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
@ -47,9 +55,11 @@ export default {
List, List,
Button, Button,
FilterDropdown, FilterDropdown,
Modal,
ExportWizard,
}, },
data() { data() {
return { listConfig: undefined }; return { listConfig: undefined, openExportModal: !false };
}, },
async activated() { async activated() {
if (typeof this.filters === 'object') { if (typeof this.filters === 'object') {