fix: other reported issues (#11015)
* fix: weird margin top in avatar * fix: pending users are shown on booking page * fix: avatar and naming issues * fix: toast alignment and removing unneeded titles * missing changes from toast improvements * feat: empty state for teams without event types * Removing console.logpull/10992/head
parent
1bf68d50e0
commit
356117feaf
|
@ -9,7 +9,6 @@ import type { Options } from "react-select";
|
|||
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Label, Select } from "@calcom/ui";
|
||||
|
@ -18,13 +17,14 @@ interface IUserToValue {
|
|||
id: number | null;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const mapUserToValue = ({ id, name, username, email }: IUserToValue, pendingString: string) => ({
|
||||
const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({
|
||||
value: `${id || ""}`,
|
||||
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
|
||||
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||
avatar,
|
||||
email,
|
||||
});
|
||||
|
||||
|
|
|
@ -183,12 +183,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
created: true,
|
||||
}))
|
||||
);
|
||||
showToast(
|
||||
t("event_type_updated_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
showToast(t("event_type_updated_successfully"), "success");
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
|
|
|
@ -830,6 +830,25 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<EmptyScreen
|
||||
headline={t("team_no_event_types")}
|
||||
buttonRaw={
|
||||
<Button
|
||||
href={`?dialog=new&eventPage=${group.profile.slug}&teamId=${group.teamId}`}
|
||||
variant="button"
|
||||
className="mt-5">
|
||||
{t("create")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Main = ({
|
||||
status,
|
||||
errorMessage,
|
||||
|
@ -871,12 +890,16 @@ const Main = ({
|
|||
orgSlug={orgBranding?.slug}
|
||||
/>
|
||||
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
{group.eventTypes.length ? (
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
) : (
|
||||
<EmptyEventTypeList group={group} />
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
|
|
@ -64,7 +64,7 @@ test.describe("Booking with Seats", () => {
|
|||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
const eventTitle = "My 2-seated event";
|
||||
await createNewSeatedEventType(page, { eventTitle });
|
||||
await expect(page.locator(`text=${eventTitle} event type updated successfully`)).toBeVisible();
|
||||
await expect(page.locator(`text=Event type updated successfully`)).toBeVisible();
|
||||
});
|
||||
|
||||
test("Multiple Attendees can book a seated event time slot", async ({ users, page }) => {
|
||||
|
|
|
@ -678,8 +678,8 @@
|
|||
"new_event_type_btn": "New event type",
|
||||
"new_event_type_heading": "Create your first event type",
|
||||
"new_event_type_description": "Event types enable you to share links that show available times on your calendar and allow people to make bookings with you.",
|
||||
"event_type_created_successfully": "{{eventTypeTitle}} event type created successfully",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} event type updated successfully",
|
||||
"event_type_created_successfully": "Event type created successfully",
|
||||
"event_type_updated_successfully": "Event type updated successfully",
|
||||
"event_type_deleted_successfully": "Event type deleted successfully",
|
||||
"hours": "Hours",
|
||||
"people": "People",
|
||||
|
@ -2032,7 +2032,8 @@
|
|||
"mark_dns_configured": "Mark as DNS configured",
|
||||
"value": "Value",
|
||||
"your_organization_updated_sucessfully": "Your organization updated successfully",
|
||||
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
|
||||
"team_no_event_types": "This team has no event types",
|
||||
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
|
||||
"include_calendar_event": "Include calendar event",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
|
|
|
@ -161,7 +161,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
|||
|
||||
// Setting month make sure small calendar in fullscreen layouts also updates.
|
||||
// If selectedDate is null, prevents setting month to Invalid-Date
|
||||
if (selectedDate && newSelection.month() !== currentSelection.month() ) {
|
||||
if (selectedDate && newSelection.month() !== currentSelection.month()) {
|
||||
set({ month: newSelection.format("YYYY-MM") });
|
||||
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe
|
|||
(profile.name !== users[0].name && schedulingType === SchedulingType.COLLECTIVE);
|
||||
|
||||
const avatars: Avatar[] = shownUsers.map((user) => ({
|
||||
title: `${user.name}`,
|
||||
title: `${user.name || user.username}`,
|
||||
image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`,
|
||||
alt: user.name || undefined,
|
||||
href: `/${user.username}`,
|
||||
|
@ -54,7 +54,7 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe
|
|||
|
||||
// Add profile later since we don't want to force creating an avatar for this if it doesn't exist.
|
||||
avatars.unshift({
|
||||
title: `${profile.name}`,
|
||||
title: `${profile.name || profile.username}`,
|
||||
image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined,
|
||||
alt: profile.name || undefined,
|
||||
href: profile.username ? `${CAL_URL}/${profile.username}` : undefined,
|
||||
|
|
|
@ -65,7 +65,6 @@ export const AboutOrganizationForm = () => {
|
|||
<Avatar
|
||||
alt=""
|
||||
fallback={<Plus className="text-subtle h-6 w-6" />}
|
||||
asChild
|
||||
className="items-center"
|
||||
imageSrc={image}
|
||||
size="lg"
|
||||
|
|
|
@ -8,7 +8,6 @@ import type { ControlProps } from "react-select";
|
|||
import { components } from "react-select";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { AvailableTimes } from "@calcom/features/bookings";
|
||||
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { Props } from "react-select";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
@ -34,7 +36,7 @@ export const ChildrenEventTypeSelect = ({
|
|||
onChange: (value: readonly ChildrenEventType[]) => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const orgBranding = useOrgBranding();
|
||||
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||
|
||||
return (
|
||||
|
@ -61,7 +63,9 @@ export const ChildrenEventTypeSelect = ({
|
|||
<Avatar
|
||||
size="mdLg"
|
||||
className="overflow-visible"
|
||||
imageSrc={`${CAL_URL}/${children.owner.username}/avatar.png`}
|
||||
imageSrc={`${orgBranding ? getOrgFullDomain(orgBranding.slug) : CAL_URL}/${
|
||||
children.owner.username
|
||||
}/avatar.png`}
|
||||
alt={children.owner.name || children.owner.email || ""}
|
||||
/>
|
||||
<div className="flex w-full flex-row justify-between">
|
||||
|
|
|
@ -117,7 +117,7 @@ export default function CreateEventTypeDialog({
|
|||
const createMutation = trpc.viewer.eventTypes.create.useMutation({
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.replace("/event-types/" + eventType.id);
|
||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
showToast(t("event_type_created_successfully"), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
|
|
|
@ -66,7 +66,7 @@ const DuplicateDialog = () => {
|
|||
const duplicateMutation = trpc.viewer.eventTypes.duplicate.useMutation({
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.replace("/event-types/" + eventType.id);
|
||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
showToast(t("event_type_created_successfully"), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
|
|
|
@ -285,7 +285,7 @@ function getProfileFromEvent(event: Event) {
|
|||
function getUsersFromEvent(event: Event) {
|
||||
const { team, hosts, owner } = event;
|
||||
if (team) {
|
||||
return (hosts || []).map(mapHostsToUsers);
|
||||
return (hosts || []).filter((host) => host.user.username).map(mapHostsToUsers);
|
||||
}
|
||||
if (!owner) {
|
||||
return null;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { getLocationGroupedOptions } from "@calcom/app-store/server";
|
|||
import type { StripeData } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import { getOrgFullDomain } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseBookingLimit, parseDurationLimit, parseRecurringEvent } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
|
@ -113,6 +114,11 @@ export default async function getEventTypeById({
|
|||
name: true,
|
||||
slug: true,
|
||||
parentId: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
select: {
|
||||
role: true,
|
||||
|
@ -319,7 +325,9 @@ export default async function getEventTypeById({
|
|||
const eventTypeUsers: ((typeof eventType.users)[number] & { avatar: string })[] = eventType.users.map(
|
||||
(user) => ({
|
||||
...user,
|
||||
avatar: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
avatar: `${eventType.team?.parent?.slug ? getOrgFullDomain(eventType.team?.parent?.slug) : CAL_URL}/${
|
||||
user.username
|
||||
}/avatar.png`,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -365,7 +373,11 @@ export default async function getEventTypeById({
|
|||
.map((member) => {
|
||||
const user: typeof member.user & { avatar: string } = {
|
||||
...member.user,
|
||||
avatar: `${CAL_URL}/${member.user.username}/avatar.png`,
|
||||
avatar: `${
|
||||
eventTypeObject.team?.parent?.slug
|
||||
? getOrgFullDomain(eventTypeObject.team?.parent?.slug)
|
||||
: CAL_URL
|
||||
}/${member.user.username}/avatar.png`,
|
||||
};
|
||||
return {
|
||||
...user,
|
||||
|
|
|
@ -633,4 +633,3 @@ export const ZVerifyCodeInputSchema = z.object({
|
|||
export type ZVerifyCodeInputSchema = z.infer<typeof ZVerifyCodeInputSchema>;
|
||||
|
||||
export const coerceToDate = z.coerce.date();
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { getOrgFullDomain } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
|
@ -24,7 +25,9 @@ export const meHandler = async ({ ctx }: MeOptions) => {
|
|||
locale: user.locale,
|
||||
timeFormat: user.timeFormat,
|
||||
timeZone: user.timeZone,
|
||||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
avatar: `${user.organization?.slug ? getOrgFullDomain(user.organization.slug) : WEBAPP_URL}/${
|
||||
user.username
|
||||
}/avatar.png`,
|
||||
createdDate: user.createdDate,
|
||||
trialEndsAt: user.trialEndsAt,
|
||||
defaultScheduleId: user.defaultScheduleId,
|
||||
|
|
|
@ -165,6 +165,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
|||
|
||||
type EventTypeGroup = {
|
||||
teamId?: number | null;
|
||||
parentId?: number | null;
|
||||
membershipRole?: MembershipRole | null;
|
||||
profile: {
|
||||
slug: (typeof user)["username"];
|
||||
|
@ -226,6 +227,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
|||
)?.membershipRole;
|
||||
return {
|
||||
teamId: membership.team.id,
|
||||
parentId: membership.team.parentId,
|
||||
membershipRole:
|
||||
orgMembership && compareMembership(orgMembership, membership.role)
|
||||
? orgMembership
|
||||
|
@ -265,7 +267,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
|||
const bookerUrl = await getBookerUrl(user);
|
||||
return {
|
||||
// don't display event teams without event types,
|
||||
eventTypeGroups: eventTypeGroups.filter((groupBy) => !!groupBy.eventTypes?.length),
|
||||
eventTypeGroups: eventTypeGroups.filter((groupBy) => groupBy.parentId || !!groupBy.eventTypes?.length),
|
||||
// so we can show a dropdown when the user has teams
|
||||
profiles: eventTypeGroups.map((group) => ({
|
||||
...group.profile,
|
||||
|
|
|
@ -40,7 +40,7 @@ export function Avatar(props: AvatarProps) {
|
|||
let avatar = (
|
||||
<AvatarPrimitive.Root
|
||||
className={classNames(
|
||||
"bg-emphasis item-center relative inline-flex aspect-square justify-center overflow-hidden rounded-full",
|
||||
"bg-emphasis item-center relative aspect-square justify-center overflow-hidden rounded-full",
|
||||
props.className,
|
||||
sizesPropsBySize[size]
|
||||
)}>
|
||||
|
@ -50,7 +50,10 @@ export function Avatar(props: AvatarProps) {
|
|||
alt={alt}
|
||||
className={classNames("aspect-square rounded-full", sizesPropsBySize[size])}
|
||||
/>
|
||||
<AvatarPrimitive.Fallback delayMs={600} asChild={props.asChild} className="flex items-center">
|
||||
<AvatarPrimitive.Fallback
|
||||
delayMs={600}
|
||||
asChild={props.asChild}
|
||||
className="flex h-full items-center justify-center">
|
||||
<>
|
||||
{props.fallback ? (
|
||||
props.fallback
|
||||
|
|
|
@ -22,7 +22,7 @@ export function EmptyScreen({
|
|||
Icon?: SVGComponent | IconType;
|
||||
avatar?: React.ReactElement;
|
||||
headline: string | React.ReactElement;
|
||||
description: string | React.ReactElement;
|
||||
description?: string | React.ReactElement;
|
||||
buttonText?: string;
|
||||
buttonOnClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
buttonRaw?: ReactNode; // Used incase you want to provide your own button.
|
||||
|
@ -49,9 +49,11 @@ export function EmptyScreen({
|
|||
)}
|
||||
<div className="flex max-w-[420px] flex-col items-center">
|
||||
<h2 className="text-semibold font-cal text-emphasis mt-6 text-center text-xl">{headline}</h2>
|
||||
<div className="text-default mb-8 mt-3 text-center text-sm font-normal leading-6">
|
||||
{description}
|
||||
</div>
|
||||
{description && (
|
||||
<div className="text-default mb-8 mt-3 text-center text-sm font-normal leading-6">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{buttonOnClick && buttonText && <Button onClick={(e) => buttonOnClick(e)}>{buttonText}</Button>}
|
||||
{buttonRaw}
|
||||
</div>
|
||||
|
|
|
@ -13,56 +13,64 @@ type IToast = {
|
|||
export const SuccessToast = ({ message, toastVisible, onClose, toastId }: IToast) => (
|
||||
<button
|
||||
className={classNames(
|
||||
"data-testid-toast-success bg-brand-default text-inverted mb-2 flex h-auto items-center space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
"data-testid-toast-success bg-brand-default text-inverted mb-2 flex h-auto space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
toastVisible && "animate-fade-in-up cursor-pointer"
|
||||
)}
|
||||
onClick={() => onClose(toastId)}>
|
||||
<span>
|
||||
<span className="mt-0.5">
|
||||
<Check className="h-4 w-4" />
|
||||
</span>
|
||||
<p data-testid="toast-success">{message}</p>
|
||||
<p data-testid="toast-success" className="text-left">
|
||||
{message}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
|
||||
export const ErrorToast = ({ message, toastVisible, onClose, toastId }: IToast) => (
|
||||
<button
|
||||
className={classNames(
|
||||
"animate-fade-in-up bg-error text-error mb-2 flex h-auto items-center space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
"animate-fade-in-up bg-error text-error mb-2 flex h-auto space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
toastVisible && "animate-fade-in-up cursor-pointer"
|
||||
)}
|
||||
onClick={() => onClose(toastId)}>
|
||||
<span>
|
||||
<span className="mt-0.5">
|
||||
<Info className="h-4 w-4" />
|
||||
</span>
|
||||
<p data-testid="toast-error">{message}</p>
|
||||
<p data-testid="toast-error" className="text-left">
|
||||
{message}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
|
||||
export const WarningToast = ({ message, toastVisible, onClose, toastId }: IToast) => (
|
||||
<button
|
||||
className={classNames(
|
||||
"animate-fade-in-up bg-brand-default text-brand mb-2 flex h-auto items-center space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
"animate-fade-in-up bg-brand-default text-brand mb-2 flex h-auto space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
toastVisible && "animate-fade-in-up cursor-pointer"
|
||||
)}
|
||||
onClick={() => onClose(toastId)}>
|
||||
<span>
|
||||
<span className="mt-0.5">
|
||||
<Info className="h-4 w-4" />
|
||||
</span>
|
||||
<p data-testid="toast-warning">{message}</p>
|
||||
<p data-testid="toast-warning" className="text-left">
|
||||
{message}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
|
||||
export const DefaultToast = ({ message, toastVisible, onClose, toastId }: IToast) => (
|
||||
<button
|
||||
className={classNames(
|
||||
"animate-fade-in-up bg-brand-default text-inverted mb-2 flex h-auto items-center space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
"animate-fade-in-up bg-brand-default text-inverted mb-2 flex h-auto space-x-2 rounded-md p-3 text-sm font-semibold shadow-md rtl:space-x-reverse md:max-w-sm",
|
||||
toastVisible && "animate-fade-in-up cursor-pointer"
|
||||
)}
|
||||
onClick={() => onClose(toastId)}>
|
||||
<span>
|
||||
<span className="mt-0.5">
|
||||
<Check className="h-4 w-4" />
|
||||
</span>
|
||||
<p data-testid="toast-default">{message}</p>
|
||||
<p data-testid="toast-default" className="text-left">
|
||||
{message}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in New Issue