mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
feat: add should submit
- code for insertion - success state
This commit is contained in:
parent
f7937935fb
commit
916d0ecee4
@ -12,12 +12,26 @@ export const importable = [
|
|||||||
'Item',
|
'Item',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type Status = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
names: string[];
|
||||||
|
};
|
||||||
|
|
||||||
type Exclusion = {
|
type Exclusion = {
|
||||||
[key: string]: string[];
|
[key: string]: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Map = {
|
type Map = {
|
||||||
[key: string]: string | boolean | object;
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ObjectMap = {
|
||||||
|
[key: string]: Map;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LabelTemplateFieldMap = {
|
||||||
|
[key: string]: TemplateField;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TemplateField {
|
interface TemplateField {
|
||||||
@ -27,6 +41,27 @@ interface TemplateField {
|
|||||||
doctype: string;
|
doctype: string;
|
||||||
options?: string[];
|
options?: string[];
|
||||||
fieldtype: FieldType;
|
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 = {
|
const exclusion: Exclusion = {
|
||||||
@ -35,11 +70,21 @@ const exclusion: Exclusion = {
|
|||||||
Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'],
|
Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'],
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
|
function getFilteredDocFields(
|
||||||
const fields: TemplateField[] = [];
|
df: string | string[]
|
||||||
|
): [TemplateField[], string[][]] {
|
||||||
|
let doctype = df[0];
|
||||||
|
let parentField = df[1] ?? '';
|
||||||
|
|
||||||
|
if (typeof df === 'string') {
|
||||||
|
doctype = df;
|
||||||
|
parentField = '';
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const primaryFields: Field[] = frappe.models[doctype].fields;
|
const primaryFields: Field[] = frappe.models[doctype].fields;
|
||||||
const tableTypes: string[] = [];
|
const fields: TemplateField[] = [];
|
||||||
|
const tableTypes: string[][] = [];
|
||||||
const exclusionFields: string[] = exclusion[doctype] ?? [];
|
const exclusionFields: string[] = exclusion[doctype] ?? [];
|
||||||
|
|
||||||
primaryFields.forEach(
|
primaryFields.forEach(
|
||||||
@ -54,15 +99,16 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
|
|||||||
options,
|
options,
|
||||||
}) => {
|
}) => {
|
||||||
if (
|
if (
|
||||||
readOnly ||
|
!(fieldname === 'name' && !parentField) &&
|
||||||
|
(readOnly ||
|
||||||
(hidden && typeof hidden === 'number') ||
|
(hidden && typeof hidden === 'number') ||
|
||||||
exclusionFields.includes(fieldname)
|
exclusionFields.includes(fieldname))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldtype === FieldType.Table && childtype) {
|
if (fieldtype === FieldType.Table && childtype) {
|
||||||
tableTypes.push(childtype);
|
tableTypes.push([childtype, fieldname]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +118,7 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
|
|||||||
doctype,
|
doctype,
|
||||||
options,
|
options,
|
||||||
fieldtype,
|
fieldtype,
|
||||||
|
parentField,
|
||||||
required: Boolean(required ?? false),
|
required: Boolean(required ?? false),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -85,7 +132,7 @@ function getTemplateFields(doctype: string): TemplateField[] {
|
|||||||
if (!doctype) {
|
if (!doctype) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const doctypes: string[] = [doctype];
|
const doctypes: string[][] = [[doctype]];
|
||||||
while (doctypes.length > 0) {
|
while (doctypes.length > 0) {
|
||||||
const dt = doctypes.pop();
|
const dt = doctypes.pop();
|
||||||
if (!dt) {
|
if (!dt) {
|
||||||
@ -126,7 +173,8 @@ export class Importer {
|
|||||||
parsedValues: string[][] = [];
|
parsedValues: string[][] = [];
|
||||||
assignedMap: Map = {}; // target: import
|
assignedMap: Map = {}; // target: import
|
||||||
requiredMap: Map = {};
|
requiredMap: Map = {};
|
||||||
labelFieldMap: Map = {};
|
labelTemplateFieldMap: LabelTemplateFieldMap = {};
|
||||||
|
shouldSubmit: boolean = false;
|
||||||
|
|
||||||
constructor(doctype: string) {
|
constructor(doctype: string) {
|
||||||
this.doctype = doctype;
|
this.doctype = doctype;
|
||||||
@ -141,10 +189,13 @@ export class Importer {
|
|||||||
acc[k.label] = k.required;
|
acc[k.label] = k.required;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
this.labelFieldMap = this.templateFields.reduce((acc: Map, k) => {
|
this.labelTemplateFieldMap = this.templateFields.reduce(
|
||||||
|
(acc: LabelTemplateFieldMap, k) => {
|
||||||
acc[k.label] = k;
|
acc[k.label] = k;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get assignableLabels() {
|
get assignableLabels() {
|
||||||
@ -225,9 +276,11 @@ export class Importer {
|
|||||||
this.parsedLabels = csv[0];
|
this.parsedLabels = csv[0];
|
||||||
const values = csv.slice(1);
|
const values = csv.slice(1);
|
||||||
|
|
||||||
|
/*
|
||||||
if (values.some((v) => v.length !== this.parsedLabels.length)) {
|
if (values.some((v) => v.length !== this.parsedLabels.length)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
this.parsedValues = values;
|
this.parsedValues = values;
|
||||||
this._setAssigned();
|
this._setAssigned();
|
||||||
@ -244,9 +297,81 @@ export class Importer {
|
|||||||
this.assignedMap[l] = l;
|
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
|
// @ts-ignore
|
||||||
window.gtf = getTemplateFields;
|
window.gtf = getTemplateFields;
|
||||||
|
@ -10,10 +10,10 @@
|
|||||||
<DropdownWithActions
|
<DropdownWithActions
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
:actions="actions"
|
:actions="actions"
|
||||||
v-if="canCancel || importType"
|
v-if="(canCancel || importType) && !complete"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="importType"
|
v-if="importType && !complete"
|
||||||
type="primary"
|
type="primary"
|
||||||
class="text-sm ml-2"
|
class="text-sm ml-2"
|
||||||
@click="handlePrimaryClick"
|
@click="handlePrimaryClick"
|
||||||
@ -21,9 +21,12 @@
|
|||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</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 -->
|
<!-- 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
|
<FormControl
|
||||||
:df="importableDf"
|
:df="importableDf"
|
||||||
input-class="bg-gray-100 text-gray-900 text-base"
|
input-class="bg-gray-100 text-gray-900 text-base"
|
||||||
@ -32,6 +35,36 @@
|
|||||||
size="small"
|
size="small"
|
||||||
@change="setImportType"
|
@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
|
<p
|
||||||
class="text-base text-base ml-2"
|
class="text-base text-base ml-2"
|
||||||
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
||||||
@ -49,7 +82,9 @@
|
|||||||
<!-- Label Assigner -->
|
<!-- Label Assigner -->
|
||||||
<div v-if="fileName" class="pb-4">
|
<div v-if="fileName" class="pb-4">
|
||||||
<h2 class="text-lg font-semibold">{{ t`Assign Imported Labels` }}</h2>
|
<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">
|
<div v-for="(f, k) in importer.assignableLabels" :key="f + '-' + k">
|
||||||
<p class="text-gray-600 text-sm mb-1">
|
<p class="text-gray-600 text-sm mb-1">
|
||||||
{{ f }}
|
{{ f }}
|
||||||
@ -143,6 +178,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@ -166,6 +236,8 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
complete: false,
|
||||||
|
names: ['Bat', 'Baseball', 'Other Shit'],
|
||||||
file: null,
|
file: null,
|
||||||
importer: null,
|
importer: null,
|
||||||
importType: '',
|
importType: '',
|
||||||
@ -186,7 +258,7 @@ export default {
|
|||||||
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
|
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
|
||||||
},
|
},
|
||||||
condition: () => true,
|
condition: () => true,
|
||||||
action: this.cancel,
|
action: this.clear,
|
||||||
};
|
};
|
||||||
|
|
||||||
const secondaryAction = {
|
const secondaryAction = {
|
||||||
@ -216,6 +288,13 @@ export default {
|
|||||||
primaryLabel() {
|
primaryLabel() {
|
||||||
return this.file ? this.t`Import Data` : this.t`Select File`;
|
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() {
|
importableDf() {
|
||||||
return {
|
return {
|
||||||
fieldname: 'importType',
|
fieldname: 'importType',
|
||||||
@ -240,11 +319,21 @@ export default {
|
|||||||
return !!(this.file || this.importType);
|
return !!(this.file || this.importType);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deactivated() {
|
||||||
|
this.clear();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancel() {
|
showMe() {
|
||||||
|
this.clear();
|
||||||
|
const doctype = this.importer?.doctype ?? 'Item';
|
||||||
|
this.$router.push(`/list/${doctype}`);
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
this.file = null;
|
this.file = null;
|
||||||
|
this.names = [];
|
||||||
this.importer = null;
|
this.importer = null;
|
||||||
this.importType = '';
|
this.importType = '';
|
||||||
|
this.complete = false;
|
||||||
},
|
},
|
||||||
handlePrimaryClick() {
|
handlePrimaryClick() {
|
||||||
if (!this.file) {
|
if (!this.file) {
|
||||||
@ -292,10 +381,24 @@ export default {
|
|||||||
onValueChange(event, i, j) {
|
onValueChange(event, i, j) {
|
||||||
this.importer.updateValue(event.target.value, 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) {
|
setImportType(importType) {
|
||||||
if (this.importType) {
|
if (this.importType) {
|
||||||
this.cancel();
|
this.clear();
|
||||||
}
|
}
|
||||||
this.importType = importType;
|
this.importType = importType;
|
||||||
this.importer = new Importer(this.labelDoctypeMap[this.importType]);
|
this.importer = new Importer(this.labelDoctypeMap[this.importType]);
|
||||||
|
Loading…
Reference in New Issue
Block a user