scalra-flexform

2.2.0 • Public • Published

scalra-flexform

Scalra-Flexform is a rapid prototyping framework to build customized form-based IT systems with definition files and custom logic. It is based on the Scalra Node.js agile framework.

Development

Project setup

npm install

Develop

Compiles and hot-reloads for development

npm run serve

Before commit

Compiles and minifies for production

npm run build

Run your tests

npm run test

Lints and fixes files

npm run lint

Run your end-to-end tests

npm run test:e2e

Run your unit tests

npm run test:unit

More info

Customize configuration

See Configuration Reference.

Data structure

.
├── modules                  # Backend code (see [scalra](https://gitlab.com/imonology/scalra))
├── dist                     # Production assets
├── src/
│   ├── main.js              # app entry file
│   ├── App.vue              # main app component
│   ├── components/          # ui components
│   │   └── ...
│   └── assets/              # module assets (processed by webpack)
│       └── ...
├── tests                    # Automated tests
│   ├── e2e/                 # e2e test spec files
│   │   └── ...
│   └── unit/                # unit test spec files
│   │   └── ...
├── views                    # built view files
├── web                      # web assets (css, js...)
├── package.json             # build scripts and dependencies
└── README.md                # Default README file

Work with Scalra

Server Side

Model Definition

Create a file in api/models to define the form

.
└── api/models
    ├── application.js
    └── user.js
  • application.js
module.exports = {
	meta: {
		actions: {
			createPosition: 'top',
			afterCreated: 'clear',
			afterUpdated: 'last'
		}
	},
	fields: {
		name: {
			name: 'Name',
			type: 'string',
			desc: 'Applicant name',
			required: true,
			show: true
		},
		id: {
			name: "Applicant's ID",
			type: 'string',
			desc: '',
			required: true,
			show: false
		},
		gender: {
			name: 'gender',
			type: 'choice',
			desc: '',
			required: true,
			show: false,
			default_value: 'm',
			option: [
				{ text: 'male', value: 'm' },
				{ text: 'female', value: 'f' },
				{ text: 'other', value: 'o' }
			]
		},
		age: {
			name: 'age',
			type: 'number',
			desc: 'the user age',
			required: true,
			show: true,
			sortable: true
		},
		email: {
			name: 'email',
			type: 'email',
			desc: '',
			required: true,
			show: true
		},
		phone: {
			name: 'phone',
			type: 'string',
			desc: '',
			required: true,
			show: true,
			validation: {
				pattern: '/^09(\\d{8})$/g',
				message: 'Please input phone number like 0912345678',
				trigger: 'blur'
			}
		},
		skill: {
			name: 'skill',
			type: 'string',
			desc: '',
			required: true,
			show: true,
			extendable: true
		}
	}
};

Model reference to other model

In order to reference to other model you can use collection or model component

