2
0
mirror of https://github.com/frappe/books.git synced 2025-01-25 16:18:33 +00:00

feat: add assign imported labels bar

This commit is contained in:
18alantom 2022-02-22 14:13:56 +05:30
parent f230d779c9
commit be68ab84f8
2 changed files with 200 additions and 67 deletions

View File

@ -1,5 +1,6 @@
import { Field, FieldType } from '@/types/model'; import { Field, FieldType } from '@/types/model';
import frappe from 'frappe'; import frappe from 'frappe';
import { parseCSV } from './csvParser';
export const importable = [ export const importable = [
'SalesInvoice', 'SalesInvoice',
@ -11,25 +12,41 @@ export const importable = [
'Item', 'Item',
]; ];
type Exclusion = {
[key: string]: string[];
};
type Map = {
[key: string]: string;
};
interface TemplateField { interface TemplateField {
label: string; label: string;
fieldname: string; fieldname: string;
required: boolean; required: boolean;
} }
type LabelFieldMap = { const exclusion: Exclusion = {
[key: string]: string; Item: ['image'],
}; };
export function getTemplateFields(doctype: string): TemplateField[] { function getTemplateFields(doctype: string): TemplateField[] {
const fields: TemplateField[] = []; const fields: TemplateField[] = [];
if (!doctype) {
return [];
}
// @ts-ignore // @ts-ignore
const primaryFields: Field[] = frappe.models[doctype].fields; const primaryFields: Field[] = frappe.models[doctype].fields;
const tableTypes: string[] = []; const tableTypes: string[] = [];
let exclusionFields: string[] = exclusion[doctype] ?? [];
primaryFields.forEach( primaryFields.forEach(
({ label, fieldtype, childtype, fieldname, required }) => { ({ label, fieldtype, childtype, fieldname, required }) => {
if (exclusionFields.includes(fieldname)) {
return;
}
if (fieldtype === FieldType.Table && childtype) { if (fieldtype === FieldType.Table && childtype) {
tableTypes.push(childtype); tableTypes.push(childtype);
} }
@ -43,10 +60,15 @@ export function getTemplateFields(doctype: string): TemplateField[] {
); );
tableTypes.forEach((childtype) => { tableTypes.forEach((childtype) => {
exclusionFields = exclusion[childtype] ?? [];
// @ts-ignore // @ts-ignore
const childFields: Field[] = frappe.models[childtype].fields; const childFields: Field[] = frappe.models[childtype].fields;
childFields.forEach(({ label, fieldtype, fieldname, required }) => { childFields.forEach(({ label, fieldtype, fieldname, required }) => {
if (fieldtype === FieldType.Table) { if (
exclusionFields.includes(fieldname) ||
fieldtype === FieldType.Table
) {
return; return;
} }
@ -57,8 +79,8 @@ export function getTemplateFields(doctype: string): TemplateField[] {
return fields; return fields;
} }
function getLabelFieldMap(templateFields: TemplateField[]): LabelFieldMap { function getLabelFieldMap(templateFields: TemplateField[]): Map {
const map: LabelFieldMap = {}; const map: Map = {};
templateFields.reduce((acc, tf) => { templateFields.reduce((acc, tf) => {
const key = tf.label as string; const key = tf.label as string;
@ -77,21 +99,72 @@ function getTemplate(templateFields: TemplateField[]): string {
export class Importer { export class Importer {
doctype: string; doctype: string;
templateFields: TemplateField[]; templateFields: TemplateField[];
_map: LabelFieldMap; map: Map;
_template: string; template: string;
parsedLabels: string[] = [];
assignedMap: Map = {}; // target: import
constructor(doctype: string) { constructor(doctype: string) {
this.doctype = doctype; this.doctype = doctype;
this.templateFields = getTemplateFields(doctype); this.templateFields = getTemplateFields(doctype);
this._map = getLabelFieldMap(this.templateFields); this.map = getLabelFieldMap(this.templateFields);
this._template = getTemplate(this.templateFields); this.template = getTemplate(this.templateFields);
this.assignedMap = this.assignableLabels.reduce((acc: Map, k) => {
acc[k] = '';
return acc;
}, {});
} }
get map() { get assignableLabels() {
return this._map; return Object.keys(this.map);
} }
get template() { get unassignedLabels() {
return this._template; const assigned = Object.keys(this.assignedMap).map(
(k) => this.assignedMap[k]
);
return this.parsedLabels.filter((l) => !assigned.includes(l));
}
get columnsLabels() {
const assigned: string[] = [];
const unassigned: string[] = [];
Object.keys(this.map).forEach((k) => {
if (this.map) {
assigned.push(k);
return;
}
unassigned.push(k);
});
return [...assigned, ...unassigned];
}
selectFile(text: string): boolean {
const csv = parseCSV(text);
this.parsedLabels = csv[0];
const values = csv.slice(1);
if (values.some((v) => v.length !== this.parsedLabels.length)) {
return false;
}
this._setAssigned();
return true;
}
_setAssigned() {
const labels = [...this.parsedLabels];
labels.forEach((l) => {
if (this.assignedMap[l] !== '') {
return;
}
this.assignedMap[l] = l;
});
} }
} }
// @ts-ignore
window.pc = parseCSV;

View File

@ -7,62 +7,72 @@
</h1> </h1>
</template> </template>
<template #actions> <template #actions>
<DropdownWithActions
class="ml-2"
:actions="actions"
v-if="canCancel || importType"
/>
<Button <Button
v-if="canCancel" v-if="importType"
type="secondary" type="primary"
class="text-white text-xs ml-2" class="text-sm ml-2"
@click="cancel" @click="handlePrimaryClick"
>{{ primaryLabel }}</Button
> >
{{ t`Cancel` }}
</Button>
</template> </template>
</PageHeader> </PageHeader>
<div class="flex justify-center flex-1 mb-8 mt-2"> <!-- <div class="flex justify-center flex-1 mb-8 mt-2"> -->
<div <div class="flex px-8 mt-2 text-base w-full flex-col gap-8">
class=" <!-- Type selector -->
border <div class="flex flex-row justify-start items-center w-full">
rounded-lg <FormControl
shadow :df="importableDf"
h-full input-class="bg-gray-100 text-gray-900 text-base"
flex flex-col class="w-40"
justify-between :value="importType"
p-6 size="small"
" @change="setImportType"
style="width: 600px" />
> <p
<div> class="text-base text-base ml-2"
<div class="flex flex-row justify-between items-center"> :class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
<FormControl >
:df="importableDf" <span v-if="fileName" class="font-normal"
input-class="bg-gray-100 text-gray-900 text-base" >{{ t`Selected file` }}
class="w-1/4" </span>
:value="importType" {{ helperText }}{{ fileName ? ',' : '' }}
@change="setImportType" <span v-if="fileName" class="font-normal">
/> {{ t`verify imported data and click on` }} </span
<p >{{ ' ' }}<span v-if="fileName">{{ t`Import Data` }}</span
class="text-base text-base" >.
:class="fileName ? 'text-gray-900' : 'text-gray-700'" </p>
> </div>
{{ helperText }}
</p> <!-- Label Assigner -->
</div> <div v-if="fileName">
<hr class="mt-6" /> <h2 class="text-lg font-semibold">{{ t`Assign Imported Labels` }}</h2>
</div> <div class="gap-2 mt-4 grid grid-flow-col overflow-scroll pb-4">
<div v-if="importType"> <FormControl
<hr class="mb-6" /> :show-label="true"
<div class="flex flex-row justify-between text-base"> size="small"
<Button class="w-28"
class="w-1/4" input-class="bg-gray-100"
:padding="false" v-for="(f, k) in importer.assignableLabels"
@click="handleSecondaryClick" :df="getAssignerField(f)"
>{{ secondaryLabel }}</Button :value="importer.assignedMap[f] ?? ''"
> @change="(v) => onAssignedChange(f, v)"
<Button class="w-1/4" type="primary" @click="handlePrimaryClick">{{ :key="f + '-' + k"
primaryLabel />
}}</Button>
</div>
</div> </div>
</div> </div>
<!-- Data Verifier -->
<div v-if="fileName">
<h2 class="-mt-4 text-lg font-semibold">
{{ t`Verify Imported Data` }}
</h2>
<div></div>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -75,8 +85,9 @@ import Button from '@/components/Button.vue';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { IPC_ACTIONS } from '@/messages'; import { IPC_ACTIONS } from '@/messages';
import { getSavePath, saveData, showToast } from '@/utils'; import { getSavePath, saveData, showToast } from '@/utils';
import DropdownWithActions from '@/components/DropdownWithActions.vue';
export default { export default {
components: { PageHeader, FormControl, Button }, components: { PageHeader, FormControl, Button, DropdownWithActions },
data() { data() {
return { return {
file: null, file: null,
@ -85,6 +96,25 @@ export default {
}; };
}, },
computed: { computed: {
actions() {
const cancelAction = {
component: {
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
},
condition: () => true,
action: this.cancel,
};
const secondaryAction = {
component: {
template: `<span>${this.secondaryLabel}</span>`,
},
condition: () => true,
action: this.handleSecondaryClick,
};
return [secondaryAction, cancelAction];
},
fileName() { fileName() {
if (!this.file) { if (!this.file) {
return ''; return '';
@ -161,6 +191,24 @@ export default {
} }
await saveData(template, filePath); await saveData(template, filePath);
}, },
getAssignerField(targetLabel) {
const assigned = this.importer.assignedMap[targetLabel];
return {
fieldname: 'assignerField',
label: targetLabel,
placeholder: `Select Label`,
fieldtype: 'Select',
options: [
'',
...(assigned ? [assigned] : []),
...this.importer.unassignedLabels,
],
default: assigned ?? '',
};
},
onAssignedChange(target, value) {
this.importer.assignedMap[target] = value;
},
importData() {}, importData() {},
toggleView() {}, toggleView() {},
setImportType(importType) { setImportType(importType) {
@ -177,7 +225,7 @@ export default {
const { success, canceled, filePath, data, name } = const { success, canceled, filePath, data, name } =
await ipcRenderer.invoke(IPC_ACTIONS.GET_FILE, options); await ipcRenderer.invoke(IPC_ACTIONS.GET_FILE, options);
if (!success) { if (!success && !canceled) {
showToast({ message: this.t`File selection failed.`, type: 'error' }); showToast({ message: this.t`File selection failed.`, type: 'error' });
} }
@ -185,11 +233,23 @@ export default {
return; return;
} }
const text = new TextDecoder().decode(data);
const isValid = this.importer.selectFile(text);
if (!isValid) {
showToast({
message: this.t`Bad import data. Could not select file.`,
type: 'error',
});
return;
}
this.file = { this.file = {
name, name,
filePath, filePath,
data: new TextDecoder().decode(data), text,
}; };
window.i = this.importer;
}, },
}, },
}; };