run = async (
  table_id,
  viewname,
  {
    columns,
    layout,
    view_to_create,
    create_view_display,
    create_view_label,
    default_state,
    create_view_location,
    create_link_style,
    create_link_size,
    create_view_showif,
  },
  stateWithId,
  extraOpts,
  { listQuery }
) => {
  const table = Table.findOne(
    typeof table_id === "string" ? { name: table_id } : { id: table_id }
  );
  const pk_name = table.pk_name;
  const fields = table.getFields();
  const appState = getState();
  const locale = extraOpts.req.getLocale();
  const __ = (s) =>
    isWeb(extraOpts.req) ? appState.i18n.__({ phrase: s, locale }) || s : s;
  //move fieldview cfg into configuration subfield in each column
  for (const col of columns) {
    if (col.type === "Field") {
      const field = fields.find((f) => f.name === col.field_name);
      if (!field) continue;
      const fieldviews =
        field.type === "Key"
          ? appState.keyFieldviews
          : field.type.fieldviews || {};
      if (!fieldviews) continue;
      const fv = fieldviews[col.fieldview];
      if (fv && fv.configFields) {
        const cfgForm = await applyAsync(fv.configFields, field);
        col.configuration = {};
        for (const formField of cfgForm || []) {
          col.configuration[formField.name] = col[formField.name];
        }
      }
    }
  }
  const role =
    extraOpts && extraOpts.req && extraOpts.req.user
      ? extraOpts.req.user.role_id
      : 100;
  await set_join_fieldviews({ table, columns, fields });

  readState(stateWithId, fields, extraOpts.req);
  const id = stateWithId[pk_name];
  let state = { ...stateWithId };
  if (extraOpts?.removeIdFromstate) delete state[pk_name];

  const statehash = hashState(state, viewname);

  const { rows, rowCount } = await listQuery(state, statehash);

  const viewResults = {};
  var views = {};

  const getView = async (name, relation) => {
    if (views[name]) return views[name];
    const view_select = parse_view_select(name, relation);
    const view = View.findOne({ name: view_select.viewname });
    if (!view) return false;
    if (view.table_id === table.id) view.table = table;
    else view.table = Table.findOne({ id: view.table_id });
    view.view_select = view_select;
    views[name] = view;
    return view;
  };
  await eachView(layout, async (segment) => {
    const view = await getView(segment.view, segment.relation);
    if (!view)
      throw new InvalidConfiguration(
        `View ${viewname} incorrectly configured: cannot find view ${segment.view}`
      );
    view.check_viewtemplate();
    let stateMany, getRowState;
    const get_extra_state = (row) =>
      segment.extra_state_fml
        ? eval_expression(
            segment.extra_state_fml,
            {
              ...dollarizeObject(state),
              session_id: getSessionId(extraOpts.req),
              ...row,
            },
            extraOpts.req.user,
            `Extra state formula for view ${view.name}`
          )
        : {};
    if (view.view_select.type === "RelationPath") {
      const relation = new Relation(
        segment.relation,
        view.table_id ? Table.findOne({ id: view.table_id }).name : undefined,
        displayType(await view.get_state_fields())
      );
      switch (relation.type) {
        case RelationType.OWN:
          stateMany = {
            or: rows.map((row) => ({
              [pk_name]: row[pk_name],
              ...get_extra_state(row),
            })),
          };
          getRowState = (row) => ({
            [pk_name]: row[pk_name],
            ...get_extra_state(row),
          });
          break;
        case RelationType.PARENT_SHOW:
          const refTable = Table.findOne({ id: view.table_id });
          stateMany = {
            or: rows.map((row) => ({
              [refTable.pk_name]: row[relation.targetTblName],
              ...get_extra_state(row),
            })),
          };
          getRowState = (row) => ({
            [refTable.pk_name]: row[relation.targetTblName],
            ...get_extra_state(row),
          });
          break;
        case RelationType.INDEPENDENT:
        case RelationType.NONE:
          stateMany = segment.extra_state_fml
            ? {
                or: rows.map((row) => get_extra_state(row)),
              }
            : {};
          getRowState = (row) => get_extra_state(row);

          break;
        default:
          throw new Error(
            `View in List: invalid relation type ${relation.type}`
          );
      }
    }

    //todo:
    // other rel types
    if (this.viewtemplateObj?.runMany) {
      const runs = await view.runMany(stateMany, extraOpts);
      viewResults[segment.view + segment.relation] = (row) =>
        runs.find((rh) => rh.row[pk_name] == row[pk_name])?.html;
    } else if (this.viewtemplateObj?.renderRows) {
      const rendered = await view.viewtemplateObj.renderRows(
        view.table,
        view.name,
        view.configuration,
        extraOpts,
        rows,
        state
      );

      viewResults[segment.view + segment.relation] = (row) =>
        rendered
          .map((html, ix) => ({
            html,
            row: rows[ix],
          }))
          .find((rh) => rh.row[pk_name] == row[pk_name])?.html;
    } else {
      const results = [];

      for (const row of rows) {
        const rendered = await view.run(getRowState(row), extraOpts);
        results.push({
          html: rendered,
          row,
        });
      }
      viewResults[segment.view + segment.relation] = (row) =>
        results.find((rh) => rh.row[pk_name] == row[pk_name])?.html;
    }
  });
  const is_row_click =
    (default_state?._row_click_url_formula ||
      default_state?._row_click_action) &&
    default_state?._row_click_type !== "Nothing";
  const tfields = layout?.list_columns
    ? get_viewable_fields_from_layout(
        viewname,
        statehash,
        table,
        fields,
        columns,
        false,
        extraOpts.req,
        __,
        state,
        viewname,
        layout.besides,
        viewResults,
        is_row_click
      )
    : get_viewable_fields(
        viewname,
        statehash,
        table,
        fields,
        columns,
        false,
        extraOpts.req,
        __,
        state,
        viewname,
        is_row_click
      );
  const rows_per_page = (default_state && default_state._rows_per_page) || 20;
  const current_page = parseInt(state[`_${statehash}_page`]) || 1;
  var page_opts =
    extraOpts && extraOpts.onRowSelect
      ? { onRowSelect: extraOpts.onRowSelect, selectedId: id }
      : { selectedId: id };
  if (
    default_state?._row_click_url_formula &&
    default_state?._row_click_type !== "Nothing" &&
    default_state?._row_click_type !== "Action"
  ) {
    let fUrl = get_expression_function(
      default_state._row_click_url_formula,
      fields
    );
    if (default_state?._row_click_type === "Link new tab")
      page_opts.onRowSelect = (row) =>
        `window.open('${fUrl(row, extraOpts.req.user)}', '_blank').focus();`;
    else if (default_state?._row_click_type === "Popup")
      page_opts.onRowSelect = (row) =>
        `ajax_modal('${fUrl(row, extraOpts.req.user)}')`;
    else
      page_opts.onRowSelect = (row) =>
        `location.href='${fUrl(row, extraOpts.req.user)}'`;
  } else if (default_state?._row_click_type === "Action") {
    page_opts.onRowSelect = (row) => {
      const actionUrl = action_url(
        viewname,
        table,
        default_state?._row_click_action,
        row,
        default_state?._row_click_action,
        "action_name"
      );
      if (actionUrl.javascript) return actionUrl.javascript;
    };
  }
  page_opts.class = "";

  if (
    (!default_state?._hide_pagination &&
      rows &&
      rows.length === rows_per_page) ||
    current_page > 1
  ) {
    const nrows = rowCount;
    if (nrows > rows_per_page || current_page > 1) {
      page_opts.pagination = {
        current_page,
        pages: Math.ceil(nrows / rows_per_page),
        get_page_link: (n) =>
          `gopage(${n}, ${rows_per_page}, '${statehash}', {}, this)`,
      };
    }
  }

  if (default_state?._omit_header) {
    page_opts.noHeader = true;
  }
  if (default_state?._hover_rows) {
    page_opts.class += "table-hover ";
  }
  if (default_state?._striped_rows) {
    page_opts.class += "table-striped ";
  }
  if (default_state?._card_rows) {
    page_opts.class += "table-card-rows ";
  }
  if (default_state?._borderless) {
    page_opts.class += "table-borderless ";
  }
  page_opts.transpose = (default_state || {}).transpose;
  page_opts.transpose_width = (default_state || {}).transpose_width;
  page_opts.transpose_width_units = (default_state || {}).transpose_width_units;
  const [vpos, hpos] = (create_view_location || "Bottom left").split(" ");
  const istop = vpos === "Top";
  const isright = hpos === "right";

  var create_link = "";
  const user_id =
    extraOpts && extraOpts.req.user ? extraOpts.req.user.id : null;
  const create_link_showif_pass = create_view_showif
    ? eval_expression(
        create_view_showif,
        state,
        extraOpts.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)
  ) {
    const create_view = View.findOne({ name: view_to_create });
    const ownership_field =
      table.ownership_field_id &&
      table.fields.find((f) => f.id === table.ownership_field_id);
    const about_user = fields.some(
      (f) =>
        f.reftable_name === "users" &&
        state[f.name] &&
        state[f.name] === user_id
    );

    if (
      create_link_showif_pass ||
      role <= table.min_role_write ||
      (ownership_field?.reftable_name === "users" && about_user) ||
      create_view?.configuration?.fixed?.[`preset_${ownership_field?.name}`] ===
        "LoggedIn"
    ) {
      if (create_view_display === "Embedded") {
        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, extraOpts);
      } else {
        const target = `/view/${encodeURIComponent(
          view_to_create
        )}${stateToQueryString(state)}`;
        const hrefVal =
          isWeb(extraOpts.req) || create_view_display === "Popup"
            ? 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;

  let tableHtml;

  if (default_state?._group_by) {
    const groups = {};
    for (const row of rows) {
      const group = eval_expression(
        default_state?._group_by,
        row,
        extraOpts.req.user,
        "Group by expression"
      );
      if (!groups[group]) groups[group] = [];
      groups[group].push(row);
    }
    page_opts.grouped = true;
    tableHtml = mkTable(
      default_state?.hide_null_columns
        ? remove_null_cols(tfields, rows)
        : tfields,
      groups,
      page_opts
    );
  } else {
    tableHtml = mkTable(
      default_state?.hide_null_columns
        ? remove_null_cols(tfields, rows)
        : tfields,
      rows,
      page_opts
    );
  }

  return istop ? create_link_div + tableHtml : tableHtml + create_link_div;
}
@param {string|number} table_id
@param {string} viewname
@param {object} opts
@param {object[]} opts.columns
@param {string} [opts.view_to_create]
@param {string} opts.create_view_display
@param {string} [opts.create_view_label]
@param {object} [opts.default_state]
@param {string} [opts.create_view_location]
@param {object} [stateWithId]
@param {object} extraOpts
@returns {Promise<*>}