mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
feat: add assign imported labels bar
This commit is contained in:
parent
f230d779c9
commit
be68ab84f8
@ -1,5 +1,6 @@
|
||||
import { Field, FieldType } from '@/types/model';
|
||||
import frappe from 'frappe';
|
||||
import { parseCSV } from './csvParser';
|
||||
|
||||
export const importable = [
|
||||
'SalesInvoice',
|
||||
@ -11,25 +12,41 @@ export const importable = [
|
||||
'Item',
|
||||
];
|
||||
|
||||
type Exclusion = {
|
||||
[key: string]: string[];
|
||||
};
|
||||
|
||||
type Map = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
interface TemplateField {
|
||||
label: string;
|
||||
fieldname: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
type LabelFieldMap = {
|
||||
[key: string]: string;
|
||||
const exclusion: Exclusion = {
|
||||
Item: ['image'],
|
||||
};
|
||||
|
||||
export function getTemplateFields(doctype: string): TemplateField[] {
|
||||
function getTemplateFields(doctype: string): TemplateField[] {
|
||||
const fields: TemplateField[] = [];
|
||||
if (!doctype) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const primaryFields: Field[] = frappe.models[doctype].fields;
|
||||
const tableTypes: string[] = [];
|
||||
let exclusionFields: string[] = exclusion[doctype] ?? [];
|
||||
|
||||
primaryFields.forEach(
|
||||
({ label, fieldtype, childtype, fieldname, required }) => {
|
||||
if (exclusionFields.includes(fieldname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldtype === FieldType.Table && childtype) {
|
||||
tableTypes.push(childtype);
|
||||
}
|
||||
@ -43,10 +60,15 @@ export function getTemplateFields(doctype: string): TemplateField[] {
|
||||
);
|
||||
|
||||
tableTypes.forEach((childtype) => {
|
||||
exclusionFields = exclusion[childtype] ?? [];
|
||||
|
||||
// @ts-ignore
|
||||
const childFields: Field[] = frappe.models[childtype].fields;
|
||||
childFields.forEach(({ label, fieldtype, fieldname, required }) => {
|
||||
if (fieldtype === FieldType.Table) {
|
||||
if (
|
||||
exclusionFields.includes(fieldname) ||
|
||||
fieldtype === FieldType.Table
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -57,8 +79,8 @@ export function getTemplateFields(doctype: string): TemplateField[] {
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getLabelFieldMap(templateFields: TemplateField[]): LabelFieldMap {
|
||||
const map: LabelFieldMap = {};
|
||||
function getLabelFieldMap(templateFields: TemplateField[]): Map {
|
||||
const map: Map = {};
|
||||
|
||||
templateFields.reduce((acc, tf) => {
|
||||
const key = tf.label as string;
|
||||
@ -77,21 +99,72 @@ function getTemplate(templateFields: TemplateField[]): string {
|
||||
export class Importer {
|
||||
doctype: string;
|
||||
templateFields: TemplateField[];
|
||||
_map: LabelFieldMap;
|
||||
_template: string;
|
||||
map: Map;
|
||||
template: string;
|
||||
parsedLabels: string[] = [];
|
||||
assignedMap: Map = {}; // target: import
|
||||
|
||||
constructor(doctype: string) {
|
||||
this.doctype = doctype;
|
||||
this.templateFields = getTemplateFields(doctype);
|
||||
this._map = getLabelFieldMap(this.templateFields);
|
||||
this._template = getTemplate(this.templateFields);
|
||||
this.map = getLabelFieldMap(this.templateFields);
|
||||
this.template = getTemplate(this.templateFields);
|
||||
this.assignedMap = this.assignableLabels.reduce((acc: Map, k) => {
|
||||
acc[k] = '';
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
get map() {
|
||||
return this._map;
|
||||
get assignableLabels() {
|
||||
return Object.keys(this.map);
|
||||
}
|
||||
|
||||
get template() {
|
||||
return this._template;
|
||||
get unassignedLabels() {
|
||||
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;
|
||||
|
@ -7,62 +7,72 @@
|
||||
</h1>
|
||||
</template>
|
||||
<template #actions>
|
||||
<DropdownWithActions
|
||||
class="ml-2"
|
||||
:actions="actions"
|
||||
v-if="canCancel || importType"
|
||||
/>
|
||||
<Button
|
||||
v-if="canCancel"
|
||||
type="secondary"
|
||||
class="text-white text-xs ml-2"
|
||||
@click="cancel"
|
||||
v-if="importType"
|
||||
type="primary"
|
||||
class="text-sm ml-2"
|
||||
@click="handlePrimaryClick"
|
||||
>{{ primaryLabel }}</Button
|
||||
>
|
||||
{{ t`Cancel` }}
|
||||
</Button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<div class="flex justify-center flex-1 mb-8 mt-2">
|
||||
<div
|
||||
class="
|
||||
border
|
||||
rounded-lg
|
||||
shadow
|
||||
h-full
|
||||
flex flex-col
|
||||
justify-between
|
||||
p-6
|
||||
"
|
||||
style="width: 600px"
|
||||
>
|
||||
<div>
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<FormControl
|
||||
:df="importableDf"
|
||||
input-class="bg-gray-100 text-gray-900 text-base"
|
||||
class="w-1/4"
|
||||
:value="importType"
|
||||
@change="setImportType"
|
||||
/>
|
||||
<p
|
||||
class="text-base text-base"
|
||||
:class="fileName ? 'text-gray-900' : 'text-gray-700'"
|
||||
>
|
||||
{{ helperText }}
|
||||
</p>
|
||||
</div>
|
||||
<hr class="mt-6" />
|
||||
</div>
|
||||
<div v-if="importType">
|
||||
<hr class="mb-6" />
|
||||
<div class="flex flex-row justify-between text-base">
|
||||
<Button
|
||||
class="w-1/4"
|
||||
:padding="false"
|
||||
@click="handleSecondaryClick"
|
||||
>{{ secondaryLabel }}</Button
|
||||
>
|
||||
<Button class="w-1/4" type="primary" @click="handlePrimaryClick">{{
|
||||
primaryLabel
|
||||
}}</Button>
|
||||
</div>
|
||||
<!-- <div class="flex justify-center flex-1 mb-8 mt-2"> -->
|
||||
<div class="flex px-8 mt-2 text-base w-full flex-col gap-8">
|
||||
<!-- Type selector -->
|
||||
<div class="flex flex-row justify-start items-center w-full">
|
||||
<FormControl
|
||||
:df="importableDf"
|
||||
input-class="bg-gray-100 text-gray-900 text-base"
|
||||
class="w-40"
|
||||
:value="importType"
|
||||
size="small"
|
||||
@change="setImportType"
|
||||
/>
|
||||
<p
|
||||
class="text-base text-base ml-2"
|
||||
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
||||
>
|
||||
<span v-if="fileName" class="font-normal"
|
||||
>{{ t`Selected file` }}
|
||||
</span>
|
||||
{{ helperText }}{{ fileName ? ',' : '' }}
|
||||
<span v-if="fileName" class="font-normal">
|
||||
{{ t`verify imported data and click on` }} </span
|
||||
>{{ ' ' }}<span v-if="fileName">{{ t`Import Data` }}</span
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Label Assigner -->
|
||||
<div v-if="fileName">
|
||||
<h2 class="text-lg font-semibold">{{ t`Assign Imported Labels` }}</h2>
|
||||
<div class="gap-2 mt-4 grid grid-flow-col overflow-scroll pb-4">
|
||||
<FormControl
|
||||
:show-label="true"
|
||||
size="small"
|
||||
class="w-28"
|
||||
input-class="bg-gray-100"
|
||||
v-for="(f, k) in importer.assignableLabels"
|
||||
:df="getAssignerField(f)"
|
||||
:value="importer.assignedMap[f] ?? ''"
|
||||
@change="(v) => onAssignedChange(f, v)"
|
||||
:key="f + '-' + k"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
@ -75,8 +85,9 @@ import Button from '@/components/Button.vue';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { IPC_ACTIONS } from '@/messages';
|
||||
import { getSavePath, saveData, showToast } from '@/utils';
|
||||
import DropdownWithActions from '@/components/DropdownWithActions.vue';
|
||||
export default {
|
||||
components: { PageHeader, FormControl, Button },
|
||||
components: { PageHeader, FormControl, Button, DropdownWithActions },
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
@ -85,6 +96,25 @@ export default {
|
||||
};
|
||||
},
|
||||
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() {
|
||||
if (!this.file) {
|
||||
return '';
|
||||
@ -161,6 +191,24 @@ export default {
|
||||
}
|
||||
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() {},
|
||||
toggleView() {},
|
||||
setImportType(importType) {
|
||||
@ -177,7 +225,7 @@ export default {
|
||||
const { success, canceled, filePath, data, name } =
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.GET_FILE, options);
|
||||
|
||||
if (!success) {
|
||||
if (!success && !canceled) {
|
||||
showToast({ message: this.t`File selection failed.`, type: 'error' });
|
||||
}
|
||||
|
||||
@ -185,11 +233,23 @@ export default {
|
||||
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 = {
|
||||
name,
|
||||
filePath,
|
||||
data: new TextDecoder().decode(data),
|
||||
text,
|
||||
};
|
||||
|
||||
window.i = this.importer;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user