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.log
pull/10992/head
Leo Giovanetti 2023-08-30 17:42:34 -03:00 committed by GitHub
parent 1bf68d50e0
commit 356117feaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 102 additions and 53 deletions

View File

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

View File

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

View File

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

View File

@ -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 }) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -633,4 +633,3 @@ export const ZVerifyCodeInputSchema = z.object({
export type ZVerifyCodeInputSchema = z.infer<typeof ZVerifyCodeInputSchema>;
export const coerceToDate = z.coerce.date();

View File

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

View File

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

View File

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

View File

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

View File

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