renderRows = async (
  table,
  viewname,
  { columns, layout },
  extra,
  rows,
  state
) => {
  //console.log(columns);
  //console.log(layout);
  if (!columns || !layout) return "View not yet built";

  const fields = table.getFields();

  const role = extra.req.user ? extra.req.user.role_id : 100;
  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 set_load_actions_join_fieldviews({
    table,
    layout,
    fields,
    req: extra.req,
    res: extra.res,
  });

  const owner_field = await table.owner_fieldname();
  const subviewExtra = { ...extra };
  if (extra.req?.generate_email) {
    // no mjml markup for for nested subviews, only for the top view
    subviewExtra.req = { ...extra.req, isSubView: true };
  }
  return await asyncMap(rows, async (row) => {
    await eachView(layout, async (segment) => {
      // do all the parsing with data here? make a factory
      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();
      if (view.viewtemplateObj.renderRows && view.view_select.type === "Own") {
        segment.contents = (
          await view.viewtemplateObj.renderRows(
            view.table,
            view.name,
            view.configuration,
            subviewExtra,
            [row],
            state
          )
        )[0];
      } else {
        let state1 = {};
        const pk_name = table.pk_name;
        const get_row_val = (k) => {
          //handle expanded joinfields
          if (row[k] === null) return null;
          if (row[k]?.id === null) return null;
          return row[k]?.id || row[k];
        };
        const get_user_id = () => (extra.req.user ? extra.req.user.id : 0);
        if (view.view_select.type === "RelationPath" && view.table_id) {
          const targetTbl = Table.findOne({ id: view.table_id });
          const relation = new Relation(
            segment.relation,
            targetTbl.name,
            displayType(await view.get_state_fields())
          );
          state1 = pathToState(
            relation,
            relation.isFixedRelation() ? get_user_id : get_row_val
          );
        } else {
          switch (view.view_select.type) {
            case "Own":
              state1 = { [pk_name]: get_row_val(pk_name) };
              break;
            case "Independent":
              state1 = {};
              break;
            case "ChildList":
            case "OneToOneShow":
              state1 = {
                [view.view_select.through
                  ? `${view.view_select.throughTable}.${view.view_select.through}.${view.view_select.table_name}.${view.view_select.field_name}`
                  : view.view_select.field_name]: get_row_val(pk_name),
              };
              break;
            case "ParentShow":
              //todo set by pk name of parent tablr
              state1 = {
                id: get_row_val(view.view_select.field_name),
              };
              break;
          }
        }
        const extra_state = segment.extra_state_fml
          ? eval_expression(
              segment.extra_state_fml,
              {
                ...dollarizeObject(state),
                session_id: getSessionId(extra.req),
                ...row,
              },
              extra.req.user,
              `Extra state formula for view ${view.name}`
            )
          : {};
        const { id, ...outerState } = state;
        //console.log(segment);
        if (segment.state === "local") {
          const state2 = { ...state1, ...extra_state };
          const qs = stateToQueryString(state2, true);
          if (
            view.name === viewname &&
            JSON.stringify(state) === JSON.stringify(state2)
          )
            throw new InvalidConfiguration(
              `View ${view.name} embeds itself with same state; inifinite loop detected`
            );
          segment.contents = div(
            {
              class: "d-inline",
              "data-sc-embed-viewname": view.name,
              "data-sc-local-state": `/view/${view.name}${qs}`,
            },
            await view.run(state2, subviewExtra, view.isRemoteTable())
          );
        } else {
          const state2 = { ...outerState, ...state1, ...extra_state };
          const qs = stateToQueryString(state2, true);

          if (
            view.name === viewname &&
            JSON.stringify(state) === JSON.stringify(state2)
          )
            throw new InvalidConfiguration(
              `View ${view.name} embeds itself with same state; inifinite loop detected`
            );
          segment.contents = div(
            {
              class: "d-inline",
              "data-sc-embed-viewname": view.name,
              "data-sc-view-source": `/view/${view.name}${qs}`,
            },
            await view.run(state2, subviewExtra, view.isRemoteTable())
          );
        }
      }
    });
    const user_id = extra.req.user ? extra.req.user.id : null;

    const is_owner =
      table.ownership_formula && user_id && role > table.min_role_read
        ? await table.is_owner(extra.req.user, row)
        : owner_field && user_id && row[owner_field] === user_id;

    return render(
      row,
      fields,
      layout,
      viewname,
      table,
      role,
      extra.req,
      is_owner,
      state,
      extra
    );
  });
}
@param {object} table
@param {string} viewname
@param {object} opts
@param {object[]} opts.columns
@param {object} opts.layout
@param {object} extra
@param {object[]} rows
@returns {Promise<string>}