runSearch = async ({ q, _page, table }, req, res) => {
  const role = (req.user || {}).role_id || 100;
  // globalSearch contains list of pairs: table, view
  const cfg = getState().getConfig("globalSearch");
  const page_size = getState().getConfig("search_page_size");

  if (!cfg) {
    req.flash("warning", req.__("Search not configured"));
    res.redirect("/");
    return;
  }
  const search_table_description = getState().getConfig(
    "search_table_description",
    false
  );
  const search_results_decoration = getState().getConfig(
    "search_results_decoration",
    "Cards"
  );
  const current_page = parseInt(_page) || 1;
  const offset = (current_page - 1) * page_size;
  let tablesWithResults = [];
  let tablesConfigured = 0;
  for (const [tableName, viewName] of Object.entries(cfg)) {
    if (
      !viewName ||
      viewName === "" ||
      viewName === "search_table_description" ||
      tableName === "search_table_description" ||
      viewName === "search_results_decoration" ||
      tableName === "search_results_decoration"
    )
      continue;
    tablesConfigured += 1;
    if (table && tableName !== table) continue;
    let sectionHeader = tableName;
    if (search_table_description) {
      sectionHeader =
        Table.findOne({ name: tableName })?.description || tableName;
    }
    const view = await View.findOne({ name: viewName });
    if (!view)
      throw new InvalidConfiguration(
        `View ${viewName} selected as search results for ${tableName}: view not found`
      );
    // search table using view
    const vresps = await view.runMany(
      { _fts: q },
      { res, req, limit: page_size, offset }
    );
    let paginate = "";
    if (vresps.length === page_size || current_page > 1) {
      paginate = pagination({
        current_page,
        pages: current_page + (vresps.length === page_size ? 1 : 0),
        trailing_ellipsis: vresps.length === page_size,
        get_page_link: (n) =>
          `gopage(${n}, ${page_size}, undefined, {table:'${tableName}'}, this)`,
      });
    }

    if (vresps.length > 0) {
      tablesWithResults.push({
        tableName,
        label: sectionHeader,
        contents: vresps.map((vr) => vr.html).join("<hr>") + paginate,
      });
    }
  }

  // Prepare search form
  const form = searchForm();
  form.validate({ q });

  const mkResultDisplay = () => {
    switch (search_results_decoration) {
      case "Tabs":
        const tabContents = {};
        tablesWithResults.forEach((tblRes) => {
          tabContents[tblRes.label] = tblRes.contents;
        });
        return [{ type: "card", tabContents }];

      default:
        return tablesWithResults.map((tblRes) => ({
          type: "card",
          title: span({ id: tblRes.tableName }, tblRes.label),
          contents: tblRes.contents,
        }));
    }
  };

  // Prepare search result visualization
  const searchResult =
    tablesWithResults.length === 0
      ? [{ type: "card", contents: req.__("Not found") }]
      : mkResultDisplay();
  res.sendWrap(req.__("Search all tables"), {
    above: [
      {
        type: "card",
        contents: div(
          renderForm(form, false),
          syntax_help_link(req),
          typeof table !== "undefined" &&
            tablesConfigured > 1 &&
            div(
              req.__("Showing matches in table %s.", table),
              "&nbsp;",
              a(
                {
                  href: `javascript:set_state_fields({table:{unset:true},_page:{unset:true}})`,
                },
                req.__("Search all tables")
              )
            ),
          tablesWithResults.length > 1 &&
            div(
              req.__("Show only matches in table:"),
              "&nbsp;",
              tablesWithResults
                .map(({ tableName, label }) =>
                  a(
                    {
                      href: `javascript:set_state_field('table', '${tableName}')`,
                    },
                    label
                  )
                )
                .join(" | ")
            )
        ),
      },
      ...searchResult,
    ],
  });
}
Run search
@param {object} opts
@param {*} opts.q
@param {*} opts._page
@param {*} opts.table
@param {object} opts
@param {object} req
@param {object} res
@returns {Promise<void>}