Skip to the content

Capsid menu

app/kb/form-fields.mjs

132 lines and 16 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of form-fields.mjs
FunctionLinesComplexityCoveredCRAPMutation
keyWords15 to 191100%OkCRAP 1No data: no mutants in this code
<anonymous:18>18 to 181100%OkCRAP 1No data: no mutants in this code
<anonymous:19>19 to 191100%OkCRAP 1No data: no mutants in this code
options22 to 221100%OkCRAP 1No data: no mutants in this code
<anonymous:22>22 to 222100%OkCRAP 2No data: no mutants in this code
<anonymous:37>37 to 371100%OkCRAP 1No data: no mutants in this code
entryFields97 to 1083100%OkCRAP 3No data: no mutants in this code
<anonymous:100>100 to 1012100%OkCRAP 2No data: no mutants in this code
<anonymous:103>103 to 1031100%OkCRAP 1No data: no mutants in this code
<anonymous:105>105 to 1051100%OkCRAP 1No data: no mutants in this code
<anonymous:106>106 to 1061100%OkCRAP 1No data: no mutants in this code
fieldLabel116 to 1194100%OkCRAP 4No data: no mutants in this code
<anonymous:118>118 to 1181100%OkCRAP 1No data: no mutants in this code
itemFields129 to 1391100%OkCRAP 1No data: no mutants in this code
<anonymous:133>133 to 1373100%OkCRAP 3No data: no mutants in this code
gapReason142 to 1453100%OkCRAP 3No data: no mutants in this code

Imports

It imports app/kb/procedures/taxonomy.mjs, app/kb/procedures/validate.mjs. Imported by app/kb/form.server.ts, app/kb/gap-labels.mjs.

Source

// What the Knowledge Base form shows for each front-matter field (docs/KNOWLEDGE-BASE.md): its label, the input it is
// edited with and, for a choice, the closed list it is chosen from. The lists are the ones the validator holds a file to
// (taxonomy.mjs, validate.mjs), so the form cannot offer a value the save would refuse. A field not named here is still
// edited, as YAML, so nothing in a file is out of the form's reach.

import { BIOSAFETY_LEVELS, HOWTO_TYPES } from "./procedures/validate.mjs";
import { COURSES, METHODS, NON_STRAIN_ORGANISMS, PROTOCOL_STATUSES } from "./procedures/taxonomy.mjs";

/**
 * @typedef {"text" | "textarea" | "lines" | "date" | "number" | "checkbox" | "select" | "multi" | "choice" | "materials" | "equipment" | "primers" | "strains" | "readonly" | "yaml"} FieldKind
 * @typedef {{ key: string, label: string, kind: FieldKind, help?: string, options?: Array<{ value: string, label: string }> }} FieldSpec
 */

/** A key the form has no label for, in words: "published_doi" is "Published DOI". @param {string} key */
export const keyWords = (key) =>
  key
    .replaceAll("_", " ")
    .replace(/\b(doi|rrid|url|atcc|id|pmid)\b/gi, (m) => m.toUpperCase())
    .replace(/^./, (c) => c.toUpperCase());

/** @param {Record<string, string | { label: string }>} map */
const options = (map) => Object.entries(map).map(([value, label]) => ({ value, label: typeof label === "string" ? label : label.label }));

/** Every profile's fields first, in the order a person fills them in. @type {FieldSpec[]} */
const COMMON = [
  { key: "title", label: "Title", kind: "text" },
  { key: "seo_title", label: "Search title", kind: "text", help: "The title search engines show; at most 60 characters." },
  { key: "description", label: "Description", kind: "textarea", help: "One sentence, at most 155 characters." },
  { key: "draft", label: "Draft (only you can see it)", kind: "checkbox" },
  { key: "version", label: "Version", kind: "text", help: "Letters, digits, dots and hyphens, such as 2 or 1.1. A version, once published, is frozen." },
  { key: "updated", label: "Updated", kind: "date" },
];

/** @type {FieldSpec[]} */
const PROTOCOL = [
  { key: "status", label: "Status", kind: "select", options: options(PROTOCOL_STATUSES) },
  { key: "biosafety_level", label: "Biosafety level", kind: "choice", options: BIOSAFETY_LEVELS.map((v) => ({ value: v, label: v })) },
  { key: "method", label: "Method", kind: "multi", options: options(METHODS) },
  { key: "course", label: "Courses", kind: "multi", options: options(COURSES) },
  { key: "organism", label: "Organisms", kind: "multi" },
  { key: "target", label: "Targets", kind: "lines", help: "One per line: a gene, region or sample." },
  { key: "first_used", label: "First used", kind: "text" },
  { key: "last_run", label: "Last run", kind: "date" },
  { key: "start_here", label: "Start-here position", kind: "number", help: "Its place in the library's Start here list; leave empty for none." },
  { key: "host_strain", label: "Host strains", kind: "strains" },
  { key: "primers", label: "Primers", kind: "primers" },
  { key: "materials", label: "Reagents", kind: "materials" },
  { key: "equipment", label: "Equipment", kind: "equipment" },
  { key: "biosafety", label: "Biosafety agent", kind: "yaml", help: "The agent only: { organism, strain, atcc }, or not applicable." },
  { key: "expected_results", label: "Expected results", kind: "textarea" },
  { key: "limitations", label: "Limitations", kind: "textarea" },
];

