From e840b5bb7ca233a51c1ccb3f294b76a3953ebe42 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 31 Jul 2023 12:38:32 +0530 Subject: [PATCH] incr: add customization schemas and models --- fyo/model/types.ts | 14 +-- fyo/models/CustomField.ts | 102 ++++++++++++++++ fyo/models/CustomForm.ts | 52 ++++++++ fyo/models/index.ts | 4 + models/types.ts | 2 + schemas/core/CustomField.json | 137 ++++++++++++++++++++++ schemas/core/CustomForm.json | 22 ++++ schemas/schemas.ts | 35 +++--- schemas/types.ts | 9 +- src/pages/CustomizeForm/CustomizeForm.vue | 82 ++++++++++++- 10 files changed, 432 insertions(+), 27 deletions(-) create mode 100644 fyo/models/CustomField.ts create mode 100644 fyo/models/CustomForm.ts create mode 100644 schemas/core/CustomField.json create mode 100644 schemas/core/CustomForm.json diff --git a/fyo/model/types.ts b/fyo/model/types.ts index e88c1db8..087ce184 100644 --- a/fyo/model/types.ts +++ b/fyo/model/types.ts @@ -1,15 +1,15 @@ -import { Fyo } from 'fyo'; -import { DocValue, DocValueMap } from 'fyo/core/types'; +import type { Fyo } from 'fyo'; +import type { DocValue, DocValueMap } from 'fyo/core/types'; import type SystemSettings from 'fyo/models/SystemSettings'; -import { FieldType, Schema, SelectOption } from 'schemas/types'; -import { QueryFilter } from 'utils/db/types'; -import { RouteLocationRaw, Router } from 'vue-router'; -import { Doc } from './doc'; +import type { FieldType, Schema, SelectOption } from 'schemas/types'; +import type { QueryFilter } from 'utils/db/types'; +import type { RouteLocationRaw, Router } from 'vue-router'; +import type { Doc } from './doc'; import type { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings'; import type { Defaults } from 'models/baseModels/Defaults/Defaults'; import type { PrintSettings } from 'models/baseModels/PrintSettings/PrintSettings'; import type { InventorySettings } from 'models/inventory/InventorySettings'; -import { Misc } from 'models/baseModels/Misc'; +import type { Misc } from 'models/baseModels/Misc'; /** * The functions below are used for dynamic evaluation diff --git a/fyo/models/CustomField.ts b/fyo/models/CustomField.ts new file mode 100644 index 00000000..f576e085 --- /dev/null +++ b/fyo/models/CustomField.ts @@ -0,0 +1,102 @@ +import { Doc } from 'fyo/model/doc'; +import type { + FormulaMap, + HiddenMap, + ListsMap, + ValidationMap, +} from 'fyo/model/types'; +import type { FieldType, SelectOption } from 'schemas/types'; +import type { CustomForm } from './CustomForm'; +import { ValueError } from 'fyo/utils/errors'; +import { ModelNameEnum } from 'models/types'; + +export class CustomField extends Doc { + parentdoc?: CustomForm; + + label?: string; + fieldname?: string; + fieldtype?: FieldType; + isRequired?: boolean; + section?: string; + tab?: string; + options?: string; + target?: string; + references?: string; + + get parentSchema() { + return this.parentdoc?.parentSchema ?? null; + } + + get parentFields() { + return this.parentdoc?.parentFields; + } + + hidden: HiddenMap = { + options: () => + this.fieldtype !== 'Select' && + this.fieldtype !== 'AutoComplete' && + this.fieldtype !== 'Color', + target: () => this.fieldtype !== 'Link' && this.fieldtype !== 'Table', + references: () => this.fieldtype !== 'DynamicLink', + }; + + formulas: FormulaMap = {}; + + validations: ValidationMap = { + fieldname: (value) => { + if (typeof value !== 'string') { + return; + } + + const field = this.parentFields?.[value]; + if (field && !field.isCustom) { + throw new ValueError( + this.fyo.t`Fieldname ${value} already exists for ${this.parentdoc! + .name!}` + ); + } + + const cf = this.parentdoc?.customFields?.find( + (cf) => cf.fieldname === value + ); + if (cf) { + throw new ValueError( + this.fyo.t`Fieldname ${value} already used for Custom Field ${ + (cf.idx ?? 0) + 1 + }` + ); + } + }, + }; + + static lists: ListsMap = { + target: (doc) => { + const schemaMap = doc?.fyo.schemaMap ?? {}; + return Object.values(schemaMap) + .filter( + (s) => + !s?.isSingle && + ![ + ModelNameEnum.PatchRun, + ModelNameEnum.SingleValue, + ModelNameEnum.CustomField, + ModelNameEnum.CustomForm, + ModelNameEnum.SetupWizard, + ].includes(s?.name as ModelNameEnum) + ) + .map((s) => ({ + label: s?.label ?? '', + value: s?.name ?? '', + })); + }, + references: (doc) => { + if (!(doc instanceof CustomField)) { + return []; + } + + return (doc.parentdoc?.customFields + ?.map((cf) => ({ value: cf.fieldname, label: cf.label })) + .filter((cf) => cf.label && cf.value) ?? []) as SelectOption[]; + }, + }; +} diff --git a/fyo/models/CustomForm.ts b/fyo/models/CustomForm.ts new file mode 100644 index 00000000..fc429876 --- /dev/null +++ b/fyo/models/CustomForm.ts @@ -0,0 +1,52 @@ +import { Doc } from 'fyo/model/doc'; +import { HiddenMap, ListsMap } from 'fyo/model/types'; +import { ModelNameEnum } from 'models/types'; +import type { CustomField } from './CustomField'; +import { getMapFromList } from 'utils/index'; +import { Field } from 'schemas/types'; + +export class CustomForm extends Doc { + name?: string; + customFields?: CustomField[]; + + get parentSchema() { + return this.fyo.schemaMap[this.name ?? ''] ?? null; + } + + get parentFields(): Record { + const fields = this.parentSchema?.fields; + if (!fields) { + return {}; + } + + return getMapFromList(fields, 'fieldname'); + } + + static lists: ListsMap = { + name: (doc) => + Object.values(doc?.fyo.schemaMap ?? {}) + .filter((s) => { + if (!s || !s.label || !s.name) { + return false; + } + + if (s.isSingle) { + return false; + } + + return ![ + ModelNameEnum.PatchRun, + ModelNameEnum.SingleValue, + ModelNameEnum.CustomField, + ModelNameEnum.CustomForm, + ModelNameEnum.SetupWizard, + ].includes(s.name as ModelNameEnum); + }) + .map((s) => ({ + value: s!.name, + label: s!.label, + })), + }; + + hidden: HiddenMap = { customFields: () => !this.name }; +} diff --git a/fyo/models/index.ts b/fyo/models/index.ts index e67582aa..a596c58a 100644 --- a/fyo/models/index.ts +++ b/fyo/models/index.ts @@ -1,8 +1,12 @@ import { ModelMap } from 'fyo/model/types'; import NumberSeries from './NumberSeries'; import SystemSettings from './SystemSettings'; +import { CustomField } from './CustomField'; +import { CustomForm } from './CustomForm'; export const coreModels = { NumberSeries, SystemSettings, + CustomForm, + CustomField, } as ModelMap; diff --git a/models/types.ts b/models/types.ts index 65077b43..a9ff080c 100644 --- a/models/types.ts +++ b/models/types.ts @@ -44,6 +44,8 @@ export enum ModelNameEnum { PurchaseReceipt = 'PurchaseReceipt', PurchaseReceiptItem = 'PurchaseReceiptItem', Location = 'Location', + CustomForm = 'CustomForm', + CustomField = 'CustomField' } export type ModelName = keyof typeof ModelNameEnum; diff --git a/schemas/core/CustomField.json b/schemas/core/CustomField.json new file mode 100644 index 00000000..953e04ef --- /dev/null +++ b/schemas/core/CustomField.json @@ -0,0 +1,137 @@ +{ + "name": "CustomField", + "label": "Custom Field", + "isChild": true, + "fields": [ + { + "fieldname": "label", + "label": "Label", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "fieldname", + "label": "Fieldname", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "fieldtype", + "label": "Fieldtype", + "fieldtype": "Select", + "options": [ + { + "label": "Data", + "value": "Data" + }, + { + "label": "Select", + "value": "Select" + }, + { + "label": "Link", + "value": "Link" + }, + { + "label": "Date", + "value": "Date" + }, + { + "label": "Date Time", + "value": "Datetime" + }, + { + "label": "Table", + "value": "Table" + }, + { + "label": "Autocomplete", + "value": "AutoComplete" + }, + { + "label": "Check", + "value": "Check" + }, + { + "label": "Attach Image", + "value": "AttachImage" + }, + { + "label": "Dynamic Link", + "value": "DynamicLink" + }, + { + "label": "Int", + "value": "Int" + }, + { + "label": "Float", + "value": "Float" + }, + { + "label": "Currency", + "value": "Currency" + }, + { + "label": "Text", + "value": "Text" + }, + { + "label": "Color", + "value": "Color" + }, + { + "label": "Attachment", + "value": "Attachment" + } + ], + "default": "Data", + "required": true + }, + { + "fieldname": "isRequired", + "label": "Is Required", + "fieldtype": "Check", + "default": false + }, + { + "fieldname": "section", + "label": "Form Section", + "fieldtype": "Data", + "default": "Default" + }, + { + "fieldname": "tab", + "label": "Form Tab", + "fieldtype": "Data", + "default": "Custom" + }, + { + "fieldname": "options", + "label": "Options", + "fieldtype": "Text" + }, + { + "fieldname": "target", + "label": "Target", + "fieldtype": "AutoComplete" + }, + { + "fieldname": "references", + "label": "References", + "fieldtype": "AutoComplete" + } + ], + "tableFields": ["label", "fieldname", "fieldtype"], + "quickEditFields": [ + "label", + "fieldname", + "fieldtype", + "required", + "options", + "target", + "references", + "section", + "tab" + ] +} diff --git a/schemas/core/CustomForm.json b/schemas/core/CustomForm.json new file mode 100644 index 00000000..24f5312d --- /dev/null +++ b/schemas/core/CustomForm.json @@ -0,0 +1,22 @@ +{ + "name": "CustomForm", + "label": "Custom Form", + "naming": "manual", + "fields": [ + { + "fieldname": "name", + "fieldtype": "AutoComplete", + "label": "Form Type", + "options": [], + "required": true + }, + { + "fieldname": "customFields", + "label": "Custom Fields", + "fieldtype": "Table", + "target": "CustomField", + "required": true, + "edit": true + } + ] +} diff --git a/schemas/schemas.ts b/schemas/schemas.ts index cceda151..9c3ed968 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -7,21 +7,6 @@ import Color from './app/Color.json'; import Currency from './app/Currency.json'; import Defaults from './app/Defaults.json'; import GetStarted from './app/GetStarted.json'; -import InventorySettings from './app/inventory/InventorySettings.json'; -import Location from './app/inventory/Location.json'; -import PriceList from './app/PriceList.json'; -import PriceListItem from './app/PriceListItem.json'; -import PurchaseReceipt from './app/inventory/PurchaseReceipt.json'; -import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json'; -import SerialNumber from './app/inventory/SerialNumber.json'; -import Shipment from './app/inventory/Shipment.json'; -import ShipmentItem from './app/inventory/ShipmentItem.json'; -import StockLedgerEntry from './app/inventory/StockLedgerEntry.json'; -import StockMovement from './app/inventory/StockMovement.json'; -import StockMovementItem from './app/inventory/StockMovementItem.json'; -import StockTransfer from './app/inventory/StockTransfer.json'; -import StockTransferItem from './app/inventory/StockTransferItem.json'; -import UOMConversionItem from './app/inventory/UOMConversionItem.json'; import Invoice from './app/Invoice.json'; import InvoiceItem from './app/InvoiceItem.json'; import Item from './app/Item.json'; @@ -32,6 +17,8 @@ import NumberSeries from './app/NumberSeries.json'; import Party from './app/Party.json'; import Payment from './app/Payment.json'; import PaymentFor from './app/PaymentFor.json'; +import PriceList from './app/PriceList.json'; +import PriceListItem from './app/PriceListItem.json'; import PrintSettings from './app/PrintSettings.json'; import PrintTemplate from './app/PrintTemplate.json'; import PurchaseInvoice from './app/PurchaseInvoice.json'; @@ -43,6 +30,21 @@ import Tax from './app/Tax.json'; import TaxDetail from './app/TaxDetail.json'; import TaxSummary from './app/TaxSummary.json'; import UOM from './app/UOM.json'; +import InventorySettings from './app/inventory/InventorySettings.json'; +import Location from './app/inventory/Location.json'; +import PurchaseReceipt from './app/inventory/PurchaseReceipt.json'; +import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json'; +import SerialNumber from './app/inventory/SerialNumber.json'; +import Shipment from './app/inventory/Shipment.json'; +import ShipmentItem from './app/inventory/ShipmentItem.json'; +import StockLedgerEntry from './app/inventory/StockLedgerEntry.json'; +import StockMovement from './app/inventory/StockMovement.json'; +import StockMovementItem from './app/inventory/StockMovementItem.json'; +import StockTransfer from './app/inventory/StockTransfer.json'; +import StockTransferItem from './app/inventory/StockTransferItem.json'; +import UOMConversionItem from './app/inventory/UOMConversionItem.json'; +import CustomField from './core/CustomField.json'; +import CustomForm from './core/CustomForm.json'; import PatchRun from './core/PatchRun.json'; import SingleValue from './core/SingleValue.json'; import SystemSettings from './core/SystemSettings.json'; @@ -124,4 +126,7 @@ export const appSchemas: Schema[] | SchemaStub[] = [ Batch as Schema, SerialNumber as Schema, + + CustomForm as Schema, + CustomField as Schema, ]; diff --git a/schemas/types.ts b/schemas/types.ts index 48ea636c..1e14dde9 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -49,8 +49,8 @@ type BaseFieldType = Exclude< export type RawValue = string | number | boolean | null; export interface BaseField { - fieldname: string; // Column name in the db - fieldtype: BaseFieldType; // UI Descriptive field types that map to column types + fieldname: string; // Column name in the db + fieldtype: BaseFieldType; // UI Descriptive field types that map to column types label: string; // Translateable UI facing name schemaName?: string; // Convenient access to schemaName incase just the field is passed required?: boolean; // Implies Not Null @@ -61,11 +61,12 @@ export interface BaseField { placeholder?: string; // UI Facing config, form field placeholder groupBy?: string; // UI Facing used in dropdowns fields meta?: boolean; // Field is a meta field, i.e. only for the db, not UI - filter?: boolean; // UI Facing config, whether to be used to filter the List. + filter?: boolean; // UI Facing config, whether to be used to filter the List. computed?: boolean; // Computed values are not stored in the database. section?: string; // UI Facing config, for grouping by sections tab?: string; // UI Facing config, for grouping by tabs - abstract?: string; // Uused to mark the location of a field in an Abstract schema + abstract?: string; // Used to mark the location of a field in an Abstract schema + isCustom?: boolean; // Whether the field is a custom field } export type SelectOption = { value: string; label: string }; diff --git a/src/pages/CustomizeForm/CustomizeForm.vue b/src/pages/CustomizeForm/CustomizeForm.vue index bd59f277..16382669 100644 --- a/src/pages/CustomizeForm/CustomizeForm.vue +++ b/src/pages/CustomizeForm/CustomizeForm.vue @@ -6,6 +6,43 @@ {{ t`Save` }} +
+ +
+ + +

+ {{ errorMessage }} +

+

+ {{ helpMessage }} +

+
+