pankod/refine

View on GitHub
packages/inferencer/src/inferencers/headless/list.tsx

Summary

Maintainability
F
5 days
Test Coverage
import { useTable } from "@refinedev/react-table";
import { flexRender } from "@tanstack/react-table";

import { createInferencer } from "../../create-inferencer";
import {
  jsx,
  componentName,
  accessor,
  printImports,
  dotAccessor,
  noOp,
  getVariableName,
  translatePrettyString,
  translateButtonTitle,
  translateActionTitle,
  getMetaProps,
  deepHasKey,
} from "../../utilities";

import { ErrorComponent } from "./error";
import { LoadingComponent } from "./loading";
import { SharedCodeViewer } from "../../components/shared-code-viewer";

import {
  InferencerResultComponent,
  InferField,
  ImportElement,
  RendererContext,
} from "../../types";

const getAccessorKey = (field: InferField) => {
  return Array.isArray(field.accessor) || field.multiple
    ? `accessorKey: "${field.key}"`
    : field.accessor
      ? `accessorKey: "${dotAccessor(field.key, undefined, field.accessor)}"`
      : `accessorKey: "${field.key}"`;
};

/**
 * a renderer function for list page with unstyled html elements
 * @internal used internally from inferencer components
 */
export const renderer = ({
  resource,
  fields,
  meta,
  isCustomPage,
  i18n,
}: RendererContext) => {
  const COMPONENT_NAME = componentName(resource.label ?? resource.name, "list");
  const recordName = "tableData?.data";
  const imports: Array<ImportElement> = [
    ["React", "react", true],
    ["useNavigation", "@refinedev/core"],
    ["useTable", "@refinedev/react-table"],
    ["ColumnDef", "@tanstack/react-table"],
    ["flexRender", "@tanstack/react-table"],
  ];

  if (i18n) {
    imports.push(["useTranslate", "@refinedev/core"]);
  }

  // has gqlQuery or gqlMutation in "meta"
  const hasGql = deepHasKey(meta || {}, ["gqlQuery", "gqlMutation"]);
  if (hasGql) {
    imports.push(["gql", "graphql-tag", true]);
  }

  const relationFields: (InferField | null)[] = fields.filter(
    (field) => field?.relation && !field?.fieldable && field?.resource,
  );

  const relationHooksCode = relationFields
    .filter(Boolean)
    .map((field) => {
      if (field?.relation && !field.fieldable && field.resource) {
        imports.push(["GetManyResponse", "@refinedev/core"]);
        imports.push(["useMany", "@refinedev/core"]);

        let idsString = "";

        if (field.multiple) {
          idsString = `[].concat(...(${recordName}?.map((item) => ${accessor(
            "item",
            field.key,
            field.accessor,
            false,
          )}) ?? []))`;
        } else {
          idsString = `${recordName}?.map((item) => ${accessor(
            "item",
            field.key,
            field.accessor,
            false,
          )}) ?? []`;
        }

        return `
                const { data: ${getVariableName(field.key, "Data")} } =
                useMany({
                    resource: "${field.resource.name}",
                    ids: ${idsString},
                    queryOptions: {
                        enabled: !!${recordName},
                    },
                    ${getMetaProps(
                      field?.resource?.identifier ?? field?.resource?.name,
                      meta,
                      ["getMany"],
                    )}
                });
                `;
      }
      return undefined;
    })
    .filter(Boolean);

  const relationVariableNames = relationFields
    ?.map((field) => {
      if (field?.resource) {
        return getVariableName(field.key, "Data");
      }
      return undefined;
    })
    .filter(Boolean);

  const renderRelationFields = (field: InferField) => {
    if (field.relation && field.resource) {
      const variableName = `${getVariableName(field.key, "Data")}?.data`;

      if (Array.isArray(field.accessor)) {
        return undefined;
      }

      const id = `id: "${field.key}"`;
      const header = `header: ${translatePrettyString({
        resource,
        field,
        i18n,
        noBraces: true,
      })}`;
      const accessorKey = getAccessorKey(field);

      let cell = "";

      // if multiple, then map it with tagfield
      // if not, then just show the value

      if (field.multiple) {
        let val = "item";

        // for multiple
        if (field?.relationInfer) {
          val = accessor("item", undefined, field.relationInfer.accessor);
        }

        if (
          field?.relationInfer &&
          field?.relationInfer?.type === "object" &&
          !field?.relationInfer?.accessor
        ) {
          console.log(
            "@refinedev/inferencer: Inferencer failed to render this field",
            {
              key: field.key,
              relation: field.relationInfer,
            },
          );

          return `cell: function render({ getValue }) {
                        return (
                            <span title="Inferencer failed to render this field (Cannot find key)">Cannot Render</span>
                        )
                    }`;
        }

        cell = `cell: function render({ getValue, table }) {
                    const meta = table.options.meta as {
                        ${getVariableName(field.key, "Data")}: GetManyResponse;
                    };

                    ${field?.accessor ? "try {" : ""}

                    const ${getVariableName(field.key, "")} = getValue<any[]>()?.map((item) => {
                        return meta.${getVariableName(field.key, "Data")}?.data?.find(
                            (resourceItems) => resourceItems.id === ${accessor(
                              "item",
                              undefined,
                              field.accessor,
                            )}
                        );
                    })


                    return (
                        <ul>
                            {${getVariableName(field.key, "")}?.map((item, index) => (
                                <li key={index}>
                                    {${val}}
                                </li>
                            ))}
                        </ul>
                    )
                    ${
                      field?.accessor ? " } catch (error) { return null; }" : ""
                    }
                }
            `;
      } else {
        if (field?.relationInfer) {
          const cannotRender =
            field?.relationInfer?.type === "object" &&
            !field?.relationInfer?.accessor;

          if (cannotRender) {
            console.log(
              "@refinedev/inferencer: Inferencer failed to render this field",
              {
                key: field.key,
                relation: field.relationInfer,
              },
            );
          }

          cell = `cell: function render({ getValue, table }) {
                        const meta = table.options.meta as {
                            ${getVariableName(field.key, "Data")}: GetManyResponse;
                        };

                        ${field?.accessor ? "try {" : ""}

                        const ${getVariableName(
                          field.key,
                          "",
                        )} = meta.${variableName}?.find(
                            (item) => item.id == getValue<any>(),
                        );

                        return ${
                          cannotRender
                            ? `<span title="Inferencer failed to render this field (Cannot find key)">Cannot Render</span>`
                            : `${accessor(
                                getVariableName(field.key),
                                undefined,
                                field?.relationInfer?.accessor,
                              )} ?? "Loading..."`
                        };

                        ${
                          field?.accessor
                            ? " } catch (error) { return null; }"
                            : ""
                        }
                    },`;
        } else {
          cell = "";
        }
      }

      return `
                {
                    ${id},
                    ${header},
                    ${accessorKey},
                    ${cell}
                }
            `;
    }
    return undefined;
  };

  const imageFields = (field: InferField) => {
    if (field.type === "image") {
      const id = `id: "${field.key}"`;
      const accessorKey = getAccessorKey(field);
      const header = `header: ${translatePrettyString({
        resource,
        field,
        i18n,
        noBraces: true,
      })}`;

      let cell = jsx`
                cell: function render({ getValue }) {
                    ${field?.accessor ? "try {" : ""}
                        return <img style={{ maxWidth: "100px" }} src={${accessor(
                          "getValue<any>()",
                          undefined,
                          Array.isArray(field.accessor)
                            ? field.accessor
                            : undefined,
                          " + ",
                        )}} />
                    ${
                      field?.accessor ? " } catch (error) { return null; }" : ""
                    }
                }
            `;

      if (field.multiple) {
        const val = accessor("item", undefined, field.accessor, " + ");

        cell = `
                    cell: function render({ getValue }) {
                        ${field?.accessor ? "try {" : ""}
                            return (
                                <ul>
                                    {getValue<any[]>()?.map((item, index) => (
                                        <li key={index}><img src={${val}} style={{ height: "50px", maxWidth: "100px" }} /></li>
                                    ))}
                                </ul>
                            )
                        ${
                          field?.accessor
                            ? " } catch (error) { return null; }"
                            : ""
                        }
                    }
                `;
      }

      return `
                {
                    ${id},
                    ${accessorKey},
                    ${header},
                    ${cell}
                }
            `;
    }
    return undefined;
  };

  const emailFields = (field: InferField) => {
    if (field.type === "email") {
      const id = `id: "${field.key}"`;
      const accessorKey = getAccessorKey(field);
      const header = `header: ${translatePrettyString({
        resource,
        field,
        i18n,
        noBraces: true,
      })}`;

      let cell = jsx`
                cell: function render({ getValue }) {
                    return <a href={"mailto:" + ${accessor(
                      "getValue<any>()",
                      undefined,
                      Array.isArray(field.accessor)
                        ? field.accessor
                        : undefined,
                      ' + " " + ',
                    )}}>{${accessor(
                      "getValue<any>()",
                      undefined,
                      Array.isArray(field.accessor)
                        ? field.accessor
                        : undefined,
                      ' + " " + ',
                    )}}</a>
                }
            `;

      if (field.multiple) {
        const val = accessor("item", undefined, field.accessor, " + ");

        cell = `
                    cell: function render({ getValue }) {
                        return (
                            <ul>
                                {getValue<any[]>()?.map((item, index) => (
                                    <li key={index}>
                                        {${val}}
                                    </li>
                                ))}
                            </ul>
                        )
                    }
                `;
      }

      return `
                {
                    ${id},
                    ${accessorKey},
                    ${header},
                    ${cell}
                }
            `;
    }
    return undefined;
  };

  const urlFields = (field: InferField) => {
    if (field.type === "url") {
      const id = `id: "${field.key}"`;
      const accessorKey = getAccessorKey(field);
      const header = `header: ${translatePrettyString({
        resource,
        field,
        i18n,
        noBraces: true,
      })}`;

      let cell = jsx`
                cell: function render({ getValue }) {
                    return <a href={${accessor(
                      "getValue<any>()",
                      undefined,
                      Array.isArray(field.accessor)
                        ? field.accessor
                        : undefined,
                      " + ",
                    )}}>{${accessor(
                      "getValue<any>()",
                      undefined,
                      Array.isArray(field.accessor)
                        ? field.accessor
                        : undefined,
                      " + ",
                    )}}</a>
                }
            `;

      if (field.multiple) {
        const val = accessor("item", undefined, field.accessor, " + ");

        cell = `
                    cell: function render({ getValue }) {
                        return (
                            <ul>
                                {getValue<any[]>()?.map((item, index) => (
                                    <li key={index}>
                                        {${val}}
                                    </li>
                                ))}
                            </ul>
                        )
                    }
                `;
      }

      return `
                {
                    ${id},
                    ${accessorKey},
                    ${header},
                    ${cell}
                }
            `;
    }
    return undefined;
  };

  const booleanFields = (field: InferField) => {
    if (field?.type === "boolean") {
      const id = `id: "${field.key}"`;
      const accessorKey = getAccessorKey(field);
      const header = `header: ${translatePrettyString({
        resource,
        field,
        i18n,
        noBraces: true,
      })}`;

      let cell = jsx`
                cell: function render({ getValue }) {
                    return ${accessor(
                      "getValue<any>()",
                      undefined,
                      Array.isArray(field.accessor)
                        ? field.accessor
                        : undefined,
                      " + ",
                    )} ? "yes" : "no"
                }
            `;

      if (field.multiple) {
        const val = accessor("item", undefined, field.accessor, " + ");

        cell = `
                    cell: function render({ getValue }) {
                        return (
                            <ul>
                                {getValue<any[]>()?.map((item, index) => (
                                    <li key={index}>
                                        {${val} ? "yes" : "no"}
                                    </li>
                                ))}
                            </ul>
                        );
                    }
                `;
      }

      return `
                {
                    ${id},
                    ${accessorKey},
                    ${header},
                    ${cell}
                }
            `;
    }

    return undefined;
  };

  const dateFields = (field: InferField) => {
    if (field.type === "date") {
      const id = `id: "${field.key}"`;
      const accessorKey = getAccessorKey(field);
      const header = `header: ${translatePrettyString({
        resource,
        field,
        i18n,
        noBraces: true,
      })}`;

      let cell = jsx`
                cell: function render({ getValue }) {
                    return (new Date(${accessor(
                      "getValue<any>()",
                      undefined,
                      Array.isArray(field.accessor)
                        ? field.accessor
                        : undefined,
                      ' + " " + ',
                    )})).toLocaleString(undefined, { timeZone: "UTC" })
                }
            `;

      if (field.multiple) {
        const val = accessor("item", undefined, field.accessor, " + ");

        cell = `
                    cell: function render({ getValue }) {
                        return (
                            <ul>
                                {getValue<any[]>()?.map((item, index) => (
                                    <li key={index}>
                                    {(new Date(${val})).toLocaleString(undefined, { timeZone: "UTC" })}
                                    </li>
                                ))}
                            </ul>
                        )
                    }
                `;
      }

      return `
                {
                    ${id},
                    ${accessorKey},
                    ${header},
                    ${cell}
                }
            `;
    }
    return undefined;
  };

  const basicFields = (field: InferField) => {
    if (
      field &&
      (field.type === "text" ||
        field.type === "number" ||
        field.type === "richtext")
    ) {
      const id = `id: "${field.key}"`;
      const accessorKey = getAccessorKey(field);
      const header = `header: ${translatePrettyString({
        resource,
        field,
        i18n,
        noBraces: true,
      })}`;

      let cell = "";

      if (field.multiple) {
        const val = accessor("item", undefined, field.accessor, ' + " " + ');

        cell = `
                    cell: function render({ getValue }) {
                        return (
                            <ul>
                                {getValue<any[]>()?.map((item, index) => (
                                    <li key={index}>
                                        {${val}}
                                    </li>
                                ))}
                            </ul>
                        )
                    }
                `;
      }

      if (!field.multiple && Array.isArray(field.accessor)) {
        cell = `
                    cell: function render({ getValue }) {
                        return (
                            <>{${accessor(
                              "getValue<any>()",
                              field.key,
                              field.accessor,
                            )}}</>
                        );
                    }
                `;
      }

      return `
                {
                    ${id},
                    ${accessorKey},
                    ${header},
                    ${cell}
                }
            `;
    }
    return undefined;
  };

  const { canEdit, canShow, canCreate } = resource ?? {};

  const actionColumnTitle = i18n ? `translate("table.actions")` : `"Actions"`;

  const actionButtons =
    canEdit || canShow
      ? jsx`
    {
        id: "actions",
        accessorKey: "id",
        header: ${actionColumnTitle},
        cell: function render({ getValue }) {
            return (
                <div
                    style={{
                        display: "flex",
                        flexDirection: "row",
                        flexWrap: "wrap",
                        gap: "4px",
                    }}
                >
                ${
                  canShow
                    ? jsx`
                    <button
                        onClick={() => {
                            show("${resource.name}", getValue() as string);
                        }}
                    >
                        ${translateButtonTitle({
                          action: "show",
                          i18n,
                          noQuotes: true,
                        })}
                    </button>
                    `
                    : ""
                }
                    ${
                      canEdit
                        ? jsx`
                            <button
                            onClick={() => {
                                edit("${resource.name}", getValue() as string);
                            }}
                        >
                            ${translateButtonTitle({
                              action: "edit",
                              i18n,
                              noQuotes: true,
                            })}
                        </button>
                    `
                        : ""
                    }
                </div>
            );
        },
    },
        `
      : "";

  const renderedFields: Array<string | undefined> = fields.map((field) => {
    switch (field?.type) {
      case "text":
      case "number":
      case "richtext":
        return basicFields(field);
      case "email":
        return emailFields(field);
      case "image":
        return imageFields(field);
      case "date":
        return dateFields(field);
      case "boolean":
        return booleanFields(field);
      case "url":
        return urlFields(field);
      case "relation":
        return renderRelationFields(field);
      default:
        return undefined;
    }
  });

  noOp(imports);

  const useTranslateHook = i18n && "const translate = useTranslate();";

  return jsx`
    ${printImports(imports)}
    
    export const ${COMPONENT_NAME} = () => {
        ${useTranslateHook}
        const columns = React.useMemo<ColumnDef<any>[]>(() => [
            ${[...renderedFields, actionButtons].filter(Boolean).join(",")}
        ], [${i18n ? "translate" : ""}]);

        ${
          canEdit || canShow
            ? jsx`
        const { ${canEdit ? "edit," : ""} ${canShow ? "show," : ""} ${
          canCreate ? "create," : ""
        } } = useNavigation();
        `
            : ""
        }

        const {
            getHeaderGroups,
            getRowModel,
            setOptions,
            refineCore: {
                tableQueryResult: { data: tableData },
            },
            getState,
            setPageIndex,
            getCanPreviousPage,
            getPageCount,
            getCanNextPage,
            nextPage,
            previousPage,
            setPageSize,
            getColumn,
        } = useTable({
            columns,
            ${
              isCustomPage
                ? `
            refineCoreProps: {
                resource: "${resource.name}",
                ${getMetaProps(resource?.identifier ?? resource?.name, meta, [
                  "getList",
                ])}
            }
            `
                : getMetaProps(resource?.identifier ?? resource?.name, meta, [
                      "getList",
                    ])
                  ? `refineCoreProps: { ${getMetaProps(
                      resource?.identifier ?? resource?.name,
                      meta,
                      ["getList"],
                    )} },`
                  : ""
            }
            
        });

        ${relationHooksCode}

        setOptions((prev) => ({
            ...prev,
            meta: {
                ...prev.meta,
                ${relationVariableNames.join(", ")}
            },
        }));

        return (
            <div style={{ padding: "16px" }}>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                <h1>${translateActionTitle({
                  resource,
                  action: "list",
                  i18n,
                })}</h1>
                ${
                  canCreate
                    ? jsx`<button onClick={() => create("${
                        resource.name
                      }")}>${translateButtonTitle({
                        action: "create",
                        i18n,
                        noQuotes: true,
                      })}</button>`
                    : ""
                }
            </div>
                <div style={{ maxWidth: "100%", overflowY: "scroll" }}>
                    <table>
                        <thead>
                            {getHeaderGroups().map((headerGroup) => (
                                <tr key={headerGroup.id}>
                                    {headerGroup.headers.map((header) => (
                                        <th key={header.id}>
                                            {!header.isPlaceholder && (
                                                flexRender(
                                                    header.column.columnDef
                                                        .header,
                                                    header.getContext(),
                                                )
                                            )}
                                        </th>
                                    ))}
                                </tr>
                            ))}
                        </thead>
                        <tbody>
                            {getRowModel().rows.map((row) => (
                                <tr key={row.id}>
                                    {row.getVisibleCells().map((cell) => (
                                        <td key={cell.id}>
                                            {flexRender(
                                                cell.column.columnDef.cell,
                                                cell.getContext(),
                                            )}
                                        </td>
                                    ))}
                                </tr>
                            ))}
                        </tbody>
                    </table>
                </div>
                <div style={{ marginTop: "12px" }}>
                    <button
                        onClick={() => setPageIndex(0)}
                        disabled={!getCanPreviousPage()}
                    >
                        {"<<"}
                    </button>
                    <button
                        onClick={() => previousPage()}
                        disabled={!getCanPreviousPage()}
                    >
                        {"<"}
                    </button>
                    <button onClick={() => nextPage()} disabled={!getCanNextPage()}>
                        {">"}
                    </button>
                    <button
                        onClick={() => setPageIndex(getPageCount() - 1)}
                        disabled={!getCanNextPage()}
                    >
                        {">>"}
                    </button>
                    <span>
                        <strong>
                            {" "} {getState().pagination.pageIndex + 1} / {" "} {getPageCount()} {" "}
                        </strong>
                    </span>
                    <span>
                        | ${
                          i18n ? `{translate("pagination.go")}` : "Go to Page"
                        }:{" "}
                        <input
                            type="number"
                            defaultValue={getState().pagination.pageIndex + 1}
                            onChange={(e) => {
                                const page = e.target.value
                                    ? Number(e.target.value) - 1
                                    : 0;
                                setPageIndex(page);
                            }}
                        />
                    </span>{" "}
                    <select
                        value={getState().pagination.pageSize}
                        onChange={(e) => {
                            setPageSize(Number(e.target.value));
                        }}
                    >
                        {[10, 20, 30, 40, 50].map((pageSize) => (
                            <option key={pageSize} value={pageSize}>
                                ${
                                  i18n
                                    ? `{translate("pagination.show")}`
                                    : "Show"
                                } {pageSize}
                            </option>
                        ))}
                    </select>
                </div>
            </div>   
        );
    };
    `;
};

/**
 * @experimental This is an experimental component
 */
export const ListInferencer: InferencerResultComponent = createInferencer({
  type: "list",
  additionalScope: [
    ["@refinedev/react-table", "RefineReactTable", { useTable }],
    ["@tanstack/react-table", "TanstackReactTable", { flexRender }],
  ],
  codeViewerComponent: SharedCodeViewer,
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  renderer,
});