module.exports = {
	meta: {
		actions: {
			createPosition: 'top',
			afterCreated: 'clear',
			afterUpdated: 'last'
		}
	},
	fields: {
		organization: {
			name: 'Organization name',
			model: 'organization', // choice model to reference to
			type: 'choice', // option type (multichoice, choice)
			option_text: 'company_name', // display value in option
			required: false
		},
	}

meta parameters:

Paramter Type Description
actions object Actions about CRUD
actions.createPosition string, accept ['top', 'bottom'] works in list page, the position to show create button
actions.createButtonText string The create button's text, default 'New', works only if createPosition is set
actions.afterCreated string, accept ['clear', 'last'] Works in create page, the action after entity created
actions.afterUpdated string, accept ['last', 'refresh'] Works in update page, the action after entity updated

This actions is not working in _account definition

The supported data types:

Type Description
string plain, regular string
number integer and float
date a string with date, such as "2019-07-01"
object any JSON object
account the current user's account name
choice can select only one out of many via pull-down menu (good or above)
multichoice can select multiples via checkboxes
textarea input for multiple lines of data

The supported field arguments:

Argument Datatype Description
name string field name to show as label
type string field type
show boolean to display or not
required boolean required field
extendable boolean extendable field, only supported with type string and number
validation object validation rule
validation.pattern string regex in a string, important: replace \ with \\
validation.message string validation error message
validation.trigger string event to trigger validation, default change, coule be blur

Auto-generated API

Flexform will auto-generate basic CRUD APIs from models that defined in `/api/models/. folder

Here are some Auto-generated API examples for the application model

  • GET /api/application
description

will get all applications with schema

  • GET /api/application/:id
description

will get a specific application with schema

  • POST /api/application
description

will create a new application

  • PATCH /api/application/:id
description

will update a specific application with id as a key

  • DELETE /api/application/:id
description

Delete a specific application with id as a key

handler.js

Data form controllers:

  • get schema
let form_name = 'application';
let controller = new SR.Flexform.controller(form_name);
controller.schema();
LOG.sys('Schema');
LOG.sys(controller);
  • create data
let form_name = 'application';
let controller = new SR.Flexform.controller(form_name);
let new_application = {
	name: 'John Doe',
	id: 'B99',
	gender: 'M',
	age: '27',
	email: 'JohnDoe@lapd.com',
	phone: '317980223'
};
controller.create(new_application);
controller.find();
LOG.sys('Create');
LOG.sys(controller);
  • update data
let form_name = 'application';
let controller = new SR.Flexform.controller(form_name);
let record_id = 'some_record_id';
let application = {
	age: '28'
};
controller.update({
	record_id,
	values: application
});
controller.find({ query: { record_id } });
LOG.sys('Result');
LOG.sys(controller);
  • destroy data
let form_name = 'application';
let controller = new SR.Flexform.controller(form_name);
let record_id = 'some_record_id';
controller.destroy({
	record_id
});
LOG.sys('Done');
  • find data
// Return all 18-year-old data
let form_name = 'application';
let controller = new SR.Flexform.controller(form_name);
controller.find(
	{
		query: {
			age: 18
		}
	},
	{
		with_fields: false // Whether to output schema
	}
);
LOG.sys('Result');
LOG.sys(controller);
  • findOne data
// Return the first 18-year-old data found
let form_name = 'application';
let controller = new SR.Flexform.controller(form_name);
controller.findOne({
	query: {
		age: 18
	}
});
LOG.sys('Result');
LOG.sys(controller);

router.js

Scalra-flexform works as a Single-Page Application (SPA), server side provides menu and api Every menu item should contain a type field('list', 'create', 'update'), indicated what kind of page you want to show page type detail in Page default actions section

  • menu
app.get('/api/menu', (req, res) => {
 const menu = const menu = [
			{
				path: '/',
				redirect: '/dashboard',
				name: 'Home',
				hidden: true,
				children: [
					{
						path: 'dashboard',
					},
				],
			},
			{
				path: '/device',
				redirect: '/device',
				name: 'Device Manege',
				meta: {
					title: 'device',
					icon: 'device',
				},
				children: [
					{
						path: 'create',
						name: 'create device',
						type: 'create',
						meta: {
							title: 'create device',
							icon: 'device',
						},
					},
					{
						path: 'list',
						name: 'list device',
						type: 'list',
						meta: {
							title: 'list',
							icon: 'edit',
						},
					},
				],
			},
			{
				path: 'external-link',
				children: [
					{
						path: 'https://www.google.com/',
						meta: {
							title: 'External Link',
							icon: 'link',
						},
					},
				],
			},
			{
				path: '/survey',
				name: 'survey',
				hidden: true,
				type: 'create',
				meta: {
					schemaUrl: '/api/device/schema',
					dataUrl: '/api/device',
				},
			},
		];
 res.send(menu);
});
  • to render page
app.get('/api/application', (req, res) => {
	const application = flexform('application');
	delete application.values.data;
	res.send(application);
});
  • for listing all applications
app.get('/api/applications', (req, res) => {
	const application = flexform('application');
	res.send(applications);
});
  • show one application
app.get('/api/application/:id', (req, res) => {
	const applications = flexform('applications');
	const application = applications[req.param['id']] || {};
	res.send(application);
});
  • for creating new application
app.post('/api/application', (req, res) => {
	let new_application = req.body;
	SR.API.UPDATE_FIELD(
		{
			form: 'application',
			values: new_application
		},
		(err, result) => {
			if (err) {
				LOG.error(err);
				return res.send(err);
			}
			return res.send(result);
		}
	);
});
  • for updating an application
app.put('/api/application/:id', (req, res) => {
	let new_application = req.body;
	const id = req.params['id'];
	SR.API.UPDATE_FIELD(
		{
			form: 'application',
			record_id: id,
			values: new_application
		},
		(err, result) => {
			if (err) {
				LOG.error(err);
				return res.send(err);
			}
			return res.send(result);
		}
	);
});
  • for filtering data with login account
app.get('/api/class', (req, res) => {
	let user = l_checkLogin(req);
	let query = {};
	if (user.account && user.account !== 'admin') {
		query.teacher = user.account;
	}
	let controller = SR.Flexform.controllers['class'];
	controller.find({ query });
	controller.populated();
	res.send(controller);
});
  • for setting default_value with login account
app.get('/api/class/schema', (req, res) => {
	let user = l_checkLogin(req);

	let controller = SR.Flexform.controllers['class'];
	controller.schema();
	controller.populated();
	if (user.account && user.account !== 'admin') {
		controller.data.fields = controller.data.fields.map(field => {
			if (field.id === 'teacher') {
				return Object.assign({}, field, {
					default_value: user.account
				});
			} else {
				return field;
			}
		});
	}
	res.send(controller);
});
  • for filtering data with login account
app.get('/api/class', (req, res) => {
	let user = l_checkLogin(req);
	let query = {};
	if (user.account && user.account !== 'admin') {
		query.teacher = user.account;
	}
	let controller = SR.Flexform.controllers['class'];
	controller.find({ query });
	controller.populated();
	res.send(controller);
});
  • for setting default_value with login account
app.get('/api/class/schema', (req, res) => {
	let user = l_checkLogin(req);

	let controller = SR.Flexform.controllers['class'];
	controller.schema();
	controller.populated();
	if (user.account && user.account !== 'admin') {
		controller.data.fields = controller.data.fields.map(field => {
			if (field.id === 'teacher') {
				return Object.assign({}, field, {
					default_value: user.account
				});
			} else {
				return field;
			}
		});
	}
	res.send(controller);
});
  • vue-router structure
/**
* hidden: true            		if `hidden:true` will not show in the sidebar(default is false)
* alwaysShow: true        		if set true, will always show the root menu, whatever its child routes length
*                         		if not set alwaysShow, only more than one route under the children
*                         		it will becomes nested mode, otherwise not show the root menu
* redirect: noredirect    		if `redirect:noredirect` will no redirect in the breadcrumb
* name:'router-name'      		the name is used by <keep-alive> (must set!!!)
* meta : {
    title: 'title'        		the name show in subMenu and breadcrumb (recommend set)
    icon: 'svg-name'      		the icon show in the sidebar
    breadcrumb: false     		if false, the item will hidden in breadcrumb(default is true)
  }
* path: '/item1'           		show on url
* component: Layout       		if this item is displayed in menu, must add this
**/
    {
        path: '/example',
        component: Layout,
        redirect: '/example/table', //redirect to 1st item in level 2 sidebar
        name: 'Example',
        meta: { title: 'Example', icon: 'example' },
        children: [ // nested menu (level 2 sidebar)
            {   // leve 2 sidebar item
                path: 'table',
                name: 'Table',
                component: () => import('@/views/table/index'), // show this specific page
                meta: { title: 'Table', icon: 'table' }
            },
            {   // leve 2 sidebar item
                path: 'tree',
                name: 'Tree',
                component: () => import('@/views/tree/index'),
                meta: { title: 'Tree', icon: 'tree' }
            }
        ]
    },

page default actions

  • create:
    • page url: /[model]/create
      • API:
        • render: GET /api/[model]/schema
        • submit: POST /api/[model]
  • update
    • page url: /[model]/update/:id
    • API:
      • render: GET /api/[model]/:id
      • submit: PATCH /api/[model]/:id
  • list
    • page url: /[model]/list
    • API:
      • render: GET /api/[model]

custom page actions

To fetch custom API, add API url in meta under your menu entry like:

  {
    path: '/survey',
    name: 'survey',
    hidden: true,
    type: 'create',
    meta: {
      schemaUrl: '/api/device/schema',
      submitUrl: '/api/device',
    },
  },

Now support:

  • Create page

    • schemaUrl : The API to generate fields
    • submitUrl : The API to submit data
  • Update page

    • schemaUrl : The API to generate and fill fields
    • submitUrl : The API to submit data
  • Detail page

    • schemaUrl : The API to generate and fill fields

Permission

Permission switch

add the config below into project settings(pathtoproject/settings.js) to turn on permission.

Flexform: {
  permission: 'on'
}

Permission whitelist

Assign some pages which can be access without login

Flexform: {
  permission: 'on',
  whitelist: ['/survey']
}

And add a route item as well.

//...
{
  url: '/survey',
  type: 'create',
  hidden: true
},
//...

create account

visit the url: /register to add a new account

Account manager

login with admin to access the accounts managemant page which will show on the menu

Customized fields in account

edit _account.js file under project/api/models to create customized fields in account.

A field named in ['account', 'password', 'email', 'roles', 'name'] will be ignored.

module.exports = {
	name: '_account',
	fields: {
		dept: {
			name: 'departmant',
			type: 'string',
			desc: 'departmant',
			require: false
		}
	}
};

roles

edit _role.js file under project/api/ to edit roles.

module.exports = [
	{
		name: 'admin',
		label: '管理員'
	},
	{
		name: 'user',
		label: '使用者',
		default: true
	}
];

menu permission

all menu can be access by admin account

edit api/menu route in lobby/router.js

add roles: [] in any menu element's meta property to config the access permission

roles should be an array of strings which are from _role.js 's name

example:

 ...
 path: '/class',
 redirect: '/class',
 name: 'Class Management',
 meta: {
  roles: ['manager', 'teacher']
 }
 ...

Custom Logic

You can customize the behavior for each view of the form, whether it is before-submit, or after-submit.

Simply follow the format below in your server-side logic (usually lobby/handler.js file) and you can customize what happens before and after form submissions.

Use onDone() to return data to frontend

onDone() has two parameters. The first is the the error message you want to deliver. The second is the data that will be sent to the frontend.

Currently, the custom data only supports the key-pair value format, and the key should be downloadUrl. For further works, we will design a more

general method to allow forntend to run custom logic sent by the server.

Notice that only the return value returned by the first onDone() will be combined with the initial response data.

example:

addFlexformLogic

SR.API.addFlexformLogic(
	'`form_name`',
	function(record_id, record, onDone) {
		LOG.warn('executing custom logic for record_id: ' + record_id);
		LOG.warn('record content:');
		LOG.warn(record);

		// some custom logic...

		// When everything is done, the frontend will automactically request the download url received from the backend.
		onDone(null, {downloadUrl: "http://example.com/hello.txt"});
	},
	function(err) {
		// error when adding the new logic
		if (err) {
			return LOG.error(err);
		}
	}
);

addFlexflowLogic

SR.API.addFlexflowLogic(
    '`flow_name`',
    function (next_step, flow_name, flow_record_id, forms, onDone) {
        /// TODO: your code...
        console.log(`mff_crop : \n${flow_name}
            \n${flow_record_id}
            \n${JSON.stringify(forms)}
	        \n${JSON.stringify(next_step)}`)
        onDone();
    }, function (err) {
        // error when adding the new logic
        if (err) {
            return LOG.error(err);
        }
    }
);

Dashboard display

You can customise the data displayed in the dashboard page

Add dashboard path in the router file of the project (lobby/router.js) to init the dashboard api

app.get('/api/menu', (req, res) => {
        const menu = [
            {
                path: '/',
                redirect: '/dashboard',
                name: 'Home',
                hidden: true,
                children: [
                    {
                        path: 'dashboard',
                        name: 'dashboard',
                        type: 'dashboard',
                        meta: {
                            title: 'dashboard',
                            schemaUrl: '/api/dashboard_cus'
                        }
                    },
                ],
			},
	
	//...
}

Then you need to create a dashboard model in model folder of the project (like this model/dashboard.js) and you can customise what will be display in the dashboard.

module.exports = {
    name: 'admin',
    fields: {
		// info fields will display the account and role info 
        info: {
            name: 'info',
            type: 'string',
            data: 'account_info',
            desc: '',
            must: true,
		},
		
		// the fields that has `name: 'stats'` will show the data of the others model and it statistic in the dashboard as you define like this 
		// the currennt dashboard model only support 2 type of name is 'info' and 'stats'
        mff_crop: {
            name: 'stats',
            type: 'table',
            data: 'mff_crop', // name of the model table you want to get data from 
			stage: ['pending', 'done', 'closed'], // stage of the data that you want to show like pending, done or close in array datatype 
            desc: 'Show number of open and close application', // customise text display in dashboard 
            must: true,
        },
        mff_reg_ins: {
            name: 'stats',
            type: 'table',
            data: 'mff_reg_ins',
            stage: ['pending'],
			desc: 'Display text',
			onData: '',
            must: true,
        },
    }
}

Create dashboard customise api as sample below and add api url

app.get('/api/dashboard_cus', (req, res, next) => {
        let dashboard_controller = new SR.Flexform.controller('dashboard');
        const found_account = l_checkLogin(req).account;

        dashboard_controller.find();

        let num = Object.keys(basic_info_GC_controller.data.values).length;
		let fields = dashboard_controller.data.fields
		
        Object.keys(fields).forEach(key => {
            if(fields[key].name === 'stats') {
                fields[key].onData = num;
            }
            if(fields[key].name === 'info') {
                fields[key].onData = found_account;
            }
        })
        
        res.send(dashboard_controller)
	})

Redirect after form created

Add 'afterSubmit: 'url'' in meta field of the api

	const menu = [
			{
				path: '/salary_filter',
				redirect: '/salary_filter',
				name: 'salary track',
				meta: {
					title: 'Salary Filter',
					icon: 'salary_filter',
				},
				children: [
					{
						path: 'create',
						name: 'Salary Filter',
						type: 'create',
						props: {
							edit: true,
						},
						meta: {
							title: 'Salary Filter',
							icon: 'edit',
							isUpdate: false,
							roles: ['admin'],
							afterSubmit: '/salary_sum_record/list',
						},
					},
				],
			},
	]

Customise button in list view

You can create customise button beside default button to use in list view. The button will display list of data record related to the the target record. And You can also redirect button to other list view

const menu = [
	{
		path: '/progress',
		redirect: '/progress',
		name: 'progress',
		meta: {
			title: 'Progress',
			icon: 'progress',
		},
		children: [
			{
				path: 'list',
				name: 'get track list',
				type: 'list',
				props: {
					edit: true,
				},
				meta: {
					title: 'Track List',
					icon: 'edit',
					roles: ['admin'],
					extra_btn: [		// Set up customise button with name, url path of button, button name 
						{
							name: 'check',
							button_name: 'custom',
							// path: 'custom/',
							// redirect: '/salary_sum_record/list'     // Redirect button function to other view with url 

							// 'custom_path' set up a custom function to return a custom url and query params 
							custom_path: `(record) => {                
								var query = {
									filter: record.account
								};
								return {url: '/search_care/list_detail', query}
							}`

							// 'custom_path' can redirect to a custom url list review and to a specific tab of the view by add the tab name in the end of the url + '/tab+name' 
							custom_path: `(record) => {
								var url_str = '/' + record.flow_name + '/' + 
                                                record.flow_step + flow_step[record.flow_step] + record.flow_record_id + '/crop_info_GC' ;
								return {url: url_str}
							}`
							
						},
					],
					actions: {				// option disable default button
						edit: 'disabled',			
						delete: 'disabled'
					},
				},
			},
		],
	},
]

Bar chart

Install and use chartjs and vue-chartjs package Now chart is fixed using the api data

Need to import the '@/components/BarChart' view to use and display

<bar-chart
		:chartData="arrData" 
		:options="chartOptions" 
		:chartColors="positiveChartColors" 
		label="Positive">
</bar-chart>
import BarChart from '@/components/BarChart';

axios.get("https://api.covidtracking.com/v1/us/daily.json")
			.then(res1 => {
				let data = res.data
				data.forEach(d => {
						const col = d.name
						const {
							stat
						} = d;
						this.arrData.push({col, total: stat});
				})
			})

Register onclick callback on menu tab

The onclick callback mechanism can help you process some business logic when the menu tab is pressed. Currently, the parameter of callback function is the user account.

// ...
meta: {
    title: '第一次定檢',
    icon: 'create',
    flow_name: 'mff_reg_ins',
    onclick: `
    (useraccount) => {
        this.$alert('Onclick!!!', useraccount, {
            type: 'info',
            confirmButtonText: ''
        });
    }`
},
// ...

Dependencies (38)

Dev Dependencies (0)

    Package Sidebar

    Install

    npm i scalra-flexform

    Weekly Downloads

    4

    Version

    2.2.0

    License

    AGPL-3.0

    Unpacked Size

    1.2 MB

    Total Files

    188

    Last publish

    Collaborators

    • bluet
    • syhu
    • sunnyworm
    • kenornotes
    • kevinho627627