Source: saltcorn-data/base-plugin/types.js

  1. /**
  2. * Embedded Types definition.
  3. *
  4. * More types can be added by plugin store mechanism https://store.saltcorn.com/
  5. */
  6. const moment = require("moment");
  7. const {
  8. input,
  9. select,
  10. option,
  11. text,
  12. div,
  13. h3,
  14. a,
  15. i,
  16. button,
  17. textarea,
  18. span,
  19. img,
  20. text_attr,
  21. } = require("@saltcorn/markup/tags");
  22. const { contract, is } = require("contractis");
  23. const { radio_group } = require("@saltcorn/markup/helpers");
  24. const isdef = (x) => (typeof x === "undefined" || x === null ? false : true);
  25. const eqStr = (x, y) => `${x}` === `${y}`;
  26. const getStrOptions = (v, optsStr) =>
  27. typeof optsStr === "string"
  28. ? optsStr
  29. .split(",")
  30. .map((o) => o.trim())
  31. .map((o) =>
  32. option(
  33. { value: text_attr(o), ...(eqStr(v, o) && { selected: true }) },
  34. text_attr(o)
  35. )
  36. )
  37. : optsStr.map((o, ix) =>
  38. o && typeof o.name !== "undefined" && typeof o.label !== "undefined"
  39. ? option(
  40. {
  41. value: o.name,
  42. ...((eqStr(v, o.name) ||
  43. (ix === 0 && typeof v === "undefined" && o.disabled)) && {
  44. selected: true,
  45. }),
  46. ...(o.disabled && { disabled: true }),
  47. },
  48. o.label
  49. )
  50. : option({ value: o, ...(eqStr(v, o) && { selected: true }) }, o)
  51. );
  52. /**
  53. * String type
  54. * @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}))}}
  55. */
  56. const string = {
  57. name: "String",
  58. sql_name: "text",
  59. attributes: [
  60. {
  61. name: "regexp",
  62. type: "String",
  63. required: false,
  64. sublabel: "Match regular expression",
  65. validator(s) {
  66. if (!is_valid_regexp(s)) return "Not a valid Regular Expression";
  67. },
  68. },
  69. {
  70. name: "re_invalid_error",
  71. type: "String",
  72. required: false,
  73. sublabel: "Error message when regular expression does not match",
  74. },
  75. {
  76. name: "max_length",
  77. type: "Integer",
  78. required: false,
  79. sublabel: "The maximum number of characters in the string",
  80. },
  81. {
  82. name: "min_length",
  83. type: "Integer",
  84. required: false,
  85. sublabel: "The minimum number of characters in the string",
  86. },
  87. {
  88. name: "options",
  89. type: "String",
  90. required: false,
  91. sublabel:
  92. '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.',
  93. },
  94. ],
  95. contract: ({ options }) =>
  96. typeof options === "string"
  97. ? is.one_of(options.split(","))
  98. : typeof options === "undefined"
  99. ? is.str
  100. : is.one_of(options.map((o) => (typeof o === "string" ? o : o.name))),
  101. fieldviews: {
  102. as_text: { isEdit: false, run: (s) => text_attr(s || "") },
  103. as_link: {
  104. isEdit: false,
  105. run: (s) => a({ href: text(s || "") }, text_attr(s || "")),
  106. },
  107. img_from_url: {
  108. isEdit: false,
  109. run: (s, req, attrs) => img({ src: text(s || ""), style: "width:100%" }),
  110. },
  111. as_header: { isEdit: false, run: (s) => h3(text_attr(s || "")) },
  112. edit: {
  113. isEdit: true,
  114. configFields: (field) => [
  115. ...(field.attributes.options &&
  116. field.attributes.options.length > 0 &&
  117. !field.required
  118. ? [
  119. {
  120. name: "neutral_label",
  121. label: "Neutral label",
  122. type: "String",
  123. },
  124. {
  125. name: "force_required",
  126. label: "Required",
  127. sublabel:
  128. "User must select a value, even if the table field is not required",
  129. type: "Bool",
  130. },
  131. ]
  132. : []),
  133. {
  134. name: "placeholder",
  135. label: "Placeholder",
  136. type: "String",
  137. },
  138. ],
  139. run: (nm, v, attrs, cls, required, field) =>
  140. attrs.options && (attrs.options.length > 0 || !required)
  141. ? select(
  142. {
  143. class: ["form-control", cls],
  144. name: text_attr(nm),
  145. "data-fieldname": text_attr(field.name),
  146. id: `input${text_attr(nm)}`,
  147. disabled: attrs.disabled,
  148. },
  149. required || attrs.force_required
  150. ? getStrOptions(v, attrs.options)
  151. : [
  152. option({ value: "" }, attrs.neutral_label || ""),
  153. ...getStrOptions(v, attrs.options),
  154. ]
  155. )
  156. : attrs.options
  157. ? i("None available")
  158. : attrs.calcOptions
  159. ? select(
  160. {
  161. class: ["form-control", cls],
  162. name: text_attr(nm),
  163. disabled: attrs.disabled,
  164. "data-fieldname": text_attr(field.name),
  165. id: `input${text_attr(nm)}`,
  166. "data-selected": v,
  167. "data-calc-options": encodeURIComponent(
  168. JSON.stringify(attrs.calcOptions)
  169. ),
  170. },
  171. option({ value: "" }, "")
  172. )
  173. : input({
  174. type: "text",
  175. disabled: attrs.disabled,
  176. class: ["form-control", cls],
  177. placeholder: attrs.placeholder,
  178. "data-fieldname": text_attr(field.name),
  179. name: text_attr(nm),
  180. id: `input${text_attr(nm)}`,
  181. ...(isdef(v) && { value: text_attr(v) }),
  182. }),
  183. },
  184. textarea: {
  185. isEdit: true,
  186. run: (nm, v, attrs, cls, required, field) =>
  187. textarea(
  188. {
  189. class: ["form-control", cls],
  190. name: text_attr(nm),
  191. "data-fieldname": text_attr(field.name),
  192. disabled: attrs.disabled,
  193. id: `input${text_attr(nm)}`,
  194. rows: 5,
  195. },
  196. text(v) || ""
  197. ),
  198. },
  199. radio_group: {
  200. isEdit: true,
  201. run: (nm, v, attrs, cls, required, field) =>
  202. attrs.options
  203. ? radio_group({
  204. class: cls,
  205. name: text_attr(nm),
  206. disabled: attrs.disabled,
  207. options: attrs.options.split(",").map((o) => o.trim()),
  208. value: v,
  209. })
  210. : i("None available"),
  211. },
  212. password: {
  213. isEdit: true,
  214. run: (nm, v, attrs, cls, required, field) =>
  215. input({
  216. type: "password",
  217. disabled: attrs.disabled,
  218. class: ["form-control", cls],
  219. "data-fieldname": text_attr(field.name),
  220. name: text_attr(nm),
  221. id: `input${text_attr(nm)}`,
  222. ...(isdef(v) && { value: text_attr(v) }),
  223. }),
  224. },
  225. },
  226. read: (v) => {
  227. switch (typeof v) {
  228. case "string":
  229. //PG dislikes null bytes
  230. return v.replace(/\0/g, "");
  231. default:
  232. return undefined;
  233. }
  234. },
  235. presets: {
  236. IP: ({ req }) => req.ip,
  237. SessionID: ({ req }) => req.sessionID || req.cookies['express:sess'],
  238. },
  239. validate: ({ min_length, max_length, regexp, re_invalid_error }) => (x) => {
  240. if (!x || typeof x !== "string") return true; //{ error: "Not a string" };
  241. if (isdef(min_length) && x.length < min_length)
  242. return { error: `Must be at least ${min_length} characters` };
  243. if (isdef(max_length) && x.length > max_length)
  244. return { error: `Must be at most ${max_length} characters` };
  245. if (isdef(regexp) && !new RegExp(regexp).test(x))
  246. return { error: re_invalid_error || `Does not match regular expression` };
  247. return true;
  248. },
  249. validate_attributes: ({ min_length, max_length, regexp }) =>
  250. (!isdef(min_length) || !isdef(max_length) || max_length >= min_length) &&
  251. (!isdef(regexp) || is_valid_regexp(regexp)),
  252. };
  253. const is_valid_regexp = (s) => {
  254. try {
  255. new RegExp(s);
  256. return true;
  257. } catch {
  258. return false;
  259. }
  260. };
  261. /**
  262. * Integer type
  263. * @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))}}
  264. */
  265. const int = {
  266. name: "Integer",
  267. sql_name: "int",
  268. contract: ({ min, max }) => is.integer({ lte: max, gte: min }),
  269. primaryKey: { sql_type: "serial" },
  270. fieldviews: {
  271. show: { isEdit: false, run: (s) => text(s) },
  272. edit: {
  273. isEdit: true,
  274. run: (nm, v, attrs, cls, required, field) =>
  275. input({
  276. type: "number",
  277. class: ["form-control", cls],
  278. disabled: attrs.disabled,
  279. "data-fieldname": text_attr(field.name),
  280. name: text_attr(nm),
  281. id: `input${text_attr(nm)}`,
  282. step: "1",
  283. ...(attrs.max && { max: attrs.max }),
  284. ...(attrs.min && { min: attrs.min }),
  285. ...(isdef(v) && { value: text_attr(v) }),
  286. }),
  287. },
  288. },
  289. attributes: [
  290. { name: "max", type: "Integer", required: false },
  291. { name: "min", type: "Integer", required: false },
  292. ],
  293. validate_attributes: ({ min, max }) =>
  294. !isdef(min) || !isdef(max) || max > min,
  295. read: (v) => {
  296. switch (typeof v) {
  297. case "number":
  298. return Math.round(v);
  299. case "string":
  300. if (v === "") return undefined;
  301. const parsed = +v;
  302. return isNaN(parsed) ? undefined : parsed;
  303. default:
  304. return undefined;
  305. }
  306. },
  307. validate: ({ min, max }) => (x) => {
  308. if (isdef(min) && x < min) return { error: `Must be ${min} or higher` };
  309. if (isdef(max) && x > max) return { error: `Must be ${max} or less` };
  310. return true;
  311. },
  312. };
  313. /**
  314. * Color Type
  315. * @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)}}
  316. */
  317. const color = {
  318. name: "Color",
  319. sql_name: "text",
  320. contract: () => is.str,
  321. fieldviews: {
  322. show: {
  323. isEdit: false,
  324. run: (s) =>
  325. s
  326. ? div({
  327. class: "color-type-show",
  328. style: `background: ${s};`,
  329. })
  330. : "",
  331. },
  332. edit: {
  333. isEdit: true,
  334. run: (nm, v, attrs, cls, required, field) =>
  335. input({
  336. type: "color",
  337. class: ["form-control", cls],
  338. disabled: attrs.disabled,
  339. "data-fieldname": text_attr(field.name),
  340. name: text_attr(nm),
  341. id: `input${text_attr(nm)}`,
  342. ...(isdef(v) && { value: text_attr(v) }),
  343. }),
  344. },
  345. },
  346. attributes: [],
  347. read: (v) => {
  348. switch (typeof v) {
  349. case "string":
  350. return v;
  351. default:
  352. return undefined;
  353. }
  354. },
  355. validate: () => (x) => {
  356. return true;
  357. },
  358. };
  359. /**
  360. * Float type
  361. * @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))}}
  362. */
  363. const float = {
  364. name: "Float",
  365. sql_name: "double precision",
  366. contract: ({ min, max }) => is.number({ lte: max, gte: min }),
  367. fieldviews: {
  368. show: { isEdit: false, run: (s) => text(s) },
  369. edit: {
  370. isEdit: true,
  371. run: (nm, v, attrs, cls, required, field) =>
  372. input({
  373. type: "number",
  374. class: ["form-control", cls],
  375. name: text_attr(nm),
  376. "data-fieldname": text_attr(field.name),
  377. disabled: attrs.disabled,
  378. step: attrs.decimal_places
  379. ? Math.pow(10, -attrs.decimal_places)
  380. : "0.01",
  381. id: `input${text_attr(nm)}`,
  382. ...(attrs.max && { max: attrs.max }),
  383. ...(attrs.min && { min: attrs.min }),
  384. ...(isdef(v) && { value: text_attr(v) }),
  385. }),
  386. },
  387. },
  388. attributes: [
  389. { name: "max", type: "Float", required: false },
  390. { name: "min", type: "Float", required: false },
  391. { name: "units", type: "String", required: false },
  392. { name: "decimal_places", type: "Integer", required: false },
  393. ],
  394. read: (v) => {
  395. switch (typeof v) {
  396. case "number":
  397. return v;
  398. case "string":
  399. const parsed = parseFloat(v);
  400. return isNaN(parsed) ? undefined : parsed;
  401. default:
  402. return undefined;
  403. }
  404. },
  405. validate: ({ min, max }) => (x) => {
  406. if (isdef(min) && x < min) return { error: `Must be ${min} or higher` };
  407. if (isdef(max) && x > max) return { error: `Must be ${max} or less` };
  408. return true;
  409. },
  410. };
  411. const locale = (req) => {
  412. //console.log(req && req.getLocale ? req.getLocale() : undefined);
  413. return req && req.getLocale ? req.getLocale() : undefined;
  414. };
  415. const logit = (x) => {
  416. console.log(x);
  417. return x;
  418. };
  419. /**
  420. * Date type
  421. * @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(*=): *)}}
  422. */
  423. const date = {
  424. name: "Date",
  425. sql_name: "timestamp",
  426. contract: () => is.date,
  427. attributes: [],
  428. fieldviews: {
  429. show: {
  430. isEdit: false,
  431. run: (d, req) =>
  432. text(
  433. typeof d === "string"
  434. ? text(d)
  435. : d && d.toLocaleString
  436. ? d.toLocaleString(locale(req))
  437. : ""
  438. ),
  439. },
  440. showDay: {
  441. isEdit: false,
  442. run: (d, req) =>
  443. text(
  444. typeof d === "string"
  445. ? text(d)
  446. : d && d.toLocaleDateString
  447. ? d.toLocaleDateString(locale(req))
  448. : ""
  449. ),
  450. },
  451. format: {
  452. isEdit: false,
  453. configFields: [
  454. {
  455. name: "format",
  456. label: "Format",
  457. type: "String",
  458. sublabel: "moment.js format specifier",
  459. },
  460. ],
  461. run: (d, req, options) => {
  462. if (!d) return "";
  463. if (!options || !options.format) return text(moment(d).format());
  464. return text(moment(d).format(options.format));
  465. },
  466. },
  467. relative: {
  468. isEdit: false,
  469. run: (d, req) => {
  470. if (!d) return "";
  471. const loc = locale(req);
  472. if (loc) return text(moment(d).locale(loc).fromNow());
  473. else return text(moment(d).fromNow());
  474. },
  475. },
  476. yearsAgo: {
  477. isEdit: false,
  478. run: (d, req) => {
  479. if (!d) return "";
  480. return text(moment.duration(new Date() - d).years());
  481. },
  482. },
  483. edit: {
  484. isEdit: true,
  485. run: (nm, v, attrs, cls, required, field) =>
  486. input({
  487. type: "text",
  488. class: ["form-control", cls],
  489. "data-fieldname": text_attr(field.name),
  490. name: text_attr(nm),
  491. disabled: attrs.disabled,
  492. id: `input${text_attr(nm)}`,
  493. ...(isdef(v) && {
  494. value: text_attr(
  495. typeof v === "string" ? v : v.toLocaleString(attrs.locale)
  496. ),
  497. }),
  498. }),
  499. },
  500. editDay: {
  501. isEdit: true,
  502. run: (nm, v, attrs, cls, required, field) =>
  503. input({
  504. type: "text",
  505. class: ["form-control", cls],
  506. "data-fieldname": text_attr(field.name),
  507. name: text_attr(nm),
  508. disabled: attrs.disabled,
  509. id: `input${text_attr(nm)}`,
  510. ...(isdef(v) && {
  511. value: text_attr(
  512. typeof v === "string" ? v : v.toLocaleDateString(attrs.locale)
  513. ),
  514. }),
  515. }),
  516. },
  517. },
  518. presets: {
  519. Now: () => new Date(),
  520. },
  521. read: (v, attrs) => {
  522. if (v instanceof Date && !isNaN(v)) return v;
  523. if (typeof v === "string") {
  524. if (attrs && attrs.locale) {
  525. const d = moment(v, "L LT", attrs.locale).toDate();
  526. if (d instanceof Date && !isNaN(d)) return d;
  527. }
  528. const d = new Date(v);
  529. if (d instanceof Date && !isNaN(d)) return d;
  530. else return null;
  531. }
  532. },
  533. validate: ({}) => (v) => v instanceof Date && !isNaN(v),
  534. };
  535. /**
  536. * Boolean Type
  537. * @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)}}
  538. */
  539. const bool = {
  540. name: "Bool",
  541. sql_name: "boolean",
  542. contract: () => is.bool,
  543. fieldviews: {
  544. show: {
  545. isEdit: false,
  546. run: (v) =>
  547. v === true
  548. ? i({
  549. class: "fas fa-lg fa-check-circle text-success",
  550. })
  551. : v === false
  552. ? i({
  553. class: "fas fa-lg fa-times-circle text-danger",
  554. })
  555. : "",
  556. },
  557. checkboxes: {
  558. isEdit: false,
  559. run: (v) =>
  560. v === true
  561. ? input({ disabled: true, type: "checkbox", checked: true })
  562. : v === false
  563. ? input({ type: "checkbox", disabled: true })
  564. : "",
  565. },
  566. TrueFalse: {
  567. isEdit: false,
  568. run: (v) => (v === true ? "True" : v === false ? "False" : ""),
  569. },
  570. edit: {
  571. isEdit: true,
  572. run: (nm, v, attrs, cls, required, field) =>
  573. input({
  574. class: ["form-check-input", cls],
  575. "data-fieldname": text_attr(field.name),
  576. type: "checkbox",
  577. disabled: attrs.disabled,
  578. name: text_attr(nm),
  579. id: `input${text_attr(nm)}`,
  580. ...(v && { checked: true }),
  581. }),
  582. },
  583. tristate: {
  584. isEdit: true,
  585. run: (nm, v, attrs, cls, required, field) =>
  586. attrs.disabled
  587. ? !(!isdef(v) || v === null)
  588. ? ""
  589. : v
  590. ? "T"
  591. : "F"
  592. : input({
  593. type: "hidden",
  594. "data-fieldname": text_attr(field.name),
  595. name: text_attr(nm),
  596. id: `input${text_attr(nm)}`,
  597. value: !isdef(v) || v === null ? "?" : v ? "on" : "off",
  598. }) +
  599. button(
  600. {
  601. onClick: `tristateClick('${text_attr(nm)}')`,
  602. type: "button",
  603. id: `trib${text_attr(nm)}`,
  604. },
  605. !isdef(v) || v === null ? "?" : v ? "T" : "F"
  606. ),
  607. },
  608. },
  609. attributes: [],
  610. readFromFormRecord: (rec, name) => {
  611. if (!rec[name]) return false;
  612. if (["undefined", "false", "off"].includes(rec[name])) return false;
  613. if (rec[name] === "?") return null;
  614. return rec[name] ? true : false;
  615. },
  616. read: (v) => {
  617. switch (typeof v) {
  618. case "string":
  619. if (["TRUE", "T", "ON"].includes(v.toUpperCase())) return true;
  620. if (v === "?") return null;
  621. else return false;
  622. default:
  623. if (v === null) return null;
  624. return v ? true : false;
  625. }
  626. },
  627. readFromDB: (v) => !!v,
  628. listAs: (v) => JSON.stringify(v),
  629. validate: () => (x) => true,
  630. };
  631. module.exports = { string, int, bool, date, float, color };