feat: add 'View Responses' menu item to Routing Form dropdown (#21114)

* feat: add 'View Responses' menu item to Routing Form dropdown

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: use nuqs createSerializer for URL generation

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: move serializer to data-table lib and use proper type constants

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: type errors in serializers and FormActions

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: use parseAsArrayOf with parseAsJson for proper nuqs serialization

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: update to use dataTableQueryParamsSerializer with proper nuqs pattern

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: extract parsers from DataTableProvider to be reused in serializers

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: remove deprecated activeFiltersSerializer and use empty array instead of null

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: only show View Responses menu when form id and teamId exist

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* remove teamId condition

* refactor: improve viewResponses URL generation with cleaner conditional

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* Update packages/features/data-table/lib/parsers.ts

* refactor: make constants in parsers.ts non-exported

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add explicit type annotations to constants in parsers.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add explicit type annotations and export DEFAULT_PAGE_SIZE in parsers.ts

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: add explicit type assertions to DEFAULT_ACTIVE_FILTERS and DEFAULT_SORTING

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-05-07 13:10:21 +00:00
1 parent 61397e5184
commit a4134aa5a3
7 files changed
+100 -28

No files matched your search

@@ -1566,6 +1566,7 @@
"workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully",
"download_responses": "Download responses",
"download_responses_description": "Download all responses to your form in CSV format.",
"view_responses": "View Responses",
"download": "Download",
"downloading": "Downloading",
"download_recording": "Download Recording",
@@ -4,6 +4,8 @@ import { useForm } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { dataTableQueryParamsSerializer } from "@calcom/features/data-table/lib/serializers";
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { RoutingFormEmbedButton, RoutingFormEmbedDialog } from "@calcom/features/embed/RoutingFormEmbed";
import { EmbedDialogProvider } from "@calcom/features/embed/lib/hooks/useEmbedDialogCtx";
@@ -400,6 +402,7 @@ type FormActionType =
| "duplicate"
| "download"
| "copyRedirectUrl"
| "viewResponses"
| "create";
type FormActionProps<T> = {
@@ -484,6 +487,17 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
create: {
onClick: () => setNewFormDialogState?.({ action: "new", target: "" }),
},
viewResponses: {
href: `/insights/routing${
routingForm?.id
? dataTableQueryParamsSerializer({
activeFilters: [
{ f: "formId", v: { type: ColumnFilterType.SINGLE_SELECT, data: routingForm.id } },
],
})
: ""
}`,
},
copyRedirectUrl: {
onClick: () => {
navigator.clipboard.writeText(redirectUrl);
@@ -141,6 +141,18 @@ const Actions = ({
StartIcon="download">
{t("download_responses")}
</FormAction>
{form?.id && (
<FormAction
action="viewResponses"
routingForm={form}
className="w-full"
color="minimal"
type="button"
data-testid="view-responses"
StartIcon="eye">
{t("view_responses")}
</FormAction>
)}
<FormAction
action="embed"
routingForm={form}
@@ -4,16 +4,23 @@ import type { SortingState, OnChangeFn, VisibilityState, ColumnSizingState } fro
// eslint-disable-next-line no-restricted-imports
import debounce from "lodash/debounce";
import { usePathname } from "next/navigation";
import { useQueryState, parseAsArrayOf, parseAsJson, parseAsInteger, parseAsString } from "nuqs";
import { useQueryState } from "nuqs";
import { createContext, useCallback, useEffect, useRef, useMemo } from "react";
import { useSegmentsNoop } from "./hooks/useSegmentsNoop";
import {
activeFiltersParser,
sortingParser,
columnVisibilityParser,
columnSizingParser,
segmentIdParser,
pageIndexParser,
pageSizeParser,
searchTermParser,
DEFAULT_PAGE_SIZE,
} from "./lib/parsers";
import {
type FilterValue,
ZSorting,
ZColumnVisibility,
ZActiveFilter,
ZColumnSizing,
type FilterSegmentOutput,
type ActiveFilters,
type UseSegments,
@@ -61,12 +68,6 @@ export type DataTableContextType = {
export const DataTableContext = createContext<DataTableContextType | null>(null);
const DEFAULT_ACTIVE_FILTERS: ActiveFilters = [];
const DEFAULT_SORTING: SortingState = [];
const DEFAULT_COLUMN_VISIBILITY: VisibilityState = {};
const DEFAULT_COLUMN_SIZING: ColumnSizingState = {};
const DEFAULT_PAGE_SIZE = 10;
interface DataTableProviderProps {
useSegments?: UseSegments;
tableIdentifier?: string;
@@ -83,26 +84,17 @@ export function DataTableProvider({
ctaContainerClassName = CTA_CONTAINER_CLASS_NAME,
}: DataTableProviderProps) {
const filterToOpen = useRef<string | undefined>(undefined);
const [activeFilters, setActiveFilters] = useQueryState(
"activeFilters",
parseAsArrayOf(parseAsJson(ZActiveFilter.parse)).withDefault(DEFAULT_ACTIVE_FILTERS)
);
const [sorting, setSorting] = useQueryState(
"sorting",
parseAsArrayOf(parseAsJson(ZSorting.parse)).withDefault(DEFAULT_SORTING)
);
const [activeFilters, setActiveFilters] = useQueryState("activeFilters", activeFiltersParser);
const [sorting, setSorting] = useQueryState("sorting", sortingParser);
const [columnVisibility, setColumnVisibility] = useQueryState<VisibilityState>(
"cols",
parseAsJson(ZColumnVisibility.parse).withDefault(DEFAULT_COLUMN_VISIBILITY)
columnVisibilityParser
);
const [columnSizing, setColumnSizing] = useQueryState<ColumnSizingState>(
"widths",
parseAsJson(ZColumnSizing.parse).withDefault(DEFAULT_COLUMN_SIZING)
);
const [segmentId, setSegmentId] = useQueryState("segment", parseAsInteger.withDefault(-1));
const [pageIndex, setPageIndex] = useQueryState("page", parseAsInteger.withDefault(0));
const [pageSize, setPageSize] = useQueryState("size", parseAsInteger.withDefault(defaultPageSize));
const [searchTerm, setSearchTerm] = useQueryState("q", parseAsString.withDefault(""));
const [columnSizing, setColumnSizing] = useQueryState<ColumnSizingState>("widths", columnSizingParser);
const [segmentId, setSegmentId] = useQueryState("segment", segmentIdParser);
const [pageIndex, setPageIndex] = useQueryState("page", pageIndexParser);
const [pageSize, setPageSize] = useQueryState("size", pageSizeParser);
const [searchTerm, setSearchTerm] = useQueryState("q", searchTermParser);
const setDebouncedSearchTerm = useMemo(
() => debounce((value: string | null) => setSearchTerm(value ? value.trim() : null), 500),
+1
View File
@@ -1,6 +1,7 @@
export * from "./components";
export * from "./lib/types";
export * from "./lib/utils";
export * from "./lib/serializers";
export * from "./DataTableProvider";
export * from "./hooks/useColumnResizing";
export * from "./hooks";
@@ -0,0 +1,24 @@
import type { SortingState, VisibilityState, ColumnSizingState } from "@tanstack/react-table";
import { parseAsArrayOf, parseAsJson, parseAsInteger, parseAsString } from "nuqs";
import { ZActiveFilter, ZSorting, ZColumnVisibility, ZColumnSizing } from "./types";
import type { ActiveFilters } from "./types";
const DEFAULT_ACTIVE_FILTERS: ActiveFilters = [] as ActiveFilters;
const DEFAULT_SORTING: SortingState = [] as SortingState;
const DEFAULT_COLUMN_VISIBILITY: VisibilityState = {};
const DEFAULT_COLUMN_SIZING: ColumnSizingState = {};
export const DEFAULT_PAGE_SIZE = 10;
export const activeFiltersParser = parseAsArrayOf(parseAsJson(ZActiveFilter.parse)).withDefault(
DEFAULT_ACTIVE_FILTERS
);
export const sortingParser = parseAsArrayOf(parseAsJson(ZSorting.parse)).withDefault(DEFAULT_SORTING);
export const columnVisibilityParser = parseAsJson(ZColumnVisibility.parse).withDefault(
DEFAULT_COLUMN_VISIBILITY
);
export const columnSizingParser = parseAsJson(ZColumnSizing.parse).withDefault(DEFAULT_COLUMN_SIZING);
export const segmentIdParser = parseAsInteger.withDefault(-1);
export const pageIndexParser = parseAsInteger.withDefault(0);
export const pageSizeParser = parseAsInteger.withDefault(DEFAULT_PAGE_SIZE);
export const searchTermParser = parseAsString.withDefault("");
@@ -0,0 +1,28 @@
import { createSerializer } from "nuqs";
import {
activeFiltersParser,
sortingParser,
columnVisibilityParser,
columnSizingParser,
segmentIdParser,
pageIndexParser,
pageSizeParser,
searchTermParser,
} from "./parsers";
/**
* Serializer for data table query parameters
* Used to generate URL query parameters for data tables
* Based on the parameters used in DataTableProvider
*/
export const dataTableQueryParamsSerializer = createSerializer({
activeFilters: activeFiltersParser,
sorting: sortingParser,
cols: columnVisibilityParser,
widths: columnSizingParser,
segment: segmentIdParser,
page: pageIndexParser,
size: pageSizeParser,
q: searchTermParser,
});