Compare commits

...

5 Commits

Author SHA1 Message Date
Udit Takkar efdca50ddc
fix: upgrade btn bug (#9104)
* fix: upgrade btn bug

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: use border-subtle

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-26 13:14:07 +00:00
GitStart-Cal.com 711959048b
fix: Change Detroit to New York as a default for EST time zone (#8218)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-05-26 12:19:53 +00:00
jemiluv8 e7ddcf23a3
fix: Route Builder rules should be case insensitive (#9040)
* override jsonLogic operators on string operands to allow for case insensitive comparisons.

Affected Operators: "==", "===", "!=", "!==", "in"

* disable no-explicit-any on jsonLogicOverrides file since most of the code there will be from jsonLogic and may not meet our coding style.

Majority of overrides require us to copy over functions and their signatures from jsonLogic and then modify their implementation.

The signature of functions implementing most operators take the operands typed as "any", which is intended, but doesn't adhere to our coding style. Hence the need to override the eslint rule

* run linter to fix issues

* Fix bug in in operator when second arg is an array

* remove redundant indexOf check on overriden jsonLogic "in" operator.

Note: this deviates from the original implementation in the jsonLogic library because our current useage ensures that the second operand is always a string or string[] and will therefore always have .index function. Whenever our invariants change in the future, make sure to modify this implementation to prevent any unexpected

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-05-26 12:16:39 +00:00
Adithya Krishna 02b9505882
feat: Auto check PR titles if they follow conventional commits spec (#9109)
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-26 12:04:58 +00:00
Udit Takkar a6065a4dbd
refactor: radio area group (#9113)
* refactor: radio area group

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: remove select

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: e2e test

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-26 11:55:08 +00:00
14 changed files with 151 additions and 166 deletions

View File

@ -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 }}

View File

@ -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]");
});

View File

@ -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"`);
});

View File

@ -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;

View File

@ -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,

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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"

View File

@ -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}`;
};

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -1,3 +1,2 @@
export * as RadioGroup from "./RadioAreaGroup";
export { default as Select } from "./Select";
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";

View File

@ -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;