run = async (
  table_id,
  viewname,
  {
    show_view,
    order_field,
    descending,
    view_to_create,
    create_view_display,
    in_card, //legacy
    view_decoration,
    initial_open_accordions = "None",
    title_formula,
    masonry_columns,
    rows_per_page = 20,
    hide_pagination,
    create_view_label,
    create_view_location,
    create_link_style,
    create_link_size,
    create_view_showif,
    always_create_view,
    include_fml,
    exclusion_relation,
    exclusion_where,
    empty_view,
    groupby,
    lazy_accordions,
    local_state,
    ...cols
  },
  state,
  extraArgs,
  { countRowsQuery, runManyQuery }
) => {
  const table = Table.findOne({ id: table_id });
  const fields = table.getFields();
  readState(state, fields);
  const stateHash = hashState(state, show_view);
  const appState = getState();
  const locale = extraArgs.req.getLocale();
  const __ = isNode()
    ? (s) => appState.i18n.__({ phrase: s, locale }) || s
    : (s) => s;
  if (!show_view)
    throw new InvalidConfiguration(
      `View ${viewname} incorrectly configured: Single item view not specified`
    );
  const sview = View.findOne({ name: show_view });
  if (!sview)
    throw new InvalidConfiguration(
      `View ${viewname} incorrectly configured: cannot find view ${show_view}`
    );
  const q = stateFieldsToQuery({ state, fields });
  let qextra = {};
  if (!q.orderBy) {
    qextra.orderBy = order_field;
    if (descending) qextra.orderDesc = true;
  }
  qextra.limit = q.limit || rows_per_page;
  const current_page = parseInt(state[`_${stateHash}_page`]) || 1;
  const user_id =
    extraArgs && extraArgs.req.user ? extraArgs.req.user.id : null;
  if (include_fml)
    qextra.where = jsexprToWhere(
      include_fml,
      {
        ...state,
        user_id,
        user: extraArgs?.req?.user,
      },
      table.fields
    );
  if (exclusion_relation) {
    const [reltable, relfld] = exclusion_relation.split(".");
    const relTable = Table.findOne({ name: reltable });
    const relWhere = exclusion_where
      ? jsexprToWhere(
          exclusion_where,
          {
            user_id,
            user: extraArgs?.req?.user,
          },
          relTable.fields
        )
      : {};
    const relRows = await relTable.getRows(relWhere);
    if (!qextra.where) qextra.where = {};
    // TODO sqlite not in
    qextra.where.id = { not: { in: relRows.map((r) => r[relfld]) } };
  }
  qextra.joinFields = {};
  add_free_variables_to_joinfields(
    freeVariables(title_formula),
    qextra.joinFields,
    fields
  );
  add_free_variables_to_joinfields(
    freeVariables(groupby || ""),
    qextra.joinFields,
    fields
  );
  const { req, res, ...selectOpts } = extraArgs;
  const sresp = await runManyQuery(state, qextra, selectOpts);
  let paginate = "";

  if (sresp.length === 0 && empty_view) {
    const emptyView = View.findOne({ name: empty_view });
    if (!emptyView)
      throw new InvalidConfiguration(
        `View ${viewname} incorrectly configured: cannot find empty view ${empty_view}`
      );
    return div(
      {
        class: "d-inline",
        "data-sc-embed-viewname": emptyView.name,
      },
      await emptyView.run(state, extraArgs)
    );
  }

  if (!hide_pagination && (sresp.length === qextra.limit || current_page > 1)) {
    const nrows = await countRowsQuery(state);
    if (nrows > qextra.limit || current_page > 1) {
      paginate = pagination({
        current_page,
        pages: Math.ceil(nrows / qextra.limit),
        get_page_link: (n) =>
          `gopage(${n}, ${qextra.limit}, '${stateHash}', {}, this)`,
      });
    }
  }
  const [vpos, hpos] = (create_view_location || "Bottom left").split(" ");
  const istop = vpos === "Top";
  const isright = hpos === "right";
  const role =
    extraArgs && extraArgs.req && extraArgs.req.user
      ? extraArgs.req.user.role_id
      : 100;
  var create_link = "";

  const about_user = fields.some(
    (f) =>
      f.reftable_name === "users" && state[f.name] && state[f.name] === user_id
  );
  const create_link_showif_pass = create_view_showif
    ? eval_expression(
        create_view_showif,
        state,
        extraArgs.req.user,
        "Create view show if formula"
      )
    : undefined;
  if (
    create_link_showif_pass !== false &&
    view_to_create &&
    (create_link_showif_pass ||
      role <= table.min_role_write ||
      (table.ownership_field_id && (about_user || always_create_view)))
  ) {
    if (create_view_display === "Embedded") {
      const create_view = await View.findOne({ name: view_to_create });
      if (!create_view)
        throw new InvalidConfiguration(
          `View ${viewname} incorrectly configured: cannot find embedded view to create ${view_to_create}`
        );
      create_link = await create_view.run(state, extraArgs);
    } else {
      const target = `/view/${encodeURIComponent(
        view_to_create
      )}${stateToQueryString(state)}`;
      const hrefVal = isWeb(extraArgs.req)
        ? target
        : `javascript:execLink('${target}');`;
      create_link = link_view(
        hrefVal,
        __(create_view_label) || `Add ${pluralize(table.name, 1)}`,
        create_view_display === "Popup" ? { reload_view: viewname } : false,
        create_link_style,
        create_link_size
      );
    }
  }
  const create_link_div = isright
    ? div({ class: "float-end" }, create_link)
    : create_link;

  const setCols = (sz) => `col-${sz}-${Math.round(12 / cols[`cols_${sz}`])}`;

  const wrapScEmbed = (r, neverLazy) =>
    div(
      {
        class: "d-inline",
        "data-sc-embed-viewname": show_view,
        "data-sc-view-source": local_state
          ? false
          : `/view/${show_view}?${table.pk_name}=${r.row[table.pk_name]}`,
        "data-sc-local-state": local_state
          ? `/view/${show_view}?${table.pk_name}=${r.row[table.pk_name]}`
          : false,
      },
      view_decoration === "Accordion" && lazy_accordions && !neverLazy
        ? ""
        : r.html
    );

  const showRowInner = (r, ix) =>
    (!view_decoration && in_card) || view_decoration === "Card"
      ? div(
          { class: `card shadow ${masonry_columns ? "mt-2" : "mt-4 h-100"}` },
          title_formula
            ? div(
                { class: "card-header" },
                eval_expression(
                  title_formula,
                  r.row,
                  extraArgs.req.user,
                  "Card title formula"
                )
              )
            : undefined,
          div({ class: "card-body" }, wrapScEmbed(r))
        )
      : view_decoration === "Tabs"
        ? div(
            {
              class: ["tab-pane fade", ix == 0 && "show active"],
              id: `feedtab${viewname.replaceAll(" ", "_")}_${ix}`,
              role: "tabpanel",
              "aria-labelledby": `feedtab${viewname.replaceAll(
                " ",
                "_"
              )}_${ix}-tab`,
            },
            wrapScEmbed(r)
          )
        : view_decoration === "Accordion"
          ? div(
              { class: "accordion-item" },
              h2(
                { class: "accordion-header", id: `a${stateHash}head${ix}` },
                button(
                  {
                    class: [
                      "accordion-button",
                      (initial_open_accordions === "None" ||
                        (initial_open_accordions === "First" && ix > 0)) &&
                        "collapsed",
                    ],
                    type: "button",
                    "data-bs-toggle": "collapse",
                    "data-bs-target": `#a${stateHash}tab${ix}`,
                    "aria-expanded": "false",
                    "aria-controls": `a${stateHash}tab${ix}`,
                  },
                  (title_formula
                    ? eval_expression(
                        title_formula,
                        r.row,
                        extraArgs.req.user,
                        `Accordion title formula`
                      )
                    : "") || "Missing title"
                )
              ),
              div(
                {
                  class: [
                    "accordion-collapse",
                    "collapse",
                    !(
                      initial_open_accordions === "None" ||
                      (initial_open_accordions === "First" && ix > 0)
                    ) && "show",
                  ],
                  id: `a${stateHash}tab${ix}`,
                  "aria-labelledby": `a${stateHash}head${ix}`,
                  "data-bs-parent": `#top${stateHash}`,
                },
                div(
                  { class: ["accordion-body"] },
                  wrapScEmbed(
                    r,
                    !(
                      initial_open_accordions === "None" ||
                      (initial_open_accordions === "First" && ix > 0)
                    )
                  )
                )
              )
            )
          : wrapScEmbed(r);

  const showRow = (r) =>
    div(
      {
        class: [setCols("sm"), setCols("md"), setCols("lg"), setCols("xl")],
      },
      showRowInner(r)
    );
  const is_in_card =
    (!view_decoration && in_card) || view_decoration === "Card";

  const correct_order = ([main, pagin, create]) =>
    istop ? [create, main, pagin] : [main, pagin, create];
  if (groupby) {
    const groups = {};
    for (const r of sresp) {
      const group = eval_expression(
        groupby,
        r.row,
        extraArgs.req.user,
        "Group by expression"
      );
      if (!groups[group]) groups[group] = [];
      groups[group].push(r);
    }
    return div(
      correct_order([
        Object.entries(groups).map(
          ([group, sr]) =>
            h3({ class: "feed-group-header" }, group) +
            (is_in_card && masonry_columns
              ? div({ class: "card-columns" }, sr.map(showRowInner))
              : view_decoration === "Accordion"
                ? div(
                    {
                      class: [
                        "accordion",
                        lazy_accordions && "lazy-accoordion",
                      ],
                      id: `top${stateHash}`,
                    },
                    sr.map(showRowInner)
                  )
                : div(
                    {
                      class: [
                        "row",
                        !masonry_columns &&
                          is_in_card &&
                          `row-cols-md-${cols[`cols_md`]} g-4 mb-3`,
                      ],
                    },
                    sr.map(showRow)
                  ))
        ),
        paginate,
        create_link_div,
      ])
    );
  }
  const tabHeader = ({ row }, ix) => {
    const title =
      (title_formula
        ? eval_expression(
            title_formula,
            row,
            extraArgs.req.user,
            `Tab title formula`
          )
        : "") || "Missing title";
    return li(
      { class: "nav-item" },
      a(
        {
          class: ["nav-link", ix == 0 && "active"],
          "data-bs-toggle": "tab",
          href: `#feedtab${viewname.replaceAll(" ", "_")}_${ix}`,
          id: `feedtab${viewname.replaceAll(" ", "_")}_${ix}-tab`,
          role: "tab",
          "aria-controls": "home",
          "aria-selected": "true",
        },
        text(title)
      )
    );
  };
  return div(
    correct_order([
      is_in_card && masonry_columns
        ? div({ class: "card-columns" }, sresp.map(showRowInner))
        : view_decoration === "Tabs"
          ? div(
              ul(
                { class: "nav nav-tabs", role: "tablist" },
                sresp.map(tabHeader)
              ),
              div({ class: "tab-content" }, sresp.map(showRowInner))
            )
          : view_decoration === "Accordion"
            ? div(
                {
                  class: ["accordion", lazy_accordions && "lazy-accoordion"],
                  id: `top${stateHash}`,
                },
                sresp.map(showRowInner)
              )
            : div(
                {
                  class: [
                    "row",
                    !masonry_columns &&
                      is_in_card &&
                      `row-cols-md-${cols.cols_md} row-cols-sm-${cols.cols_sm} row-cols-sm-${cols.cols_lg}  row-cols-cl-${cols.cols_xl} g-4 mb-3`,
                  ],
                },
                sresp.map(showRow)
              ),
      paginate,
      create_link_div,
    ])
  );
}
@param {number} table_id
@param {string} viewname
@param {object} opts
@param {string} opts.show_view
@param {name} opts.order_field
@param {boolean} opts.descending
@param {string} [opts.view_to_create]
@param {string} opts.create_view_display
@param {boolean} opts.in_card
@param {string} opts.masonry_columns
@param {number} [opts.rows_per_page = 20]
@param {boolean} opts.hide_pagination
@param {string} [opts.create_view_label]
@param {string} [opts.create_view_location]
@param {boolean} opts.always_create_view
@param {*} opts.cols
@param {object} state
@param {*} extraArgs
@returns {Promise<div>}