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:
parent
f230d779c9
commit
be68ab84f8
@ -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;
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user