scalra-flexform
v2.2.0
Published
A flexible form module for Scalra framework
Downloads
38
Readme
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
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: '[email protected]',
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]
- render: GET
- API:
- page url:
- update
- page url:
/[model]/update/:id
- API:
- render: GET
/api/[model]/:id
- submit: PATCH
/api/[model]/:id
- render: GET
- page url:
- list
- page url:
/[model]/list
- API:
- render: GET
/api/[model]
- render: GET
- page url:
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 fieldssubmitUrl
: The API to submit data
Update page
schemaUrl
: The API to generate and fill fieldssubmitUrl
: 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: ''
});
}`
},
// ...