/**
* Embedded Types definition.
*
* More types can be added by plugin store mechanism https://store.saltcorn.com/
*/
const moment = require("moment");
const {
input,
select,
option,
text,
div,
h3,
a,
i,
button,
textarea,
span,
img,
text_attr,
} = require("@saltcorn/markup/tags");
const { contract, is } = require("contractis");
const { radio_group } = require("@saltcorn/markup/helpers");
const isdef = (x) => (typeof x === "undefined" || x === null ? false : true);
const eqStr = (x, y) => `${x}` === `${y}`;
const getStrOptions = (v, optsStr) =>
typeof optsStr === "string"
? optsStr
.split(",")
.map((o) => o.trim())
.map((o) =>
option(
{ value: text_attr(o), ...(eqStr(v, o) && { selected: true }) },
text_attr(o)
)
)
: optsStr.map((o, ix) =>
o && typeof o.name !== "undefined" && typeof o.label !== "undefined"
? option(
{
value: o.name,
...((eqStr(v, o.name) ||
(ix === 0 && typeof v === "undefined" && o.disabled)) && {
selected: true,
}),
...(o.disabled && { disabled: true }),
},
o.label
)
: option({ value: o, ...(eqStr(v, o) && { selected: true }) }, o)
);
/**
* String type
* @type {{read: ((function(*=): (*))|*), presets: {IP: (function({req: *}): string), SessionID: (function({req: *}))}, fieldviews: {as_text: {isEdit: boolean, run: (function(*=): string|string)}, as_link: {isEdit: boolean, run: (function(*=): string)}, as_header: {isEdit: boolean, run: (function(*=): string)}, password: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}, img_from_url: {isEdit: boolean, run: (function(*=, *, *): string)}, edit: {isEdit: boolean, run: (function(*=, *=, *, *, *=, *): string), configFields: (function(*): [...[{name: string, label: string, type: string}, {name: string, label: string, type: string, sublabel: string}]|*[], {name: string, label: string, type: string}])}, radio_group: {isEdit: boolean, run: (function(*=, *=, *, *=, *, *): *|string)}, textarea: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}}, contract: (function({options?: *}): (function(*=): *)|*), name: string, attributes: [{name: string, validator(*=): (string|undefined), type: string, required: boolean, sublabel: string}, {name: string, type: string, required: boolean, sublabel: string}, {name: string, type: string, required: boolean, sublabel: string}, {name: string, type: string, required: boolean, sublabel: string}, {name: string, type: string, required: boolean, sublabel: string}], sql_name: string, validate_attributes: (function({min_length?: *, max_length?: *, regexp?: *})), validate: (function({min_length?: *, max_length?: *, regexp?: *, re_invalid_error?: *}): function(*=): (boolean|{error: string}))}}
*/
const string = {
name: "String",
sql_name: "text",
attributes: [
{
name: "regexp",
type: "String",
required: false,
sublabel: "Match regular expression",
validator(s) {
if (!is_valid_regexp(s)) return "Not a valid Regular Expression";
},
},
{
name: "re_invalid_error",
type: "String",
required: false,
sublabel: "Error message when regular expression does not match",
},
{
name: "max_length",
type: "Integer",
required: false,
sublabel: "The maximum number of characters in the string",
},
{
name: "min_length",
type: "Integer",
required: false,
sublabel: "The minimum number of characters in the string",
},
{
name: "options",
type: "String",
required: false,
sublabel:
'Use this to restrict your field to a list of options (separated by commas). For instance, if the permissible values are "Red", "Green" and "Blue", enter "Red, Green, Blue" here. Leave blank if the string can hold any value.',
},
],
contract: ({ options }) =>
typeof options === "string"
? is.one_of(options.split(","))
: typeof options === "undefined"
? is.str
: is.one_of(options.map((o) => (typeof o === "string" ? o : o.name))),
fieldviews: {
as_text: { isEdit: false, run: (s) => text_attr(s || "") },
as_link: {
isEdit: false,
run: (s) => a({ href: text(s || "") }, text_attr(s || "")),
},
img_from_url: {
isEdit: false,
run: (s, req, attrs) => img({ src: text(s || ""), style: "width:100%" }),
},
as_header: { isEdit: false, run: (s) => h3(text_attr(s || "")) },
edit: {
isEdit: true,
configFields: (field) => [
...(field.attributes.options &&
field.attributes.options.length > 0 &&
!field.required
? [
{
name: "neutral_label",
label: "Neutral label",
type: "String",
},
{
name: "force_required",
label: "Required",
sublabel:
"User must select a value, even if the table field is not required",
type: "Bool",
},
]
: []),
{
name: "placeholder",
label: "Placeholder",
type: "String",
},
],
run: (nm, v, attrs, cls, required, field) =>
attrs.options && (attrs.options.length > 0 || !required)
? select(
{
class: ["form-control", cls],
name: text_attr(nm),
"data-fieldname": text_attr(field.name),
id: `input${text_attr(nm)}`,
disabled: attrs.disabled,
},
required || attrs.force_required
? getStrOptions(v, attrs.options)
: [
option({ value: "" }, attrs.neutral_label || ""),
...getStrOptions(v, attrs.options),
]
)
: attrs.options
? i("None available")
: attrs.calcOptions
? select(
{
class: ["form-control", cls],
name: text_attr(nm),
disabled: attrs.disabled,
"data-fieldname": text_attr(field.name),
id: `input${text_attr(nm)}`,
"data-selected": v,
"data-calc-options": encodeURIComponent(
JSON.stringify(attrs.calcOptions)
),
},
option({ value: "" }, "")
)
: input({
type: "text",
disabled: attrs.disabled,
class: ["form-control", cls],
placeholder: attrs.placeholder,
"data-fieldname": text_attr(field.name),
name: text_attr(nm),
id: `input${text_attr(nm)}`,
...(isdef(v) && { value: text_attr(v) }),
}),
},
textarea: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
textarea(
{
class: ["form-control", cls],
name: text_attr(nm),
"data-fieldname": text_attr(field.name),
disabled: attrs.disabled,
id: `input${text_attr(nm)}`,
rows: 5,
},
text(v) || ""
),
},
radio_group: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
attrs.options
? radio_group({
class: cls,
name: text_attr(nm),
disabled: attrs.disabled,
options: attrs.options.split(",").map((o) => o.trim()),
value: v,
})
: i("None available"),
},
password: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
input({
type: "password",
disabled: attrs.disabled,
class: ["form-control", cls],
"data-fieldname": text_attr(field.name),
name: text_attr(nm),
id: `input${text_attr(nm)}`,
...(isdef(v) && { value: text_attr(v) }),
}),
},
},
read: (v) => {
switch (typeof v) {
case "string":
//PG dislikes null bytes
return v.replace(/\0/g, "");
default:
return undefined;
}
},
presets: {
IP: ({ req }) => req.ip,
SessionID: ({ req }) => req.sessionID || req.cookies['express:sess'],
},
validate: ({ min_length, max_length, regexp, re_invalid_error }) => (x) => {
if (!x || typeof x !== "string") return true; //{ error: "Not a string" };
if (isdef(min_length) && x.length < min_length)
return { error: `Must be at least ${min_length} characters` };
if (isdef(max_length) && x.length > max_length)
return { error: `Must be at most ${max_length} characters` };
if (isdef(regexp) && !new RegExp(regexp).test(x))
return { error: re_invalid_error || `Does not match regular expression` };
return true;
},
validate_attributes: ({ min_length, max_length, regexp }) =>
(!isdef(min_length) || !isdef(max_length) || max_length >= min_length) &&
(!isdef(regexp) || is_valid_regexp(regexp)),
};
const is_valid_regexp = (s) => {
try {
new RegExp(s);
return true;
} catch {
return false;
}
};
/**
* Integer type
* @type {{read: ((function(*=): (number))|*), fieldviews: {edit: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}, show: {isEdit: boolean, run: (function(*=): string|*)}}, contract: (function({min?: *, max?: *}): function(*=): *), name: string, attributes: [{name: string, type: string, required: boolean}, {name: string, type: string, required: boolean}], sql_name: string, validate_attributes: (function({min?: *, max?: *})), primaryKey: {sql_type: string}, validate: (function({min?: *, max?: *}): function(*): ({error: string}|boolean))}}
*/
const int = {
name: "Integer",
sql_name: "int",
contract: ({ min, max }) => is.integer({ lte: max, gte: min }),
primaryKey: { sql_type: "serial" },
fieldviews: {
show: { isEdit: false, run: (s) => text(s) },
edit: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
input({
type: "number",
class: ["form-control", cls],
disabled: attrs.disabled,
"data-fieldname": text_attr(field.name),
name: text_attr(nm),
id: `input${text_attr(nm)}`,
step: "1",
...(attrs.max && { max: attrs.max }),
...(attrs.min && { min: attrs.min }),
...(isdef(v) && { value: text_attr(v) }),
}),
},
},
attributes: [
{ name: "max", type: "Integer", required: false },
{ name: "min", type: "Integer", required: false },
],
validate_attributes: ({ min, max }) =>
!isdef(min) || !isdef(max) || max > min,
read: (v) => {
switch (typeof v) {
case "number":
return Math.round(v);
case "string":
if (v === "") return undefined;
const parsed = +v;
return isNaN(parsed) ? undefined : parsed;
default:
return undefined;
}
},
validate: ({ min, max }) => (x) => {
if (isdef(min) && x < min) return { error: `Must be ${min} or higher` };
if (isdef(max) && x > max) return { error: `Must be ${max} or less` };
return true;
},
};
/**
* Color Type
* @type {{read: ((function(*=): (string))|*), fieldviews: {edit: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}, show: {isEdit: boolean, run: (function(*): string|string)}}, contract: (function(): (function(*=): *)|*), name: string, attributes: *[], sql_name: string, validate: (function(): function(*): boolean)}}
*/
const color = {
name: "Color",
sql_name: "text",
contract: () => is.str,
fieldviews: {
show: {
isEdit: false,
run: (s) =>
s
? div({
class: "color-type-show",
style: `background: ${s};`,
})
: "",
},
edit: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
input({
type: "color",
class: ["form-control", cls],
disabled: attrs.disabled,
"data-fieldname": text_attr(field.name),
name: text_attr(nm),
id: `input${text_attr(nm)}`,
...(isdef(v) && { value: text_attr(v) }),
}),
},
},
attributes: [],
read: (v) => {
switch (typeof v) {
case "string":
return v;
default:
return undefined;
}
},
validate: () => (x) => {
return true;
},
};
/**
* Float type
* @type {{read: ((function(*=): (number))|*), fieldviews: {edit: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}, show: {isEdit: boolean, run: (function(*=): string|*)}}, contract: (function({min?: *, max?: *}): function(*=): *), name: string, attributes: [{name: string, type: string, required: boolean}, {name: string, type: string, required: boolean}, {name: string, type: string, required: boolean}, {name: string, type: string, required: boolean}], sql_name: string, validate: (function({min?: *, max?: *}): function(*): ({error: string}|boolean))}}
*/
const float = {
name: "Float",
sql_name: "double precision",
contract: ({ min, max }) => is.number({ lte: max, gte: min }),
fieldviews: {
show: { isEdit: false, run: (s) => text(s) },
edit: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
input({
type: "number",
class: ["form-control", cls],
name: text_attr(nm),
"data-fieldname": text_attr(field.name),
disabled: attrs.disabled,
step: attrs.decimal_places
? Math.pow(10, -attrs.decimal_places)
: "0.01",
id: `input${text_attr(nm)}`,
...(attrs.max && { max: attrs.max }),
...(attrs.min && { min: attrs.min }),
...(isdef(v) && { value: text_attr(v) }),
}),
},
},
attributes: [
{ name: "max", type: "Float", required: false },
{ name: "min", type: "Float", required: false },
{ name: "units", type: "String", required: false },
{ name: "decimal_places", type: "Integer", required: false },
],
read: (v) => {
switch (typeof v) {
case "number":
return v;
case "string":
const parsed = parseFloat(v);
return isNaN(parsed) ? undefined : parsed;
default:
return undefined;
}
},
validate: ({ min, max }) => (x) => {
if (isdef(min) && x < min) return { error: `Must be ${min} or higher` };
if (isdef(max) && x > max) return { error: `Must be ${max} or less` };
return true;
},
};
const locale = (req) => {
//console.log(req && req.getLocale ? req.getLocale() : undefined);
return req && req.getLocale ? req.getLocale() : undefined;
};
const logit = (x) => {
console.log(x);
return x;
};
/**
* Date type
* @type {{presets: {Now: (function(): Date)}, read: ((function(*=, *=): (Date|null|undefined))|*), fieldviews: {edit: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}, editDay: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}, yearsAgo: {isEdit: boolean, run: ((function(*=, *): (string|string|*))|*)}, show: {isEdit: boolean, run: (function(*=, *=): string|*)}, showDay: {isEdit: boolean, run: (function(*=, *=): string|*)}, format: {isEdit: boolean, run: ((function(*=, *, *=): (string|string|*))|*), configFields: [{name: string, label: string, type: string, sublabel: string}]}, relative: {isEdit: boolean, run: ((function(*=, *=): (string|string|*))|*)}}, contract: (function(): (function(*=): *)|*), name: string, attributes: *[], sql_name: string, validate: (function({}): function(*=): *)}}
*/
const date = {
name: "Date",
sql_name: "timestamp",
contract: () => is.date,
attributes: [],
fieldviews: {
show: {
isEdit: false,
run: (d, req) =>
text(
typeof d === "string"
? text(d)
: d && d.toLocaleString
? d.toLocaleString(locale(req))
: ""
),
},
showDay: {
isEdit: false,
run: (d, req) =>
text(
typeof d === "string"
? text(d)
: d && d.toLocaleDateString
? d.toLocaleDateString(locale(req))
: ""
),
},
format: {
isEdit: false,
configFields: [
{
name: "format",
label: "Format",
type: "String",
sublabel: "moment.js format specifier",
},
],
run: (d, req, options) => {
if (!d) return "";
if (!options || !options.format) return text(moment(d).format());
return text(moment(d).format(options.format));
},
},
relative: {
isEdit: false,
run: (d, req) => {
if (!d) return "";
const loc = locale(req);
if (loc) return text(moment(d).locale(loc).fromNow());
else return text(moment(d).fromNow());
},
},
yearsAgo: {
isEdit: false,
run: (d, req) => {
if (!d) return "";
return text(moment.duration(new Date() - d).years());
},
},
edit: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
input({
type: "text",
class: ["form-control", cls],
"data-fieldname": text_attr(field.name),
name: text_attr(nm),
disabled: attrs.disabled,
id: `input${text_attr(nm)}`,
...(isdef(v) && {
value: text_attr(
typeof v === "string" ? v : v.toLocaleString(attrs.locale)
),
}),
}),
},
editDay: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
input({
type: "text",
class: ["form-control", cls],
"data-fieldname": text_attr(field.name),
name: text_attr(nm),
disabled: attrs.disabled,
id: `input${text_attr(nm)}`,
...(isdef(v) && {
value: text_attr(
typeof v === "string" ? v : v.toLocaleDateString(attrs.locale)
),
}),
}),
},
},
presets: {
Now: () => new Date(),
},
read: (v, attrs) => {
if (v instanceof Date && !isNaN(v)) return v;
if (typeof v === "string") {
if (attrs && attrs.locale) {
const d = moment(v, "L LT", attrs.locale).toDate();
if (d instanceof Date && !isNaN(d)) return d;
}
const d = new Date(v);
if (d instanceof Date && !isNaN(d)) return d;
else return null;
}
},
validate: ({}) => (v) => v instanceof Date && !isNaN(v),
};
/**
* Boolean Type
* @type {{readFromFormRecord: ((function(*, *): (boolean|null|boolean))|*), read: ((function(*=): (boolean|null|boolean))|*), fieldviews: {TrueFalse: {isEdit: boolean, run: (function(*): string)}, tristate: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string|string)}, checkboxes: {isEdit: boolean, run: (function(*): string|string)}, edit: {isEdit: boolean, run: (function(*=, *=, *, *, *, *): string)}, show: {isEdit: boolean, run: (function(*): string|string)}}, contract: (function(): (function(*=): *)|*), name: string, listAs: (function(*=): string), attributes: *[], sql_name: string, readFromDB: (function(*=): boolean), validate: (function(): function(*): boolean)}}
*/
const bool = {
name: "Bool",
sql_name: "boolean",
contract: () => is.bool,
fieldviews: {
show: {
isEdit: false,
run: (v) =>
v === true
? i({
class: "fas fa-lg fa-check-circle text-success",
})
: v === false
? i({
class: "fas fa-lg fa-times-circle text-danger",
})
: "",
},
checkboxes: {
isEdit: false,
run: (v) =>
v === true
? input({ disabled: true, type: "checkbox", checked: true })
: v === false
? input({ type: "checkbox", disabled: true })
: "",
},
TrueFalse: {
isEdit: false,
run: (v) => (v === true ? "True" : v === false ? "False" : ""),
},
edit: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
input({
class: ["form-check-input", cls],
"data-fieldname": text_attr(field.name),
type: "checkbox",
disabled: attrs.disabled,
name: text_attr(nm),
id: `input${text_attr(nm)}`,
...(v && { checked: true }),
}),
},
tristate: {
isEdit: true,
run: (nm, v, attrs, cls, required, field) =>
attrs.disabled
? !(!isdef(v) || v === null)
? ""
: v
? "T"
: "F"
: input({
type: "hidden",
"data-fieldname": text_attr(field.name),
name: text_attr(nm),
id: `input${text_attr(nm)}`,
value: !isdef(v) || v === null ? "?" : v ? "on" : "off",
}) +
button(
{
onClick: `tristateClick('${text_attr(nm)}')`,
type: "button",
id: `trib${text_attr(nm)}`,
},
!isdef(v) || v === null ? "?" : v ? "T" : "F"
),
},
},
attributes: [],
readFromFormRecord: (rec, name) => {
if (!rec[name]) return false;
if (["undefined", "false", "off"].includes(rec[name])) return false;
if (rec[name] === "?") return null;
return rec[name] ? true : false;
},
read: (v) => {
switch (typeof v) {
case "string":
if (["TRUE", "T", "ON"].includes(v.toUpperCase())) return true;
if (v === "?") return null;
else return false;
default:
if (v === null) return null;
return v ? true : false;
}
},
readFromDB: (v) => !!v,
listAs: (v) => JSON.stringify(v),
validate: () => (x) => true,
};
module.exports = { string, int, bool, date, float, color };