Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-cycles-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofgeist/kit": patch
---

Fixed infinite table queries for other field names
5 changes: 5 additions & 0 deletions .changeset/ninety-ligers-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofgeist/kit": patch
---

New infinite table editable template
2 changes: 1 addition & 1 deletion cli/src/cli/add/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function runAddAuthAction() {
abortIfCancel(
await p.select({
message: `What email provider do you want to use?\n${chalk.dim(
"Used to send email verification codes. If you skip this, the codes will be displayed in your terminal."
"Used to send email verification codes. If you skip this, the codes will be displayed here in your terminal."
)}`,
options: [
{
Expand Down
6 changes: 3 additions & 3 deletions cli/src/cli/add/fmschema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,6 @@ export const runAddSchemaAction = async (opts?: {
.map((s) => s.schemaName)
.filter(Boolean);

// list other common layout names to exclude
existingLayouts.push("-");

spinner.stop("Loaded layouts from your FileMaker file");

if (existingLayouts.length > 0) {
Expand All @@ -96,6 +93,9 @@ export const runAddSchemaAction = async (opts?: {
);
}

// list other common layout names to exclude
existingLayouts.push("-");

let passedInLayoutName: string | undefined = opts?.layoutName;
if (
passedInLayoutName === "" ||
Expand Down
8 changes: 2 additions & 6 deletions cli/src/generators/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function addAuth({
if (options.type === "clerk") {
await addClerkAuth({ projectDir });
} else if (options.type === "fmaddon") {
await addFmaddonAuth(options);
await addFmaddonAuth();
}

if (!noInstall) {
Expand All @@ -50,11 +50,7 @@ async function addClerkAuth({
mergeSettings({ auth: { type: "clerk" } });
}

async function addFmaddonAuth({
emailProvider,
}: {
emailProvider?: "plunk" | "resend";
}) {
async function addFmaddonAuth() {
await proofkitAuthInstaller();
mergeSettings({ auth: { type: "fmaddon" } });
}
14 changes: 3 additions & 11 deletions cli/src/installers/install-fm-addon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import chalk from "chalk";
import fs from "fs-extra";

import { PKG_ROOT } from "~/consts.js";
import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
import { logger } from "~/utils/logger.js";

export async function installFmAddon({
Expand Down Expand Up @@ -62,29 +61,22 @@ export async function installFmAddon({
if (addonName === "auth") {
console.log(
`${chalk.yellowBright(
"You must install the FM Add-on Auth addon in your FileMaker file."
"You must install the FM Add-on Auth addon in your FileMaker file to continue."
)} ${chalk.dim("(Learn more: https://proofkit.dev/auth/fm-addon)")}`
);
} else {
console.log(
`${chalk.yellowBright(
"You must install the ProofKit WebViewer addon in your FileMaker file."
"You must install the ProofKit WebViewer addon in your FileMaker file to continue."
)} ${chalk.dim("(Learn more: https://proofkit.dev/webviewer)")}`
);
}
const steps = [
"Restart FileMaker Pro (if it's currently running)",
`Open your FileMaker file, go to layout mode, and install the ${addonDisplayName} addon to the file`,
"Run the typegen command to add the types into your project:",
"Come back here to continue the installation",
];
steps.forEach((step, index) => {
console.log(`${index + 1}. ${step}`);
});

console.log(chalk.cyan(` ${getUserPkgManager()} typegen`));
console.log("");

throw new Error(
"You must install the FM Add-on Auth addon in your FileMaker file."
);
}
31 changes: 24 additions & 7 deletions cli/src/installers/proofkit-auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import path from "path";
import * as p from "@clack/prompts";
import { type OttoAPIKey } from "@proofgeist/fmdapi";
import chalk from "chalk";
import dotenv from "dotenv";
import fs from "fs-extra";
import { SyntaxKind, type SourceFile } from "ts-morph";

import { getLayouts } from "~/cli/fmdapi.js";
import { abortIfCancel, UserAbortedError } from "~/cli/utils.js";
import { PKG_ROOT } from "~/consts.js";
import { addConfig, runCodegenCommand } from "~/generators/fmdapi.js";
import { injectTanstackQuery } from "~/generators/tanstack-query.js";
Expand All @@ -16,7 +18,7 @@ import { getSettings } from "~/utils/parseSettings.js";
import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";
import { addToHeaderSlot } from "./auth-shared.js";
import { installFmAddon } from "./install-fm-addon.js";
import { installReactEmail } from "./react-email.js";
import { installEmailProvider, installPlunk } from "./react-email.js";

export const proofkitAuthInstaller = async () => {
const projectDir = state.projectDir;
Expand Down Expand Up @@ -105,7 +107,9 @@ export const proofkitAuthInstaller = async () => {
projectDir,
runCodegen: false,
});
await installReactEmail({ project });

// install email files based on the email provider in state
await installEmailProvider({ project });

protectMainLayout(
project.addSourceFileAtPath(
Expand All @@ -115,12 +119,25 @@ export const proofkitAuthInstaller = async () => {

await formatAndSaveSourceFiles(project);

const hasProofKitLayouts = await checkForProofKitLayouts(projectDir);
if (hasProofKitLayouts) {
await runCodegenCommand({ projectDir });
let hasProofKitLayouts = false;
while (!hasProofKitLayouts) {
hasProofKitLayouts = await checkForProofKitLayouts(projectDir);

if (!hasProofKitLayouts) {
const shouldContinue = abortIfCancel<boolean>(
await p.confirm({
message:
"I have followed the above instructions, continue installing",
initialValue: true,
active: "Continue",
inactive: "Abort",
})
);

if (!shouldContinue) throw new UserAbortedError();
}
}

await installDependencies({ projectDir });
await runCodegenCommand({ projectDir });
};

function addToSafeActionClient(sourceFile?: SourceFile) {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/installers/react-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { addToEnv } from "~/utils/addToEnvs.js";
import { logger } from "~/utils/logger.js";
import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js";

export async function installReactEmail({ ...args }: { project?: Project }) {
export async function installEmailProvider({ ...args }: { project?: Project }) {
const projectDir = state.projectDir;
addPackageDependency({
dependencies: ["@react-email/components", "@react-email/render"],
Expand Down
1 change: 1 addition & 0 deletions cli/template/extras/config/_eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const _initialConfig = {
],
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-misused-promises": [
"error",
{ checksVoidReturn: { attributes: false } },
Expand Down
84 changes: 84 additions & 0 deletions cli/template/pages/nextjs/table-infinite-edit/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use server";

import {
__TYPE_NAME__,
__ZOD_TYPE_NAME__,
} from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__";
import { __CLIENT_NAME__ } from "@/config/schemas/__SOURCE_NAME__/client";
import { __ACTION_CLIENT__ } from "@/server/safe-action";
import { ListParams, Query } from "@proofgeist/fmdapi/dist/client-types.js";
import dayjs from "dayjs";
import { z } from "zod";

import { idFieldName } from "./schema";

const limit = 50; // raise or lower this number depending on how your layout performs
export const fetchData = __ACTION_CLIENT__
.schema(
z.object({
offset: z.number().catch(0),
sorting: z.array(
z.object({ id: z.string(), desc: z.boolean().default(false) })
),
columnFilters: z.array(z.object({ id: z.string(), value: z.unknown() })),
})
)
.action(async ({ parsedInput: { offset, sorting, columnFilters } }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getOptions: ListParams<__TYPE_NAME__, any> & {
query: Query<__TYPE_NAME__>[];
} = {
limit,
offset,
query: [{ ["__FIRST_FIELD_NAME__"]: "*" }],
};

if (sorting.length > 0) {
getOptions.sort = sorting.map(({ id, desc }) => ({
fieldName: id as keyof __TYPE_NAME__,
sortOrder: desc ? "descend" : "ascend",
}));
}

if (columnFilters.length > 0) {
getOptions.query = columnFilters
.map(({ id, value }) => {
if (typeof value === "string") {
return {
[id]: value,
};
} else if (typeof value === "object" && value instanceof Date) {
return {
[id]: dayjs(value).format("YYYY+MM+DD"),
};
}
return null;
})
.filter(Boolean) as Query<any>[];
}

const data = await __CLIENT_NAME__.find(getOptions);

return {
data: data.data,
hasNextPage: data.dataInfo.foundCount > limit + offset,
totalCount: data.dataInfo.foundCount,
};
});

export const updateRecord = __ACTION_CLIENT__
.schema(__ZOD_TYPE_NAME__.partial())
.action(async ({ parsedInput }) => {
const id = parsedInput[idFieldName];
delete parsedInput[idFieldName]; // this ensures the id field value is not included in the updated fieldData
const data = parsedInput;

const {
data: { recordId },
} = await __CLIENT_NAME__.findOne({ query: { [idFieldName]: `==${id}` } });

return await __CLIENT_NAME__.update({
recordId,
fieldData: data,
});
});
23 changes: 23 additions & 0 deletions cli/template/pages/nextjs/table-infinite-edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Stack, Text, Code } from "@mantine/core";
import React from "react";

import TableContent from "./table";
import { idFieldName } from "./schema";

export default async function TablePage() {
return (
<Stack>
<div>
<Text>
This table allows editing. Double-click on a cell to edit the value.
</Text>
<Text size="sm" c="dimmed">
NOTE: This feature requires a primary key field on your API layout. If your
primary key field is not <Code>{idFieldName}</Code>, update the
<Code>idFieldName</Code> variable in the <Code>schema.ts</Code> file.
</Text>
</div>
<TableContent />
</Stack>
);
}
81 changes: 81 additions & 0 deletions cli/template/pages/nextjs/table-infinite-edit/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import { showErrorNotification } from "@/utils/notification-helpers";
import {
useInfiniteQuery,
useMutation,
useQueryClient,
} from "@tanstack/react-query";
import type {
MRT_ColumnFiltersState,
MRT_SortingState,
} from "mantine-react-table";
import { useMemo } from "react";

import { fetchData, updateRecord } from "./actions";
import { idFieldName } from "./schema";

export function useAllData({
sorting,
columnFilters,
}: {
sorting: MRT_SortingState;
columnFilters: MRT_ColumnFiltersState;
}) {
const queryKey = ["all-__SCHEMA_NAME__", sorting, columnFilters];
// useInfiniteQuery is used to help with automatic pagination
const qr = useInfiniteQuery({
queryKey,
queryFn: async ({ pageParam: offset }) => {
const result = await fetchData({ offset, sorting, columnFilters });
if (!result) throw new Error(`Failed to fetch __SCHEMA_NAME__`);
if (!result.data) throw new Error(`No data found for __SCHEMA_NAME__`);
return result?.data;
},
retry: false,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) =>
lastPage.hasNextPage
? pages.flatMap((page) => page.data).length
: undefined,
});

const flatData = useMemo(
() =>
qr.data?.pages.flatMap((page) => page.data).map((o) => o.fieldData) ?? [],
[qr.data]
);
const totalFetched = flatData.length;
const totalDBRowCount = qr.data?.pages?.[0]?.totalCount ?? 0;

const queryClient = useQueryClient();

const updateRecordMutation = useMutation({
mutationFn: updateRecord,
onMutate: async (newRecord) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey });

// Optimistically update to the new value
queryClient.setQueryData<typeof qr.data>(queryKey, (old) => {
if (!old) return old;
return {
...old,
pages: old.pages.map((page) => ({
...page,
data: page.data.map((row) =>
row.fieldData[idFieldName] === newRecord[idFieldName]
? { ...row, fieldData: { ...row.fieldData, ...newRecord } }
: row,
),
})),
};
});
},
onError: () => {
showErrorNotification("Failed to update record");
},
});

return { ...qr, data: flatData, totalDBRowCount, totalFetched, updateRecord: updateRecordMutation.mutate };
}
4 changes: 4 additions & 0 deletions cli/template/pages/nextjs/table-infinite-edit/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { type __TYPE_NAME__ } from "@/config/schemas/__SOURCE_NAME__/__SCHEMA_NAME__";

// TODO: Make sure this variable is properly set to your primary key field
export const idFieldName: keyof __TYPE_NAME__ = "__FIRST_FIELD_NAME__";
Loading