Compare commits
5 Commits
main
...
gh-readonl
Author | SHA1 | Date | |
---|---|---|---|
|
efdca50ddc | ||
|
711959048b | ||
|
e7ddcf23a3 | ||
|
02b9505882 | ||
|
a6065a4dbd |
|
@ -0,0 +1,21 @@
|
|||
name: "Validate PRs"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
validate-pr:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -39,10 +39,10 @@ test.describe("Managed Event Types tests", () => {
|
|||
await page.click("[data-testid=new-event-type-dropdown]");
|
||||
await page.click("[data-testid=option-team-1]");
|
||||
// Expecting we can add a managed event type as team owner
|
||||
await expect(page.locator('input[value="MANAGED"]')).toBeVisible();
|
||||
await expect(page.locator('button[value="MANAGED"]')).toBeVisible();
|
||||
|
||||
// Actually creating a managed event type to test things further
|
||||
await page.click('input[value="MANAGED"]');
|
||||
await page.click('button[value="MANAGED"]');
|
||||
await page.fill("[name=title]", "managed");
|
||||
await page.click("[type=submit]");
|
||||
});
|
||||
|
|
|
@ -77,5 +77,5 @@ it("should render city name as option label if cityData is not empty", () => {
|
|||
});
|
||||
|
||||
it("should return timezone as option label if cityData is empty", () => {
|
||||
expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los_Angeles GMT -8:00"`);
|
||||
expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los Angeles GMT -8:00"`);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import jsonLogic from "json-logic-js";
|
||||
|
||||
// converts input to lowercase if string
|
||||
function normalize<T extends string | string[]>(input: T): T {
|
||||
if (typeof input === "string") {
|
||||
return input.toLowerCase() as T;
|
||||
}
|
||||
if (input instanceof Array) {
|
||||
return input.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return item.toLowerCase();
|
||||
}
|
||||
// if array item is not a string, return it as is
|
||||
return item;
|
||||
}) as T;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single Select equals and not equals uses it
|
||||
* Short Text equals and not equals uses it
|
||||
*/
|
||||
jsonLogic.add_operation("==", function (a: any, b: any) {
|
||||
return normalize(a) == normalize(b);
|
||||
});
|
||||
|
||||
jsonLogic.add_operation("===", function (a: any, b: any) {
|
||||
return normalize(a) === normalize(b);
|
||||
});
|
||||
|
||||
jsonLogic.add_operation("!==", function (a: any, b: any) {
|
||||
return normalize(a) !== normalize(b);
|
||||
});
|
||||
|
||||
jsonLogic.add_operation("!=", function (a: any, b: any) {
|
||||
return normalize(a) != normalize(b);
|
||||
});
|
||||
|
||||
/**
|
||||
* Multiselect "equals" and "not equals" uses it
|
||||
* Singleselect "any in" and "not in" uses it
|
||||
* Long Text/Short Text/Email/Phone "contains" also uses it.
|
||||
*/
|
||||
jsonLogic.add_operation("in", function (a: string, b: string | string[]) {
|
||||
const first = normalize(a);
|
||||
const second = normalize(b);
|
||||
if (!second) return false;
|
||||
return second.indexOf(first) !== -1;
|
||||
});
|
||||
|
||||
export default jsonLogic;
|
|
@ -1,5 +1,4 @@
|
|||
import type { App_RoutingForms_Form } from "@prisma/client";
|
||||
import jsonLogic from "json-logic-js";
|
||||
import { Utils as QbUtils } from "react-awesome-query-builder";
|
||||
import type { z } from "zod";
|
||||
|
||||
|
@ -8,6 +7,7 @@ import type { zodNonRouterRoute } from "../zod";
|
|||
import { getQueryBuilderConfig } from "./getQueryBuilderConfig";
|
||||
import { isFallbackRoute } from "./isFallbackRoute";
|
||||
import isRouter from "./isRouter";
|
||||
import jsonLogic from "./jsonLogicOverrides";
|
||||
|
||||
export function processRoute({
|
||||
form,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useState, Suspense } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
@ -8,14 +9,7 @@ import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
|||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
UpgradeTeamsBadge,
|
||||
} from "@calcom/ui";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Download } from "@calcom/ui/components/icon";
|
||||
|
||||
|
@ -101,6 +95,7 @@ const useRecordingDownload = () => {
|
|||
const ViewRecordingsList = ({ roomName, hasTeamPlan }: { roomName: string; hasTeamPlan: boolean }) => {
|
||||
const { t } = useLocale();
|
||||
const { setRecordingId, isFetching, recordingId } = useRecordingDownload();
|
||||
const router = useRouter();
|
||||
|
||||
const { data: recordings } = trpc.viewer.getCalVideoRecordings.useQuery(
|
||||
{ roomName },
|
||||
|
@ -121,7 +116,7 @@ const ViewRecordingsList = ({ roomName, hasTeamPlan }: { roomName: string; hasTe
|
|||
{recordings.data.map((recording: RecordingItemSchema, index: number) => {
|
||||
return (
|
||||
<div
|
||||
className="flex w-full items-center justify-between rounded-md border px-4 py-2"
|
||||
className="border-subtle flex w-full items-center justify-between rounded-md border px-4 py-2"
|
||||
key={recording.id}>
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-sm font-semibold">
|
||||
|
@ -138,7 +133,12 @@ const ViewRecordingsList = ({ roomName, hasTeamPlan }: { roomName: string; hasTe
|
|||
{t("download")}
|
||||
</Button>
|
||||
) : (
|
||||
<UpgradeTeamsBadge />
|
||||
<Button
|
||||
tooltip={t("upgrade_to_access_recordings_description")}
|
||||
className="ml-4 lg:ml-0"
|
||||
onClick={() => router.push("/teams")}>
|
||||
{t("upgrade")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { useRouter } from "next/router";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Users } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function UpgradeRecordingBanner() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="bg-subtle flex items-start gap-2 rounded-md p-4">
|
||||
<Users className="dark:bg-gray-90 inline-block h-5 w-5" />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold">{t("upgrade_to_access_recordings_title")}</h2>
|
||||
<p className="text-sm font-normal">{t("upgrade_to_access_recordings_description")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.push(`${WEBAPP_URL}/teams`);
|
||||
}}>
|
||||
{t("upgrade_now")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -254,6 +254,9 @@ export default function CreateEventTypeDialog({
|
|||
/>
|
||||
)}
|
||||
<RadioArea.Group
|
||||
onValueChange={(val: SchedulingType) => {
|
||||
form.setValue("schedulingType", val);
|
||||
}}
|
||||
className={classNames(
|
||||
"mt-1 flex gap-4",
|
||||
isAdmin && flags["managed-event-types"] && "flex-col"
|
||||
|
|
|
@ -35,5 +35,6 @@ export const addCitiesToDropdown = (cities: ICity[]) => {
|
|||
export const handleOptionLabel = (option: ITimezoneOption, cities: ICity[]) => {
|
||||
const timezoneValue = option.label.split(")")[0].replace("(", " ").replace("T", "T ");
|
||||
const cityName = option.label.split(") ")[1];
|
||||
return cities.length > 0 ? `${cityName}${timezoneValue}` : `${option.value}${timezoneValue}`;
|
||||
const refactoredOption = option.value.replace(/_/g, " ");
|
||||
return cities.length > 0 ? `${cityName}${timezoneValue}` : `${refactoredOption}${timezoneValue}`;
|
||||
};
|
||||
|
|
|
@ -34,6 +34,13 @@ export function TimezoneSelect({
|
|||
});
|
||||
}, [components]);
|
||||
|
||||
// We use modifiedTimezones in place of the allTimezones object replacing any underscores in the curly braces
|
||||
// with spaces and removing the America/Detroit timezone, adding the America/New_York timezone instead.
|
||||
const modifiedTimezones = useMemo(() => {
|
||||
const { "America/Detroit": _, ...rest } = allTimezones;
|
||||
return { ...rest, "America/New_York": "New York" };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BaseSelect
|
||||
className={className}
|
||||
|
@ -41,13 +48,15 @@ export function TimezoneSelect({
|
|||
isDisabled={isLoading}
|
||||
{...reactSelectProps}
|
||||
timezones={{
|
||||
...allTimezones,
|
||||
...modifiedTimezones,
|
||||
...addCitiesToDropdown(cities),
|
||||
"America/Asuncion": "Asuncion",
|
||||
}}
|
||||
onInputChange={handleInputChange}
|
||||
{...props}
|
||||
formatOptionLabel={(option) => <p className="truncate">{(option as ITimezoneOption).value}</p>}
|
||||
formatOptionLabel={(option) => (
|
||||
<p className="truncate">{(option as ITimezoneOption).value.replace(/_/g, " ")}</p>
|
||||
)}
|
||||
getOptionLabel={(option) => handleOptionLabel(option as ITimezoneOption, cities)}
|
||||
classNames={{
|
||||
...timezoneClassNames,
|
||||
|
|
|
@ -1,59 +1,57 @@
|
|||
import React from "react";
|
||||
import { useId } from "@radix-ui/react-id";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
type RadioAreaProps = React.InputHTMLAttributes<HTMLInputElement> & { classNames?: { container?: string } };
|
||||
type RadioAreaProps = RadioGroupPrimitive.RadioGroupItemProps & {
|
||||
children: ReactNode;
|
||||
classNames?: { container?: string };
|
||||
};
|
||||
|
||||
const RadioArea = React.forwardRef<HTMLInputElement, RadioAreaProps>(
|
||||
({ children, className, classNames: innerClassNames, ...props }, ref) => {
|
||||
return (
|
||||
<label className={classNames("relative flex", className)}>
|
||||
<input
|
||||
ref={ref}
|
||||
className="text-emphasis bg-subtle border-emphasis focus:ring-none peer absolute top-[0.9rem] left-3 align-baseline"
|
||||
type="radio"
|
||||
{...props}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
"text-default peer-checked:border-emphasis border-subtle rounded-md border p-4 pt-3 pl-10",
|
||||
innerClassNames?.container
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
);
|
||||
type MaybeArray<T> = T[] | T;
|
||||
type ChildrenOfType<T extends React.ElementType> = MaybeArray<
|
||||
React.ReactElement<React.ComponentPropsWithoutRef<T>>
|
||||
>;
|
||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
||||
onChange?: (value: string) => void;
|
||||
children: ChildrenOfType<typeof RadioArea>;
|
||||
}
|
||||
const RadioArea = ({ children, className, classNames: innerClassNames, ...props }: RadioAreaProps) => {
|
||||
const radioAreaId = useId();
|
||||
const id = props.id ?? radioAreaId;
|
||||
|
||||
const RadioAreaGroup = ({ children, className, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
||||
const childrenWithProps = React.Children.map(children, (child) => {
|
||||
if (onChange && React.isValidElement(child)) {
|
||||
return React.cloneElement(child, {
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
return child;
|
||||
});
|
||||
return (
|
||||
<div className={className} {...passThroughProps}>
|
||||
{childrenWithProps}
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle [&:has(input:checked)]:border-emphasis relative flex items-start rounded-md border",
|
||||
className
|
||||
)}>
|
||||
<RadioGroupPrimitive.Item
|
||||
id={id}
|
||||
{...props}
|
||||
className={classNames(
|
||||
"hover:bg-subtle border-default focus:ring-emphasis absolute top-[0.9rem] left-3 mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border focus:ring-2",
|
||||
props.disabled && "opacity-60"
|
||||
)}>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
className={classNames(
|
||||
"after:bg-default dark:after:bg-inverted relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:content-['']",
|
||||
props.disabled ? "after:bg-muted" : "bg-black"
|
||||
)}
|
||||
/>
|
||||
</RadioGroupPrimitive.Item>
|
||||
<label htmlFor={id} className={classNames("text-default p-4 pt-3 pl-10", innerClassNames?.container)}>
|
||||
{children}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RadioAreaGroup.displayName = "RadioAreaGroup";
|
||||
RadioArea.displayName = "RadioArea";
|
||||
const RadioAreaGroup = ({
|
||||
children,
|
||||
className,
|
||||
onValueChange,
|
||||
...passThroughProps
|
||||
}: RadioGroupPrimitive.RadioGroupProps) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root className={className} onValueChange={onValueChange} {...passThroughProps}>
|
||||
{children}
|
||||
</RadioGroupPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = RadioArea;
|
||||
const Group = RadioAreaGroup;
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import React from "react";
|
||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { ChevronDown } from "@calcom/ui/components/icon";
|
||||
|
||||
import { RadioArea, RadioAreaGroup } from "./RadioAreaGroup";
|
||||
|
||||
interface OptionProps
|
||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;
|
||||
interface RadioAreaSelectProps<TFieldValues extends FieldValues>
|
||||
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "form"> {
|
||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
||||
onChange?: (value: string) => void;
|
||||
form: UseFormReturn<TFieldValues>;
|
||||
name: FieldPath<TFieldValues>;
|
||||
}
|
||||
|
||||
export const Select = function RadioAreaSelect<TFieldValues extends FieldValues>(
|
||||
props: RadioAreaSelectProps<TFieldValues>
|
||||
) {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
options,
|
||||
form,
|
||||
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
|
||||
placeholder = t("select"),
|
||||
} = props;
|
||||
|
||||
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
|
||||
options.find((option: OptionProps) => option.value === value)?.label;
|
||||
|
||||
return (
|
||||
<Collapsible className={classNames("w-full", props.className)}>
|
||||
<CollapsibleTrigger
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
"focus:ring-primary-500 border-default bg-default mb-1 block w-full cursor-pointer rounded-sm border border p-2 text-left shadow-sm sm:text-sm",
|
||||
disabled && "bg-emphasis cursor-default focus:ring-0 "
|
||||
)}>
|
||||
{getLabel(props.value) ?? placeholder}
|
||||
<ChevronDown className="text-subtle float-right h-5 w-5" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<RadioAreaGroup className="space-y-2 text-sm" onChange={props.onChange}>
|
||||
{options.map((option) => (
|
||||
<RadioArea
|
||||
{...form.register(props.name)}
|
||||
{...option}
|
||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}>
|
||||
<strong className="mb-1 block">{option.label}</strong>
|
||||
<p>{option.description}</p>
|
||||
</RadioArea>
|
||||
))}
|
||||
</RadioAreaGroup>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
|
@ -1,3 +1,2 @@
|
|||
export * as RadioGroup from "./RadioAreaGroup";
|
||||
export { default as Select } from "./Select";
|
||||
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";
|
||||
|
|
|
@ -11,12 +11,12 @@ const outputDir = path.join(__dirname, "test-results");
|
|||
// Dev Server on local can be slow to start up and process requests. So, keep timeouts really high on local, so that tests run reliably locally
|
||||
|
||||
// So, if not in CI, keep the timers high, if the test is stuck somewhere and there is unnecessary wait developer can see in browser that it's stuck
|
||||
const DEFAULT_NAVIGATION_TIMEOUT = process.env.CI ? 15000 : 50000;
|
||||
const DEFAULT_EXPECT_TIMEOUT = process.env.CI ? 15000 : 50000;
|
||||
const DEFAULT_NAVIGATION_TIMEOUT = process.env.CI ? 15000 : 120000;
|
||||
const DEFAULT_EXPECT_TIMEOUT = process.env.CI ? 15000 : 120000;
|
||||
|
||||
// Test Timeout can hit due to slow expect, slow navigation.
|
||||
// So, it should me much higher than sum of expect and navigation timeouts as there can be many async expects and navigations in a single test
|
||||
const DEFAULT_TEST_TIMEOUT = process.env.CI ? 60000 : 120000;
|
||||
const DEFAULT_TEST_TIMEOUT = process.env.CI ? 60000 : 240000;
|
||||
|
||||
const headless = !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS;
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user