2
0
mirror of https://github.com/frappe/books.git synced 2024-09-20 03:29:00 +00:00

feat: add should submit

- code for insertion
- success state
This commit is contained in:
18alantom 2022-02-23 16:01:55 +05:30
parent f7937935fb
commit 916d0ecee4
2 changed files with 253 additions and 25 deletions

View File

@ -12,12 +12,26 @@ export const importable = [
'Item',
];
type Status = {
success: boolean;
message: string;
names: string[];
};
type Exclusion = {
[key: string]: string[];
};
type Map = {
[key: string]: string | boolean | object;
[key: string]: unknown;
};
type ObjectMap = {
[key: string]: Map;
};
type LabelTemplateFieldMap = {
[key: string]: TemplateField;
};
interface TemplateField {
@ -27,6 +41,27 @@ interface TemplateField {
doctype: string;
options?: string[];
fieldtype: FieldType;
parentField: string;
}
function formatValue(value: string, fieldtype: FieldType): unknown {
switch (fieldtype) {
case FieldType.Date:
return new Date(value);
case FieldType.Currency:
// @ts-ignore
return frappe.pesa(value);
case FieldType.Int:
case FieldType.Float: {
const n = parseFloat(value);
if (!Number.isNaN(n)) {
return n;
}
return 0;
}
default:
return value;
}
}
const exclusion: Exclusion = {
@ -35,11 +70,21 @@ const exclusion: Exclusion = {
Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'],
};
function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
const fields: TemplateField[] = [];
function getFilteredDocFields(
df: string | string[]
): [TemplateField[], string[][]] {
let doctype = df[0];
let parentField = df[1] ?? '';
if (typeof df === 'string') {
doctype = df;
parentField = '';
}
// @ts-ignore
const primaryFields: Field[] = frappe.models[doctype].fields;
const tableTypes: string[] = [];
const fields: TemplateField[] = [];
const tableTypes: string[][] = [];
const exclusionFields: string[] = exclusion[doctype] ?? [];
primaryFields.forEach(
@ -54,15 +99,16 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
options,
}) => {
if (
readOnly ||
!(fieldname === 'name' && !parentField) &&
(readOnly ||
(hidden && typeof hidden === 'number') ||
exclusionFields.includes(fieldname)
exclusionFields.includes(fieldname))
) {
return;
}
if (fieldtype === FieldType.Table && childtype) {
tableTypes.push(childtype);
tableTypes.push([childtype, fieldname]);
return;
}
@ -72,6 +118,7 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
doctype,
options,
fieldtype,
parentField,
required: Boolean(required ?? false),
});
}
@ -85,7 +132,7 @@ function getTemplateFields(doctype: string): TemplateField[] {
if (!doctype) {
return [];
}
const doctypes: string[] = [doctype];
const doctypes: string[][] = [[doctype]];
while (doctypes.length > 0) {
const dt = doctypes.pop();
if (!dt) {
@ -126,7 +173,8 @@ export class Importer {
parsedValues: string[][] = [];
assignedMap: Map = {}; // target: import
requiredMap: Map = {};
labelFieldMap: Map = {};
labelTemplateFieldMap: LabelTemplateFieldMap = {};
shouldSubmit: boolean = false;
constructor(doctype: string) {
this.doctype = doctype;
@ -141,10 +189,13 @@ export class Importer {
acc[k.label] = k.required;
return acc;
}, {});
this.labelFieldMap = this.templateFields.reduce((acc: Map, k) => {
this.labelTemplateFieldMap = this.templateFields.reduce(
(acc: LabelTemplateFieldMap, k) => {
acc[k.label] = k;
return acc;
}, {});
},
{}
);
}
get assignableLabels() {
@ -225,9 +276,11 @@ export class Importer {
this.parsedLabels = csv[0];
const values = csv.slice(1);
/*
if (values.some((v) => v.length !== this.parsedLabels.length)) {
return false;
}
*/
this.parsedValues = values;
this._setAssigned();
@ -244,9 +297,81 @@ export class Importer {
this.assignedMap[l] = l;
});
}
getDocs(): Map[] {
const fields = this.columnLabels.map((k) => this.labelTemplateFieldMap[k]);
const nameIndex = fields.findIndex(({ fieldname }) => fieldname === 'name');
const docMap: ObjectMap = {};
for (let r = 0; r < this.assignedMatrix.length; r++) {
const row = this.assignedMatrix[r];
const cts: ObjectMap = {};
const name = row[nameIndex];
docMap[name] ??= {};
for (let f = 0; f < fields.length; f++) {
const field = fields[f];
const value = formatValue(row[f], field.fieldtype);
if (field.parentField) {
cts[field.parentField] ??= {};
cts[field.parentField][field.fieldname] = value;
continue;
}
docMap[name][field.fieldname] = value;
}
for (const k of Object.keys(cts)) {
docMap[name][k] ??= [];
(docMap[name][k] as Map[]).push(cts[k]);
}
}
// return docObjs;
return Object.keys(docMap).map((k) => docMap[k]);
}
async importData(): Promise<Status> {
const status: Status = { success: false, names: [], message: '' };
for (const docObj of this.getDocs()) {
const doc = frappe.getNewDoc(this.doctype);
await doc.update(docObj);
try {
await doc.insert();
} catch (err) {
const message = (err as Error).message;
const messages = [
frappe.t`Could not import ${this.doctype} ${doc.name}.`,
];
if (message) {
messages.push(frappe.t`Error: ${message}.`);
}
if (status.names.length) {
messages.push(
frappe.t`The following ${
status.names.length
} entries were created: ${status.names.join(', ')}`
);
}
status.message = messages.join(' ');
return status;
}
status.names.push(doc.name);
}
status.success = true;
return status;
}
}
// @ts-ignore
window.im = importable;
// @ts-ignore
window.gtf = getTemplateFields;

View File

@ -10,10 +10,10 @@
<DropdownWithActions
class="ml-2"
:actions="actions"
v-if="canCancel || importType"
v-if="(canCancel || importType) && !complete"
/>
<Button
v-if="importType"
v-if="importType && !complete"
type="primary"
class="text-sm ml-2"
@click="handlePrimaryClick"
@ -21,9 +21,12 @@
>
</template>
</PageHeader>
<div class="flex px-8 mt-2 text-base w-full flex-col gap-8">
<div
class="flex px-8 mt-2 text-base w-full flex-col gap-8"
v-if="!complete"
>
<!-- Type selector -->
<div class="flex flex-row justify-start items-center w-full">
<div class="flex flex-row justify-start items-center w-full gap-2">
<FormControl
:df="importableDf"
input-class="bg-gray-100 text-gray-900 text-base"
@ -32,6 +35,36 @@
size="small"
@change="setImportType"
/>
<div
v-if="importType && isSubmittable"
class="
justify-center
items-center
gap-2
flex
justify-between
items-center
bg-gray-100
px-2
py-1
rounded
text-gray-900
"
>
<p>Should Submit</p>
<FormControl
size="small"
input-class="bg-gray-100"
:df="{
fieldname: 'shouldSubmit',
label: this.t`Submit on Import`,
fieldtype: 'Check',
}"
:value="Number(importer.shouldSubmit)"
@change="(value) => (importer.shouldSubmit = !!value)"
/>
</div>
<p
class="text-base text-base ml-2"
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
@ -49,7 +82,9 @@
<!-- Label Assigner -->
<div v-if="fileName" class="pb-4">
<h2 class="text-lg font-semibold">{{ t`Assign Imported Labels` }}</h2>
<div class="gap-2 mt-4 grid grid-flow-col overflow-x-scroll">
<div
class="gap-2 mt-4 grid grid-flow-col overflow-x-scroll no-scrollbar"
>
<div v-for="(f, k) in importer.assignableLabels" :key="f + '-' + k">
<p class="text-gray-600 text-sm mb-1">
{{ f }}
@ -143,6 +178,41 @@
</div>
</div>
</div>
<div v-if="complete" class="flex justify-center h-full items-center">
<div
class="
flex flex-col
justify-center
items-center
gap-8
rounded-lg
shadow-md
p-6
"
style="width: 450px"
>
<h2 class="text-xl font-semibold mt-4">{{ t`Import Success` }} 🎉</h2>
<p class="text-lg text-center">
{{ t`Successfully created the following ${names.length} entries:` }}
</p>
<div class="max-h-96 overflow-y-scroll">
<div
v-for="(n, i) in names"
:key="i"
class="grid grid-cols-2 gap-2 border-b pb-2 mb-2 pr-4 text-lg w-60"
style="grid-template-columns: 2rem auto"
>
<p class="text-right">{{ i + 1 }}.</p>
<p>
{{ n }}
</p>
</div>
</div>
<Button type="primary" class="text-sm w-28" @click="showMe">{{
t`Show Me`
}}</Button>
</div>
</div>
</div>
</template>
<script>
@ -166,6 +236,8 @@ export default {
},
data() {
return {
complete: false,
names: ['Bat', 'Baseball', 'Other Shit'],
file: null,
importer: null,
importType: '',
@ -186,7 +258,7 @@ export default {
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
},
condition: () => true,
action: this.cancel,
action: this.clear,
};
const secondaryAction = {
@ -216,6 +288,13 @@ export default {
primaryLabel() {
return this.file ? this.t`Import Data` : this.t`Select File`;
},
isSubmittable() {
const doctype = this.importer?.doctype;
if (doctype) {
return frappe.models[doctype].isSubmittable ?? false;
}
return false;
},
importableDf() {
return {
fieldname: 'importType',
@ -240,11 +319,21 @@ export default {
return !!(this.file || this.importType);
},
},
deactivated() {
this.clear();
},
methods: {
cancel() {
showMe() {
this.clear();
const doctype = this.importer?.doctype ?? 'Item';
this.$router.push(`/list/${doctype}`);
},
clear() {
this.file = null;
this.names = [];
this.importer = null;
this.importType = '';
this.complete = false;
},
handlePrimaryClick() {
if (!this.file) {
@ -292,10 +381,24 @@ export default {
onValueChange(event, i, j) {
this.importer.updateValue(event.target.value, i, j);
},
importData() {},
async importData() {
// TODO: pre import conditions
/*
if(){}
*/
const { success, names } = await this.importer.importData();
if (!success || !names.length) {
// handle failure
return;
}
this.names = names;
this.complete = true;
},
setImportType(importType) {
if (this.importType) {
this.cancel();
this.clear();
}
this.importType = importType;
this.importer = new Importer(this.labelDoctypeMap[this.importType]);