fix: include orgs slug in getUserByUsername just like [user]/[type] page (#10723)
Co-authored-by: Omar López <zomars@me.com>fix/storybook-builds
parent
d6740503be
commit
1eef5a56c3
|
@ -269,12 +269,14 @@ describe("getSchedule", () => {
|
|||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
},
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
|
@ -353,13 +355,15 @@ describe("getSchedule", () => {
|
|||
|
||||
// Day Plus 2 is completely free - It only has non accepted bookings
|
||||
const scheduleOnCompletelyFreeDay = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
// EventTypeSlug doesn't matter for non-dynamic events
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
// EventTypeSlug doesn't matter for non-dynamic events
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
// getSchedule returns timeslots in GMT
|
||||
|
@ -384,12 +388,14 @@ describe("getSchedule", () => {
|
|||
|
||||
// Day plus 3
|
||||
const scheduleForDayWithOneBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
||||
|
@ -448,12 +454,14 @@ describe("getSchedule", () => {
|
|||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const scheduleForEventWith30Length = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
||||
|
@ -482,12 +490,14 @@ describe("getSchedule", () => {
|
|||
);
|
||||
|
||||
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
// `slotInterval` takes precedence over `length`
|
||||
// 4:30 is utc so it is 10:00 in IST
|
||||
|
@ -545,12 +555,14 @@ describe("getSchedule", () => {
|
|||
const { dateString: todayDateString } = getDate();
|
||||
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
||||
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -564,12 +576,14 @@ describe("getSchedule", () => {
|
|||
);
|
||||
|
||||
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
||||
[
|
||||
|
@ -627,12 +641,14 @@ describe("getSchedule", () => {
|
|||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
||||
|
@ -700,12 +716,14 @@ describe("getSchedule", () => {
|
|||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForEventOnADayWithCalBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
||||
|
@ -757,12 +775,14 @@ describe("getSchedule", () => {
|
|||
createBookingScenario(scenarioData);
|
||||
|
||||
const schedule = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
|
@ -820,12 +840,14 @@ describe("getSchedule", () => {
|
|||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForEventOnADayWithDateOverride = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
||||
|
@ -905,12 +927,14 @@ describe("getSchedule", () => {
|
|||
// Requesting this user's availability for their
|
||||
// individual Event Type
|
||||
const thisUserAvailability = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(thisUserAvailability).toHaveTimeSlots(
|
||||
|
@ -1002,12 +1026,14 @@ describe("getSchedule", () => {
|
|||
});
|
||||
|
||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||
|
@ -1030,12 +1056,14 @@ describe("getSchedule", () => {
|
|||
);
|
||||
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
|
@ -1137,12 +1165,14 @@ describe("getSchedule", () => {
|
|||
hosts: [],
|
||||
});
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
||||
|
@ -1163,12 +1193,14 @@ describe("getSchedule", () => {
|
|||
);
|
||||
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import type { IncomingMessage } from "http";
|
||||
|
||||
import type { TGetScheduleInputSchema } from "./getSchedule.schema";
|
||||
import { getAvailableSlots } from "./util";
|
||||
|
||||
type GetScheduleOptions = {
|
||||
ctx: Record<string, unknown>;
|
||||
export type GetScheduleOptions = {
|
||||
ctx?: ContextForGetSchedule;
|
||||
input: TGetScheduleInputSchema;
|
||||
};
|
||||
|
||||
export const getScheduleHandler = async ({ input }: GetScheduleOptions) => {
|
||||
return await getAvailableSlots(input);
|
||||
interface ContextForGetSchedule extends Record<string, unknown> {
|
||||
req?: (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }) | undefined;
|
||||
}
|
||||
|
||||
export const getScheduleHandler = async ({ ctx, input }: GetScheduleOptions) => {
|
||||
return await getAvailableSlots({ ctx, input });
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { CurrentSeats } from "@calcom/core/getUserAvailability";
|
|||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds";
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
@ -19,6 +20,7 @@ import type { EventBusyDate } from "@calcom/types/Calendar";
|
|||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { GetScheduleOptions } from "./getSchedule.handler";
|
||||
import type { TGetScheduleInputSchema } from "./getSchedule.schema";
|
||||
|
||||
export const checkIfIsAvailable = ({
|
||||
|
@ -74,19 +76,27 @@ async function getEventTypeId({
|
|||
slug,
|
||||
eventTypeSlug,
|
||||
isTeamEvent,
|
||||
organizationDetails,
|
||||
}: {
|
||||
slug?: string;
|
||||
eventTypeSlug?: string;
|
||||
isTeamEvent: boolean;
|
||||
organizationDetails?: { currentOrgDomain: string | null; isValidOrgDomain: boolean };
|
||||
}) {
|
||||
if (!eventTypeSlug || !slug) return null;
|
||||
|
||||
let teamId;
|
||||
let userId;
|
||||
if (isTeamEvent) {
|
||||
teamId = await getTeamIdFromSlug(slug);
|
||||
teamId = await getTeamIdFromSlug(
|
||||
slug,
|
||||
organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false }
|
||||
);
|
||||
} else {
|
||||
userId = await getUserIdFromUsername(slug);
|
||||
userId = await getUserIdFromUsername(
|
||||
slug,
|
||||
organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false }
|
||||
);
|
||||
}
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
|
@ -104,7 +114,10 @@ async function getEventTypeId({
|
|||
return eventType?.id;
|
||||
}
|
||||
|
||||
export async function getEventType(input: TGetScheduleInputSchema) {
|
||||
export async function getEventType(
|
||||
input: TGetScheduleInputSchema,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const { eventTypeSlug, usernameList, isTeamEvent } = input;
|
||||
const eventTypeId =
|
||||
input.eventTypeId ||
|
||||
|
@ -113,6 +126,7 @@ export async function getEventType(input: TGetScheduleInputSchema) {
|
|||
slug: usernameList?.[0],
|
||||
eventTypeSlug: eventTypeSlug,
|
||||
isTeamEvent,
|
||||
organizationDetails,
|
||||
}));
|
||||
|
||||
if (!eventTypeId) {
|
||||
|
@ -223,12 +237,17 @@ export async function getDynamicEventType(input: TGetScheduleInputSchema) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getRegularOrDynamicEventType(input: TGetScheduleInputSchema) {
|
||||
export function getRegularOrDynamicEventType(
|
||||
input: TGetScheduleInputSchema,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const isDynamicBooking = input.usernameList && input.usernameList.length > 1;
|
||||
return isDynamicBooking ? getDynamicEventType(input) : getEventType(input);
|
||||
return isDynamicBooking ? getDynamicEventType(input) : getEventType(input, organizationDetails);
|
||||
}
|
||||
|
||||
export async function getAvailableSlots(input: TGetScheduleInputSchema) {
|
||||
export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
|
||||
const orgDetails = orgDomainConfig(ctx?.req?.headers.host ?? "");
|
||||
|
||||
if (input.debug === true) {
|
||||
logger.setSettings({ minLevel: "debug" });
|
||||
}
|
||||
|
@ -236,7 +255,7 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
|
|||
logger.setSettings({ minLevel: "silly" });
|
||||
}
|
||||
const startPrismaEventTypeGet = performance.now();
|
||||
const eventType = await getRegularOrDynamicEventType(input);
|
||||
const eventType = await getRegularOrDynamicEventType(input, orgDetails);
|
||||
const endPrismaEventTypeGet = performance.now();
|
||||
logger.debug(
|
||||
`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms for event:${
|
||||
|
@ -492,10 +511,15 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
|
|||
};
|
||||
}
|
||||
|
||||
async function getUserIdFromUsername(username: string) {
|
||||
async function getUserIdFromUsername(
|
||||
username: string,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -504,10 +528,15 @@ async function getUserIdFromUsername(username: string) {
|
|||
return user?.id;
|
||||
}
|
||||
|
||||
async function getTeamIdFromSlug(slug: string) {
|
||||
async function getTeamIdFromSlug(
|
||||
slug: string,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
|
Loading…
Reference in New Issue