mirror of
https://github.com/frappe/books.git
synced 2024-11-08 23:00:56 +00:00
incr(ui): export wizard
This commit is contained in:
parent
ece213d69f
commit
fb2b026e96
@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div :class="[inputClasses, containerClasses]">
|
||||
<label class="flex items-center">
|
||||
<div class="mr-3 text-gray-600 text-sm" v-if="showLabel && !labelRight">
|
||||
<label
|
||||
class="flex items-center"
|
||||
:class="spaceBetween ? 'justify-between' : ''"
|
||||
>
|
||||
<div class="mr-3" :class="labelClasses" v-if="showLabel && !labelRight">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
<div
|
||||
@ -63,7 +66,7 @@
|
||||
@focus="(e) => $emit('focus', e)"
|
||||
/>
|
||||
</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 }}
|
||||
</div>
|
||||
</label>
|
||||
@ -77,10 +80,15 @@ export default {
|
||||
extends: Base,
|
||||
emits: ['focus'],
|
||||
props: {
|
||||
spaceBetween: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
labelRight: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
labelClass: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -90,11 +98,13 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/*
|
||||
inputClasses() {
|
||||
return this.getInputClassesFromProp([]);
|
||||
labelClasses() {
|
||||
if (this.labelClass) {
|
||||
return this.labelClass;
|
||||
}
|
||||
|
||||
return 'text-gray-600 text-sm';
|
||||
},
|
||||
*/
|
||||
checked() {
|
||||
return this.value;
|
||||
},
|
||||
|
@ -1,111 +1,280 @@
|
||||
<template>
|
||||
<div id="exportWizard" class="modal-body">
|
||||
<div class="ml-4 col-6 text-left">
|
||||
<input
|
||||
id="select-cbox"
|
||||
@change="toggleSelect"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model="selectAllFlag"
|
||||
<div>
|
||||
<!-- Export Wizard Header -->
|
||||
<FormHeader :form-title="label" :form-sub-title="t`Export Wizard`" />
|
||||
<hr />
|
||||
|
||||
<!-- Export Config -->
|
||||
<div class="grid grid-cols-3 p-4 gap-4">
|
||||
<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>
|
||||
</div>
|
||||
<hr width="93%">
|
||||
<div class="row ml-4 mb-4">
|
||||
<div v-for="column in columns" :key="column.id" class="form-check mt-2 col-6">
|
||||
<input :id="column.id" class="form-check-input" type="checkbox" v-model="column.checked">
|
||||
<label class="form-check-label" :for="column.id">{{ column.content }}</label>
|
||||
<p class="text-sm">
|
||||
{{ t`${numSelected} fields selected` }}
|
||||
</p>
|
||||
<feather-icon
|
||||
:name="showFieldSelection ? 'chevron-down' : 'chevron-up'"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</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>
|
||||
<div class="row footer-divider mb-3">
|
||||
<div class="col-12" style="border-bottom:1px solid #e9ecef"></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 class="row">
|
||||
<div class="col-12 text-right">
|
||||
<f-button primary @click="save">{{ 'Download CSV' }}</f-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Button -->
|
||||
<hr />
|
||||
<div class="p-4 flex justify-between items-center">
|
||||
<p class="text-gray-600 text-sm">{{ t`${numEntries} entries` }}</p>
|
||||
<Button type="primary" @click="exportData">{{ t`Export` }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import FileSaver from 'file-saver'
|
||||
|
||||
export default {
|
||||
props: ['title', 'rows', 'columnData'],
|
||||
<script lang="ts">
|
||||
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() {
|
||||
const fields = fyo.schemaMap[this.schemaName]?.fields ?? [];
|
||||
const exportFields = getExportFields(fields);
|
||||
const exportTableFields = getExportTableFields(fields);
|
||||
|
||||
return {
|
||||
selectAllFlag: true,
|
||||
columns: this.columnData
|
||||
};
|
||||
numEntries: 0,
|
||||
useListFilters: true,
|
||||
exportFormat: 'csv',
|
||||
fields: exportFields,
|
||||
tableFields: exportTableFields,
|
||||
showFieldSelection: !false,
|
||||
} as ExportWizardData;
|
||||
},
|
||||
methods: {
|
||||
toggleSelect() {
|
||||
this.columns = this.columns.map(column => {
|
||||
getField(ef: ExportField): Field {
|
||||
return {
|
||||
id: column.id,
|
||||
content: column.content,
|
||||
checked: this.selectAllFlag
|
||||
fieldtype: 'Check',
|
||||
label: ef.label,
|
||||
fieldname: ef.fieldname,
|
||||
};
|
||||
},
|
||||
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;
|
||||
});
|
||||
},
|
||||
numSelected() {
|
||||
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
|
||||
);
|
||||
},
|
||||
configFields() {
|
||||
return {
|
||||
useListFilters: {
|
||||
fieldtype: 'Check',
|
||||
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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
components: { FormHeader, Check, Select, Button },
|
||||
});
|
||||
|
||||
function getExportFields(fields: Field[]): ExportField[] {
|
||||
return fields
|
||||
.filter((f) => !f.computed && f.label)
|
||||
.map((field) => {
|
||||
const { fieldname, label } = field;
|
||||
const fieldtype = field.fieldtype as FieldTypeEnum;
|
||||
return {
|
||||
fieldname,
|
||||
fieldtype,
|
||||
label,
|
||||
export: !excludedFieldTypes.includes(fieldtype),
|
||||
};
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.$modal.hide();
|
||||
},
|
||||
checkNoneSelected(columns) {
|
||||
for (let column of columns) {
|
||||
if (column.checked) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async save() {
|
||||
if (this.checkNoneSelected(this.columns)) {
|
||||
alert(
|
||||
`No columns have been selected.\n` +
|
||||
`Please select at least one column to perform export.`
|
||||
);
|
||||
} else {
|
||||
let selectedColumnIds = this.columns.map(column => {
|
||||
if (column.checked) return column.id;
|
||||
});
|
||||
let selectedColumns = this.columnData.filter(
|
||||
column => selectedColumnIds.indexOf(column.id) != -1
|
||||
);
|
||||
await this.exportData(this.rows, selectedColumns);
|
||||
this.$modal.hide();
|
||||
}
|
||||
},
|
||||
async exportData(rows = this.rows, columns = this.columnData) {
|
||||
let title = this.title;
|
||||
let columnNames = columns.map(column => column.content).toString();
|
||||
let columnIDs = columns.map(column => column.id);
|
||||
let rowData = rows.map(row => columnIDs.map(id => row[id]).toString());
|
||||
let csvDataArray = [columnNames, ...rowData];
|
||||
let csvData = csvDataArray.join('\n');
|
||||
let d = new Date();
|
||||
let fileName = [
|
||||
title.replace(/\s/g, '-'),
|
||||
[d.getDate(), d.getMonth(), d.getFullYear()].join('-'),
|
||||
`${d.getTime()}.csv`
|
||||
].join('_');
|
||||
var blob = new Blob([csvData], { type: 'text/plain;charset=utf-8' });
|
||||
await FileSaver.saveAs(blob, fileName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getExportTableFields(fields: Field[]): ExportTableField[] {
|
||||
return fields
|
||||
.filter((f) => f.fieldtype === FieldTypeEnum.Table)
|
||||
.map((f) => {
|
||||
const target = (f as TargetField).target;
|
||||
const tableFields = fyo.schemaMap[target]?.fields ?? [];
|
||||
const exportTableFields = getExportFields(tableFields);
|
||||
|
||||
return {
|
||||
fieldname: f.fieldname,
|
||||
label: f.label,
|
||||
target,
|
||||
fields: exportTableFields,
|
||||
};
|
||||
})
|
||||
.filter((f) => !!f.fields.length);
|
||||
}
|
||||
</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>
|
||||
|
@ -16,7 +16,7 @@
|
||||
v-if="openModal"
|
||||
>
|
||||
<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"
|
||||
@click.stop
|
||||
>
|
||||
|
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<PageHeader :title="title">
|
||||
<Button :icon="false" @click="openExportModal = true">
|
||||
{{ t`Export` }}
|
||||
</Button>
|
||||
<FilterDropdown
|
||||
ref="filterDropdown"
|
||||
@change="applyFilter"
|
||||
@ -24,11 +27,16 @@
|
||||
class="flex-1 flex h-full"
|
||||
@makeNewDoc="makeNewDoc"
|
||||
/>
|
||||
<Modal :open-modal="openExportModal" @closemodal="openExportModal = false">
|
||||
<ExportWizard :schema-name="schemaName" :title="pageTitle" />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Button from 'src/components/Button.vue';
|
||||
import ExportWizard from 'src/components/ExportWizard.vue';
|
||||
import FilterDropdown from 'src/components/FilterDropdown.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
@ -47,9 +55,11 @@ export default {
|
||||
List,
|
||||
Button,
|
||||
FilterDropdown,
|
||||
Modal,
|
||||
ExportWizard,
|
||||
},
|
||||
data() {
|
||||
return { listConfig: undefined };
|
||||
return { listConfig: undefined, openExportModal: !false };
|
||||
},
|
||||
async activated() {
|
||||
if (typeof this.filters === 'object') {
|
||||
|
Loading…
Reference in New Issue
Block a user