/** @type {FieldSpec[]} */
const RECIPE = [
  { key: "servings", label: "Servings", kind: "number" },
  { key: "prep_time", label: "Prep time", kind: "text" },
  { key: "cook_time", label: "Cook time", kind: "text" },
  { key: "cuisine", label: "Cuisine", kind: "text" },
  { key: "category", label: "Category", kind: "text" },
  { key: "diet", label: "Diet", kind: "lines" },
  { key: "materials", label: "Ingredients", kind: "materials" },
  { key: "equipment", label: "Equipment", kind: "equipment" },
];

/** @type {FieldSpec[]} */
const COMPUTATIONAL = [
  { key: "type", label: "Type", kind: "select", options: options(HOWTO_TYPES), help: "A tutorial teaches, step by step; a how-to gets one task done." },
  { key: "questions", label: "Questions", kind: "lines", help: "One per line: what the reader will be able to answer." },
  { key: "objectives", label: "Objectives", kind: "lines", help: "One per line: what the reader will be able to do. A tutorial needs them." },
  { key: "key_points", label: "Key points", kind: "lines", help: "One per line, shown at the end." },
  { key: "method", label: "Method", kind: "multi", options: options(METHODS) },
  { key: "course", label: "Courses", kind: "multi", options: options(COURSES) },
  { key: "environment", label: "Environment", kind: "textarea" },
  { key: "prerequisites", label: "Prerequisites", kind: "lines" },
  { key: "materials", label: "Software and data", kind: "materials" },
  { key: "expected_results", label: "Expected results", kind: "textarea" },
  { key: "limitations", label: "Limitations", kind: "textarea" },
];

/** Shown, never edited: a procedure's profile and address are its file's name and place (the validator ties them). */
const FIXED = [
  { key: "profile", label: "Profile", kind: /** @type {const} */ ("readonly") },
  { key: "path", label: "Address", kind: /** @type {const} */ ("readonly") },
  { key: "forked_from", label: "Copied from", kind: /** @type {const} */ ("readonly") },
];

/**
 * The fields of an entry of `profile`, then every other field the file has, as YAML. The organism list is the registry's
 * strains and the non-strain organisms, the list the validator holds `organism` to.
 *
 * @param {string} profile
 * @param {Record<string, unknown>} data
 * @param {Array<{ value: string, label: string }>} strains
 * @returns {FieldSpec[]}
 */
export function entryFields(profile, data, strains) {
  const own = profile === "recipe" ? RECIPE : profile === "computational" ? COMPUTATIONAL : PROTOCOL;
  /** @type {FieldSpec[]} */
  const specs = [...FIXED, ...COMMON, ...own].map((spec) =>
    spec.key === "organism" ? { ...spec, options: [...strains, ...options(NON_STRAIN_ORGANISMS)] } : spec,
  );
  const named = new Set(specs.map((s) => s.key));
  const rest = Object.keys(data)
    .filter((key) => !named.has(key))
    .map((key) => ({ key, label: keyWords(key), kind: /** @type {const} */ ("yaml") }));
  return [...specs, ...rest];
}

/**
 * A field's label as the form shows it, for any key a file of `profile` can have.
 *
 * @param {string} profile
 * @param {string} key
 */
export function fieldLabel(profile, key) {
  const own = profile === "recipe" ? RECIPE : profile === "computational" ? COMPUTATIONAL : PROTOCOL;
  return [...FIXED, ...COMMON, ...own].find((s) => s.key === key)?.label ?? keyWords(key);
}

/**
 * A registry item's fields: its name, whether it is a draft, then every field its kind declares. A field that holds true
 * or false is a checkbox; every other is text, since a kind's fields are words (docs/REGISTRY.md).
 *
 * @param {string[]} declared
 * @param {Record<string, unknown>} data
 * @returns {FieldSpec[]}
 */
export function itemFields(declared, data) {
  return [
    { key: "name", label: "Name", kind: "text" },
    { key: "draft", label: "Draft (only you can see it)", kind: "checkbox" },
    ...declared.map((key) => ({
      key,
      label: keyWords(key),
      kind: /** @type {FieldKind} */ (typeof data[key] === "boolean" || key.startsWith("prepared_") ? "checkbox" : "text"),
    })),
  ];
}

/** A recorded gap's reason, or null for a value. @param {unknown} value */
export function gapReason(value) {
  const match = typeof value === "string" ? /^MISSING:\s*([\s\S]+)$/.exec(value) : null;
  return match ? /** @type {string} */ (match[1]) : null;
}