view_linker = (
  {
    view,
    relation,
    view_label,
    in_modal,
    view_label_formula,
    link_style = "",
    link_size = "",
    link_icon = "",
    icon = "",
    textStyle = "",
    link_bgcol,
    link_bordercol,
    link_textcol,
    in_dropdown,
    extra_state_fml,
    link_target_blank,
    link_title,
    link_class,
  },
  fields,
  __ = (s) => s,
  isWeb = true,
  user,
  targetPrefix = "",
  state = {},
  req,
  srcViewName,
  label_attr, //for sorting
  in_row_click
) => {
  const safePrefix = (targetPrefix || "").endsWith("/")
    ? targetPrefix.substring(0, targetPrefix.length - 1)
    : targetPrefix || "";
  const get_label = (def, row) => {
    if (!view_label || view_label.length === 0) return def;
    if (!view_label_formula) return __(view_label);
    return eval_expression(view_label, row, user, "View Link label formula");
  };
  const get_extra_state = (row) => {
    if (!extra_state_fml) return "";
    const ctx = {
      ...dollarizeObject(state),
      session_id: getSessionId(req),
      ...row,
    };
    const o = eval_expression(
      extra_state_fml,
      ctx,
      user,
      "View link extra state formula"
    );
    return Object.entries(o)
      .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
      .join("&");
  };

  if (relation) {
    const topview = View.findOne({ name: srcViewName });
    const srcTable = Table.findOne({ id: topview.table_id });
    const subview = View.findOne({ name: view });
    const subTable = Table.findOne({ id: subview.table_id });
    const relObj = new Relation(
      relation,
      subTable ? subTable.name : "",
      ViewDisplayType.NO_ROW_LIMIT
    );
    const type = relObj.type;
    return {
      label: view,
      key: (r) => {
        const query = pathToQuery(relObj, srcTable, subTable, r);
        if (query === null) return "";
        else {
          let label = "";
          if (type === "ParentShow") {
            const summary_field =
              r[`summary_field_${subTable.name.toLowerCase()}`];
            label = get_label(
              typeof summary_field === "undefined" ? view : summary_field,
              r
            );
          } else label = get_label(view, r);

          const target = `${safePrefix}/view/${encodeURIComponent(view)}${query}`;
          return link_view(
            isWeb || in_modal ? target : `javascript:execLink('${target}')`,
            label,
            in_modal && srcViewName && { reload_view: srcViewName },
            link_style,
            link_size,
            link_icon || icon,
            textStyle,
            link_bgcol,
            link_bordercol,
            link_textcol,
            in_dropdown && "dropdown-item",
            get_extra_state(r),
            link_target_blank,
            label_attr,
            link_title,
            link_class,
            req,
            in_row_click
          );
        }
      },
    };
  } else {
    // legacy relation path
    const [vtype, vrest] = view.split(":");
    switch (vtype) {
      case "Own":
        const vnm = vrest;
        const viewrow = View.findOne({ name: vnm });
        const get_query = get_view_link_query(fields, viewrow || {});
        return {
          label: vnm,
          key: (r) => {
            const target = `${safePrefix}/view/${encodeURIComponent(
              vnm
            )}${get_query(r)}`;
            return link_view(
              isWeb || in_modal ? target : `javascript:execLink('${target}')`,
              get_label(vnm, r),
              in_modal && srcViewName && { reload_view: srcViewName },
              link_style,
              link_size,
              link_icon || icon,
              textStyle,
              link_bgcol,
              link_bordercol,
              link_textcol,
              in_dropdown && "dropdown-item",
              get_extra_state(r),
              link_target_blank,
              label_attr,
              link_title,
              link_class,
              req,
              in_row_click
            );
          },
        };
      case "Independent":
        const ivnm = vrest;
        return {
          label: ivnm,
          key: (r) => {
            const target = `${safePrefix}/view/${encodeURIComponent(ivnm)}`;
            return link_view(
              isWeb || in_modal ? target : `javascript:execLink('${target}')`,
              get_label(ivnm, r),
              in_modal && srcViewName && { reload_view: srcViewName },
              link_style,
              link_size,
              link_icon || icon,
              textStyle,
              link_bgcol,
              link_bordercol,
              link_textcol,
              in_dropdown && "dropdown-item",
              get_extra_state(r),
              link_target_blank,
              label_attr,
              link_title,
              link_class,
              req,
              in_row_click
            );
          },
        };
      case "ChildList":
      case "OneToOneShow":
        const [viewnm, tbl, fld, throughTable, through] = vrest.split(".");
        const varPath = through ? `${throughTable}.${through}.${fld}` : fld;
        return {
          label: viewnm,
          key: (r) => {
            const target = `${safePrefix}/view/${encodeURIComponent(viewnm)}?${varPath}=${
              r.id
            }`;
            return link_view(
              isWeb || in_modal ? target : `javascript:execLink('${target}')`,
              get_label(viewnm, r),
              in_modal && srcViewName && { reload_view: srcViewName },
              link_style,
              link_size,
              link_icon || icon,
              textStyle,
              link_bgcol,
              link_bordercol,
              link_textcol,
              in_dropdown && "dropdown-item",
              get_extra_state(r),
              link_target_blank,
              label_attr,
              link_title,
              link_class,
              req,
              in_row_click
            );
          },
        };
      case "ParentShow":
        const [pviewnm, ptbl, pfld] = vrest.split(".");
        //console.log([pviewnm, ptbl, pfld])
        return {
          label: pviewnm,
          key: (r) => {
            const reffield = fields.find((f) => f.name === pfld);
            const summary_field = r[`summary_field_${ptbl.toLowerCase()}`];
            if (r[pfld]) {
              const target = `${safePrefix}/view/${encodeURIComponent(pviewnm)}?${
                reffield.refname
              }=${typeof r[pfld] === "object" ? r[pfld].id : r[pfld]}`;
              return link_view(
                isWeb || in_modal ? target : `javascript:execLink('${target}')`,
                get_label(
                  typeof summary_field === "undefined"
                    ? pviewnm
                    : summary_field,
                  r
                ),
                in_modal && srcViewName && { reload_view: srcViewName },
                link_style,
                link_size,
                link_icon || icon,
                textStyle,
                link_bgcol,
                link_bordercol,
                link_textcol,
                in_dropdown && "dropdown-item",
                get_extra_state(r),
                link_target_blank,
                label_attr,
                link_title,
                link_class,
                req,
                in_row_click
              );
            } else return "";
          },
        };
      default:
        throw new Error("Invalid relation: " + view);
    }
  }
}
@function
@param {object} opts
@param {string} opts.view
@param {string} opts.relation
@param {object} opts.view_label missing in contract
@param {object} opts.in_modal
@param {object} opts.view_label_formula
@param {string} [opts.link_style = ""]
@param {string} [opts.link_size = ""]
@param {string} [opts.link_icon = ""]
@param {string} [opts.textStyle = ""]
@param {string} [opts.link_bgcol]
@param {string} [opts.link_bordercol]
@param {string} [opts.link_textcol]
@param {Field[]} fields
@returns {object}