run = async (
  table_id,
  viewname,
  { columns, layout },
  state,
  extra,
  { distinctValuesQuery, optionsQuery }
) => {
  //console.log(columns);
  //console.log(layout);
  if (!columns || !layout) return "View not yet built";
  const table = Table.findOne(table_id);
  const fields = table.getFields();
  readState(state, fields);
  const formFieldNames = (columns || [])
    .map((c) => c.field_name)
    .filter((n) => n);
  const { distinct_values, role } = await distinctValuesQuery(state);
  const badges = [];
  Object.entries(state).forEach(([k, v]) => {
    if (typeof v === "undefined") return;
    if (k[0] !== "_") {
      let showv = v;
      if (distinct_values[k]) {
        const realv = distinct_values[k].find((dv) => dv.value === v);
        if (realv) showv = realv.label;
      }
      badges.push({
        text: `${text_attr(k)}:${text_attr(showv)}`,
        onclick: `unset_state_field('${text_attr(k)}', this)`,
      });
    }
  });
  const evalCtx = { ...state };
  fields.forEach((f) => {
    //so it will be in scope in formula
    if (typeof evalCtx[f.name] === "undefined") evalCtx[f.name] = undefined;
  });
  evalCtx.session_id = getSessionId(extra.req);
  await traverse(layout, {
    aggregation: async (segment) => {
      const { stat, agg_field, agg_fieldview, aggwhere } = segment;
      const where = stateFieldsToWhere({ fields, state, table, prefix: "a." });
      if (aggwhere) {
        const ctx = {
          ...state,
          user_id: extra.req.user?.id || null,
          user: extra.req.user,
        };
        let where1 = jsexprToWhere(aggwhere, ctx, fields);
        mergeIntoWhere(where, where1 || {});
      }
      const { val } = await table.aggregationQuery(
        {
          val: {
            field: agg_field,
            aggregate: stat,
          },
        },
        { where }
      );
      const fld = table.getField(agg_field);
      segment.type = "blank";
      if (stat.toLowerCase() === "array_agg" && Array.isArray(val))
        segment.contents = val.map((v) => text(v.toString())).join(", ");
      else if (agg_fieldview) {
        const outcomeType =
          stat === "Percent true" || stat === "Percent false"
            ? "Float"
            : stat === "Count" || stat === "CountUnique"
              ? "Integer"
              : fld.type?.name;
        const type = getState().types[outcomeType];
        if (type?.fieldviews[agg_fieldview]) {
          const readval = type.read(val);
          segment.contents = type.fieldviews[agg_fieldview].run(
            readval,
            extra.req,
            segment?.configuration || {}
          );
        }
      } else segment.contents = text(val);
    },
    field: async (segment) => {
      const { field_name, fieldview, configuration } = segment;
      let field = fields.find((fld) => fld.name === field_name);
      if (!field) {
        if (field_name.includes(".")) {
          const kpath = field_name.split(".");
          if (kpath.length === 3) {
            const [jtNm, jFieldNm, lblField] = kpath;
            const jtable = Table.findOne({ name: jtNm });
            if (!jtable)
              throw new InvalidConfiguration(
                `View ${viewname} incorrectly configured: cannot find join table ${jtNm}`
              );
            const jfields = jtable.fields;
            field = jfields.find((f) => f.name === lblField);
          }
        }
        if (!field) return;
      }
      field.fieldview = fieldview;
      if (field.is_fkey && !field.fieldviewObj)
        field.fieldviewObj = getState().keyFieldviews[field.fieldview];
      Object.assign(field.attributes, configuration);
      await field.fill_fkey_options(
        false,
        undefined,
        extra.req.user ? { ...state, user_id: extra.req.user } : state,
        isWeb(extra.req) ? undefined : optionsQuery,
        undefined,
        undefined,
        extra.req.user || { role_id: 100 }
      );
      segment.field = field;
    },
    view: async (segment) => {
      const view = await View.findOne({ name: segment.view });
      if (!view)
        throw new InvalidConfiguration(
          `View ${viewname} incorrectly configured: cannot find view ${segment.view}`
        );
      const extra_state = segment.extra_state_fml
        ? eval_expression(
            segment.extra_state_fml,
            evalCtx,
            extra.req.user,
            `Extra state formula for view ${view.name}`
          )
        : {};
      if (segment.state === "local") {
        const state1 = { ...extra_state };
        const qs = stateToQueryString(state1, true);

        segment.contents = div(
          {
            class: "d-inline",
            "data-sc-embed-viewname": view.name,
            "data-sc-local-state": `/view/${view.name}${qs}`,
          },
          await view.run(state1, extra)
        );
      } else {
        const state1 = { ...state, ...extra_state };
        const qs = stateToQueryString(state1, true);

        segment.contents = div(
          {
            class: "d-inline",
            "data-sc-embed-viewname": view.name,
            "data-sc-view-source": `/view/${view.name}${qs}`,
          },
          await view.run(state1, extra)
        );
      }
    },
    link: (segment) => {
      //console.log("link:", segment, state);
      if (segment.transfer_state) {
        segment.url += `?` + objectToQueryString(state || {});
      }
      if (segment.view_state_fml) {
        const extra_state = segment.view_state_fml
          ? eval_expression(
              segment.view_state_fml,
              evalCtx,
              extra.req.user,
              `Extra state formula for link`
            )
          : {};
        segment.url +=
          (segment.transfer_state ? "" : `?`) +
          objectToQueryString(extra_state || {});
      }
    },
    container(segment) {
      if (segment.showIfFormula) {
        const f = get_expression_function(segment.showIfFormula, fields);

        if (!f(state, extra.req.user)) segment.hide = true;
        else segment.hide = false;
      }
    },
    tabs(segment) {
      const to_delete = new Set();

      (segment.showif || []).forEach((sif, ix) => {
        if (sif) {
          const showit = eval_expression(
            sif,
            evalCtx,
            extra.req.user,
            "Tabs show if formula"
          );
          if (!showit) to_delete.add(ix);
        }
      });

      segment.titles = segment.titles.filter((v, ix) => !to_delete.has(ix));
      segment.contents = segment.contents.filter((v, ix) => !to_delete.has(ix));
    },
    async action(segment) {
      if (segment.action_style === "on_page_load") {
        segment.type = "blank";
        segment.style = {};
        if (segment.minRole && segment.minRole != 100) {
          const minRole = +segment.minRole;
          const userRole = extra?.req?.user?.role_id || 100;
          if (minRole < userRole) return;
        }
        if (extra?.isPreview) return;
        try {
          const actionResult = await run_action_column({
            col: { ...segment },
            referrer: extra.req?.get?.("Referrer"),
            req: extra.req,
            row: state,
            table,
          });

          if (actionResult)
            segment.contents = script(
              domReady(
                `common_done(${JSON.stringify(actionResult)}, "${viewname}")`
              )
            );
          else segment.contents = "";
        } catch (e) {
          segment.contents = "";
          Crash.create(e, extra.req);
        }
      }
    },
  });
  translateLayout(layout, extra.req.getLocale());
  const blockDispatch = {
    field(segment) {
      const { field_name, fieldview, configuration, field } = segment;

      if (!field) return "";
      //console.log({ fieldview, field });
      if (fieldview && field.type && field.type === "Key") {
        const fv = getState().keyFieldviews[fieldview];
        if (fv && (fv.isEdit || fv.isFilter)) {
          segment.options = distinct_values[field_name];
          return fv.run(
            field_name,
            state[field_name],
            {
              onChange: `set_state_field('${encodeURIComponent(
                field_name
              )}', this.value, this)`,
              ...field.attributes,
              isFilter: true,
              ...configuration,
            },
            "",
            false,
            field,
            state
          );
        }
      }
      if (
        fieldview &&
        field.type &&
        field.type.fieldviews &&
        field.type.fieldviews[fieldview]
      ) {
        const fv = field.type.fieldviews[fieldview];
        if (fv.isEdit || fv.isFilter)
          return fv.run(
            field_name,
            state[field_name],
            {
              onChange: `set_state_field('${encodeURIComponent(
                field_name
              )}', this.value, this)`,
              isFilter: true,
              ...field.attributes,
              ...configuration,
            },
            "",
            false,
            field,
            state
          );
      }
      return "";
    },
    search_bar({ has_dropdown, contents, show_badges, autofocus }, go) {
      const rendered_contents = go(contents);
      const stVar = `_fts_${table.santized_name}`;
      return search_bar(stVar, state[stVar], {
        stateField: stVar,
        has_dropdown,
        autofocus,
        contents: rendered_contents,
        badges: show_badges ? badges : null,
      });
    },
    dropdown_filter(segment) {
      const { field_name, neutral_label, full_width, label_formula } = segment;

      const dvs = distinct_values[field_name] || [];
      dvs.sort((a, b) =>
        (a.label?.toLowerCase?.() || a.label) >
        (b.label?.toLowerCase?.() || b.label)
          ? 1
          : -1
      );
      const options = dvs.map(({ label, value, jsvalue }, ix) =>
        option(
          {
            value,
            selected:
              `${state[field_name]}` === `${or_if_undef(jsvalue, value)}` ||
              (!value && !state[field_name]) ||
              (jsvalue === true && state[field_name] === "on") ||
              (jsvalue === false && state[field_name] === "off"),
            class: !value && !label ? "text-muted" : undefined,
          },
          !value && !label && ix === 0 && neutral_label
            ? neutral_label
            : label_formula
              ? eval_expression(
                  label_formula,
                  { [field_name]: value },
                  extra.req.user || { role_id: 100 },
                  "Dropdown label formula"
                )
              : label
        )
      );
      return select(
        {
          name: `ddfilter${field_name}`,
          class:
            "form-control form-select d-inline-maybe scfilter selectizable",
          style: full_width ? undefined : "width: unset;",
          required: true,
          onchange: `this.value=='' ? unset_state_field('${encodeURIComponent(
            field_name
          )}', this): set_state_field('${encodeURIComponent(
            field_name
          )}', this.value, this)`,
        },
        options
      );
    },
    action(segment) {
      const {
        block,
        action_label,
        action_style,
        action_size,
        action_icon,
        action_name,
        action_row_variable,
        configuration,
        confirm,
      } = segment;
      const label = action_label || action_name;

      const confirmStr = confirm ? `if(confirm('${"Are you sure?"}'))` : "";

      if (action_name === "Clear") {
        if (action_style === "btn-link")
          return a(
            {
              onclick: `${confirmStr}clear_state('${
                configuration?.omit_fields || ""
              }', this)`,
              href: "javascript:void(0)",
            },
            action_icon ? i({ class: action_icon }) + "&nbsp;" : false,
            label
          );
        else
          return button(
            {
              onClick: `${confirmStr}clear_state('${
                configuration?.omit_fields || ""
              }', this)`,
              class: `btn ${action_style || "btn-primary"} ${
                action_size || ""
              }`,
            },
            action_icon ? i({ class: action_icon }) + "&nbsp;" : false,
            label
          );
      } else {
        const withState =
          action_row_variable === "each_matching_row" ||
          action_row_variable === "state";
        const url = {
          javascript:
            `${confirmStr}view_post('${viewname}', 'run_action', {rndid:'${segment.rndid}'}, ` +
            `null, ${withState});`,
        };

        return action_link(url, extra.req, segment);
      }
    },
    toggle_filter({ field_name, value, preset_value, label, size, style }) {
      const field = fields.find((f) => f.name === field_name);
      const isBool = field && field.type.name === "Bool";

      const use_value =
        preset_value && field.presets
          ? field.presets[preset_value]({
              user: extra.req.user,
              req: extra.req,
            })
          : value;

      const active = isBool
        ? {
            on: state[field_name],
            off: state[field_name] === false,
            "?": state[field_name] === null,
          }[use_value]
        : eq_string(state[field_name], use_value);
      return button(
        {
          class: [
            "btn",
            active
              ? `btn-${style || "primary"}`
              : `btn-outline-${style || "primary"}`,
            size && size,
          ],
          onClick:
            active || use_value === undefined
              ? `unset_state_field('${encodeURIComponent(field_name)}', this)`
              : `set_state_field('${encodeURIComponent(field_name)}', '${
                  use_value || ""
                }', this)`,
        },
        label || value || preset_value
      );
    },
  };
  return div(
    { class: "form-namespace" },
    renderLayout({
      blockDispatch,
      layout,
      role,
      req: extra.req,
      hints: getState().getLayout(extra.req.user).hints || {},
    })
  );
}
@param {number} table_id
@param {string} viewname
@param {object} opts
@param {object[]} opts.columns
@param {object} opts.layout
@param {object} state
@param {object} extra
@returns {Promise<Layout>}