Manually reorder event types (#1142)
* Add event type reordering * Add migration for position field * hack on a hack * can edit * fix ordering * Remove console.log Co-authored-by: Alex Johansson <alexander@n1s.se> Co-authored-by: KATT <alexander@n1s.se>
This commit is contained in:
parent
6fa980f801
commit
6b171a6f87
6 changed files with 184 additions and 7 deletions
|
@ -124,6 +124,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
position: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
// TODO: replace headlessui with radix-ui
|
// TODO: replace headlessui with radix-ui
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { UsersIcon } from "@heroicons/react/solid";
|
import {
|
||||||
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
|
DotsHorizontalIcon,
|
||||||
import { DotsHorizontalIcon, ExternalLinkIcon, LinkIcon } from "@heroicons/react/solid";
|
ExternalLinkIcon,
|
||||||
|
LinkIcon,
|
||||||
|
ArrowDownIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from "@heroicons/react/solid";
|
||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { Fragment, useRef } from "react";
|
import React, { Fragment, useRef, useState, useEffect } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
@ -72,10 +79,40 @@ interface EventTypeListProps {
|
||||||
}
|
}
|
||||||
const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.Element => {
|
const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.Element => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const mutation = trpc.useMutation("viewer.eventTypeOrder", {
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(err.message);
|
||||||
|
},
|
||||||
|
async onSettled() {
|
||||||
|
await utils.cancelQuery(["viewer.eventTypes"]);
|
||||||
|
await utils.invalidateQueries(["viewer.eventTypes"]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [sortableTypes, setSortableTypes] = useState(types);
|
||||||
|
useEffect(() => {
|
||||||
|
setSortableTypes(types);
|
||||||
|
}, [types]);
|
||||||
|
function moveEventType(index: number, increment: 1 | -1) {
|
||||||
|
const newList = [...sortableTypes];
|
||||||
|
|
||||||
|
const type = sortableTypes[index];
|
||||||
|
const tmp = sortableTypes[index + increment];
|
||||||
|
if (tmp) {
|
||||||
|
newList[index] = tmp;
|
||||||
|
newList[index + increment] = type;
|
||||||
|
}
|
||||||
|
setSortableTypes(newList);
|
||||||
|
mutation.mutate({
|
||||||
|
ids: newList.map((type) => type.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-16 -mx-4 overflow-hidden bg-white border border-gray-200 rounded-sm sm:mx-0">
|
<div className="mb-16 -mx-4 overflow-hidden bg-white border border-gray-200 rounded-sm sm:mx-0">
|
||||||
<ul className="divide-y divide-neutral-200" data-testid="event-types">
|
<ul className="divide-y divide-neutral-200" data-testid="event-types">
|
||||||
{types.map((type) => (
|
{sortableTypes.map((type, index) => (
|
||||||
<li
|
<li
|
||||||
key={type.id}
|
key={type.id}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -87,7 +124,17 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
||||||
"hover:bg-neutral-50 flex justify-between items-center ",
|
"hover:bg-neutral-50 flex justify-between items-center ",
|
||||||
type.$disabled && "pointer-events-none"
|
type.$disabled && "pointer-events-none"
|
||||||
)}>
|
)}>
|
||||||
<div className="flex items-center justify-between w-full px-4 py-4 sm:px-6 hover:bg-neutral-50">
|
<div className="group flex items-center justify-between w-full px-4 py-4 sm:px-6 hover:bg-neutral-50">
|
||||||
|
<button
|
||||||
|
className="absolute mb-8 left-1/2 -ml-4 sm:ml-0 sm:left-[19px] border hover:border-transparent text-gray-400 transition-all hover:text-black hover:shadow group-hover:scale-100 scale-0 w-7 h-7 p-1 invisible group-hover:visible bg-white rounded-full"
|
||||||
|
onClick={() => moveEventType(index, -1)}>
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="absolute mt-8 left-1/2 -ml-4 sm:ml-0 sm:left-[19px] border hover:border-transparent text-gray-400 transition-all hover:text-black hover:shadow group-hover:scale-100 scale-0 w-7 h-7 p-1 invisible group-hover:visible bg-white rounded-full"
|
||||||
|
onClick={() => moveEventType(index, 1)}>
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</button>
|
||||||
<Link href={"/event-types/" + type.id}>
|
<Link href={"/event-types/" + type.id}>
|
||||||
<a
|
<a
|
||||||
className="flex-grow text-sm truncate"
|
className="flex-grow text-sm truncate"
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EventType" ADD COLUMN "position" INTEGER NOT NULL DEFAULT 0;
|
|
@ -21,6 +21,7 @@ model EventType {
|
||||||
title String
|
title String
|
||||||
slug String
|
slug String
|
||||||
description String?
|
description String?
|
||||||
|
position Int @default(0)
|
||||||
locations Json?
|
locations Json?
|
||||||
length Int
|
length Int
|
||||||
hidden Boolean @default(false)
|
hidden Boolean @default(false)
|
||||||
|
|
|
@ -517,6 +517,7 @@
|
||||||
"confirm_delete_event_type": "Yes, delete event type",
|
"confirm_delete_event_type": "Yes, delete event type",
|
||||||
"integrations": "Integrations",
|
"integrations": "Integrations",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"event_type_moved_successfully": "Event type has been moved successfully",
|
||||||
"next_step": "Skip step",
|
"next_step": "Skip step",
|
||||||
"prev_step": "Prev step",
|
"prev_step": "Prev step",
|
||||||
"installed": "Installed",
|
"installed": "Installed",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { BookingStatus, Prisma } from "@prisma/client";
|
import { BookingStatus, Prisma } from "@prisma/client";
|
||||||
|
import _ from "lodash";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername";
|
import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername";
|
||||||
|
@ -85,6 +86,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
hidden: true,
|
hidden: true,
|
||||||
price: true,
|
price: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
|
position: true,
|
||||||
users: {
|
users: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -126,6 +128,14 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
},
|
},
|
||||||
eventTypes: {
|
eventTypes: {
|
||||||
select: eventTypeSelect,
|
select: eventTypeSelect,
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
position: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -136,6 +146,14 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
team: null,
|
team: null,
|
||||||
},
|
},
|
||||||
select: eventTypeSelect,
|
select: eventTypeSelect,
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
position: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -150,6 +168,14 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
},
|
},
|
||||||
select: eventTypeSelect,
|
select: eventTypeSelect,
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
position: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
type EventTypeGroup = {
|
type EventTypeGroup = {
|
||||||
|
@ -184,7 +210,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
name: user.name,
|
name: user.name,
|
||||||
image: user.avatar,
|
image: user.avatar,
|
||||||
},
|
},
|
||||||
eventTypes: mergedEventTypes,
|
eventTypes: _.orderBy(mergedEventTypes, ["position", "id"], ["desc", "asc"]),
|
||||||
metadata: {
|
metadata: {
|
||||||
membershipCount: 1,
|
membershipCount: 1,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
@ -449,6 +475,98 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.mutation("eventTypeOrder", {
|
||||||
|
input: z.object({
|
||||||
|
ids: z.array(z.number()),
|
||||||
|
}),
|
||||||
|
async resolve({ input, ctx }) {
|
||||||
|
const { prisma, user } = ctx;
|
||||||
|
const allEventTypes = await ctx.prisma.eventType.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: input.ids,
|
||||||
|
},
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
users: {
|
||||||
|
some: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const allEventTypeIds = new Set(allEventTypes.map((type) => type.id));
|
||||||
|
if (input.ids.some((id) => !allEventTypeIds.has(id))) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
_.reverse(input.ids).map((id, position) => {
|
||||||
|
return prisma.eventType.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("eventTypePosition", {
|
||||||
|
input: z.object({
|
||||||
|
eventType: z.number(),
|
||||||
|
action: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ input, ctx }) {
|
||||||
|
// This mutation is for the user to be able to order their event types by incrementing or decrementing the position number
|
||||||
|
const { prisma } = ctx;
|
||||||
|
if (input.eventType && input.action == "increment") {
|
||||||
|
await prisma.eventType.update({
|
||||||
|
where: {
|
||||||
|
id: input.eventType,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
position: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.eventType && input.action == "decrement") {
|
||||||
|
await prisma.eventType.update({
|
||||||
|
where: {
|
||||||
|
id: input.eventType,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
position: {
|
||||||
|
decrement: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const viewerRouter = createRouter()
|
export const viewerRouter = createRouter()
|
||||||
|
|
Loading…
Reference in a new issue