feat: schedule list item action menu (#230)
* feat: action menu for schedule list item * feat: schedule action menu functionality * feat: dialog provider popups for delete * feat: duplicate schedule satiesfies type * refactor: change non-null assertion to early return for rename schedule * refactor: move schedule list item dialog providers to util file * style: run prettier * chore: inline object with satisfies operator * fix: border issues * style: change popups to match figma * fix: update import for schedule list item dialog providers * style: change dropdown text style to match figma * fix: add back dialog context * style: rounded edges when hovering over action + soften border color * chore: cleanup and improve styling * fix: dialog in popupmain --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> Co-authored-by: Razboy20 <razboy20@gmail.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
import { generateRandomId } from '@shared/util/random';
|
import { generateRandomId } from '@shared/util/random';
|
||||||
|
|
||||||
|
import handleDuplicate from './handleDuplicate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new schedule with the given name
|
* Creates a new schedule with the given name
|
||||||
* @param scheduleName the name of the schedule to create
|
* @param scheduleName the name of the schedule to create
|
||||||
@@ -8,13 +10,13 @@ import { generateRandomId } from '@shared/util/random';
|
|||||||
*/
|
*/
|
||||||
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
|
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
// if (schedules.find(schedule => schedule.name === scheduleName)) {
|
|
||||||
// return `Schedule ${scheduleName} already exists`;
|
// Duplicate schedule found, we need to append a number to the end of the schedule name
|
||||||
// }
|
const updatedName = await handleDuplicate(scheduleName);
|
||||||
|
|
||||||
schedules.push({
|
schedules.push({
|
||||||
id: generateRandomId(),
|
id: generateRandomId(),
|
||||||
name: scheduleName,
|
name: updatedName,
|
||||||
courses: [],
|
courses: [],
|
||||||
hours: 0,
|
hours: 0,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ export default async function deleteSchedule(scheduleId: string): Promise<string
|
|||||||
throw new Error(`Schedule ${scheduleId} does not exist`);
|
throw new Error(`Schedule ${scheduleId} does not exist`);
|
||||||
}
|
}
|
||||||
if (scheduleIndex === activeIndex) {
|
if (scheduleIndex === activeIndex) {
|
||||||
throw new Error('You cannot delete your active schedule! Please switch to another schedule before deleting.');
|
throw new Error(`Cannot delete active schedule`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleIndex < activeIndex) {
|
||||||
|
await UserScheduleStore.set('activeIndex', activeIndex - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
schedules.splice(scheduleIndex, 1);
|
schedules.splice(scheduleIndex, 1);
|
||||||
|
|||||||
31
src/pages/background/lib/duplicateSchedule.ts
Normal file
31
src/pages/background/lib/duplicateSchedule.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
|
import { generateRandomId } from '@shared/util/random';
|
||||||
|
|
||||||
|
import handleDuplicate from './handleDuplicate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new schedule with the given name
|
||||||
|
* @param scheduleName the name of the schedule to create
|
||||||
|
* @returns undefined if successful, otherwise an error message
|
||||||
|
*/
|
||||||
|
export default async function duplicateSchedule(scheduleId: string): Promise<string | undefined> {
|
||||||
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
|
const schedule = schedules.find(schedule => schedule.id === scheduleId);
|
||||||
|
|
||||||
|
if (schedule === undefined) {
|
||||||
|
throw new Error(`Schedule ${scheduleId} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedName = await handleDuplicate(schedule.name);
|
||||||
|
|
||||||
|
schedules.push({
|
||||||
|
id: generateRandomId(),
|
||||||
|
name: updatedName,
|
||||||
|
courses: JSON.parse(JSON.stringify(schedule.courses)),
|
||||||
|
hours: schedule.hours,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
} satisfies typeof schedule);
|
||||||
|
|
||||||
|
await UserScheduleStore.set('schedules', schedules);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
37
src/pages/background/lib/handleDuplicate.ts
Normal file
37
src/pages/background/lib/handleDuplicate.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates a new schedule with the given name.
|
||||||
|
* Assumes that each schedule has a unique name.
|
||||||
|
* @param scheduleName the name of the schedule to handle duplication for
|
||||||
|
* @param schedules the list of UserSchedules to find existing names
|
||||||
|
* @returns the new name for the schedule, of the form `{baseName}({index})`
|
||||||
|
*/
|
||||||
|
export default async function handleDuplicate(scheduleName: string): Promise<string> {
|
||||||
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
|
|
||||||
|
// No point in checking for duplicates if the name is unique
|
||||||
|
if (schedules.find(schedule => schedule.name === scheduleName) === undefined) {
|
||||||
|
return scheduleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex for matching `{baseName}({index})`, where match[1] = baseName, match[2] = (index)
|
||||||
|
const regex = /^(.+?)(\(\d+\))?$/;
|
||||||
|
|
||||||
|
// Extract base name and existing index
|
||||||
|
const match = scheduleName.match(regex);
|
||||||
|
const baseName = match && match[1] ? match[1] : scheduleName;
|
||||||
|
|
||||||
|
// Extract number from parentheses and increment
|
||||||
|
let index = match && match[2] ? parseInt(match[2].slice(1, -1), 10) + 1 : 1;
|
||||||
|
|
||||||
|
let newName: string;
|
||||||
|
|
||||||
|
// Increment until an unused index is found
|
||||||
|
do {
|
||||||
|
newName = `${baseName} (${index++})`;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
|
} while (schedules.find(schedule => schedule.name === newName));
|
||||||
|
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
@@ -1,21 +1,41 @@
|
|||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
|
|
||||||
|
import handleDuplicate from './handleDuplicate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames a schedule with the specified name to a new name.
|
* Renames a schedule with the specified name to a new name.
|
||||||
* @param scheduleId - The id of the schedule to be renamed.
|
* @param scheduleId - The id of the schedule to be renamed.
|
||||||
* @param newName - The new name for the schedule.
|
* @param newName - The new name for the schedule.
|
||||||
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is renamed successfully.
|
* @returns A promise that resolves to the new name if successful, otherwise undefined.
|
||||||
*/
|
*/
|
||||||
export default async function renameSchedule(scheduleId: string, newName: string): Promise<string | undefined> {
|
export default async function renameSchedule(scheduleId: string, newName: string): Promise<string | undefined> {
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
|
|
||||||
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
||||||
if (scheduleIndex === -1) {
|
if (scheduleIndex === -1) {
|
||||||
return `Schedule ${scheduleId} does not exist`;
|
|
||||||
}
|
|
||||||
|
|
||||||
schedules[scheduleIndex]!.name = newName;
|
|
||||||
// schedules[scheduleIndex].updatedAt = Date.now();
|
|
||||||
|
|
||||||
await UserScheduleStore.set('schedules', schedules);
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const schedule = schedules[scheduleIndex];
|
||||||
|
if (schedule === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if old name is of the form `{baseName}{index}` and newName === baseName, do nothing.
|
||||||
|
const oldName = schedule.name;
|
||||||
|
const regex = /^(.+?)(\(\d+\))?$/;
|
||||||
|
const match = oldName?.match(regex);
|
||||||
|
const baseName = match?.[1] ?? '';
|
||||||
|
const baseNameOfNewName = newName.match(regex)?.[1];
|
||||||
|
|
||||||
|
if (baseName === baseNameOfNewName) {
|
||||||
|
return oldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedName = await handleDuplicate(newName);
|
||||||
|
|
||||||
|
schedule.name = updatedName;
|
||||||
|
schedule.updatedAt = Date.now();
|
||||||
|
|
||||||
|
await UserScheduleStore.set('schedules', schedules);
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import RefreshIcon from '~icons/material-symbols/refresh';
|
|||||||
import SettingsIcon from '~icons/material-symbols/settings';
|
import SettingsIcon from '~icons/material-symbols/settings';
|
||||||
|
|
||||||
import CourseStatus from './common/CourseStatus';
|
import CourseStatus from './common/CourseStatus';
|
||||||
|
import DialogProvider from './common/DialogProvider/DialogProvider';
|
||||||
import { SmallLogo } from './common/LogoIcon';
|
import { SmallLogo } from './common/LogoIcon';
|
||||||
import PopupCourseBlock from './common/PopupCourseBlock';
|
import PopupCourseBlock from './common/PopupCourseBlock';
|
||||||
import ScheduleDropdown from './common/ScheduleDropdown';
|
import ScheduleDropdown from './common/ScheduleDropdown';
|
||||||
@@ -50,12 +51,16 @@ export default function PopupMain(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionRoot>
|
<ExtensionRoot>
|
||||||
|
<DialogProvider>
|
||||||
<div className='h-screen max-h-full flex flex-col bg-white'>
|
<div className='h-screen max-h-full flex flex-col bg-white'>
|
||||||
<div className='p-5 py-3.5'>
|
<div className='p-5 py-3.5'>
|
||||||
<div className='flex items-center justify-between bg-white'>
|
<div className='flex items-center justify-between bg-white'>
|
||||||
<SmallLogo />
|
<SmallLogo />
|
||||||
<div className='flex items-center gap-2.5'>
|
<div className='flex items-center gap-2.5'>
|
||||||
<button className='bg-ut-burntorange px-2 py-1.25 btn' onClick={handleCalendarOpenOnClick}>
|
<button
|
||||||
|
className='bg-ut-burntorange px-2 py-1.25 btn'
|
||||||
|
onClick={handleCalendarOpenOnClick}
|
||||||
|
>
|
||||||
<CalendarIcon className='size-6 text-white' />
|
<CalendarIcon className='size-6 text-white' />
|
||||||
</button>
|
</button>
|
||||||
<button className='bg-transparent px-2 py-1.25 btn' onClick={handleOpenOptions}>
|
<button className='bg-transparent px-2 py-1.25 btn' onClick={handleOpenOptions}>
|
||||||
@@ -155,6 +160,7 @@ export default function PopupMain(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DialogProvider>
|
||||||
</ExtensionRoot>
|
</ExtensionRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
|
|||||||
>
|
>
|
||||||
<div className={clsx('fixed inset-0 z-50 bg-slate-700/35')} />
|
<div className={clsx('fixed inset-0 z-50 bg-slate-700/35')} />
|
||||||
</TransitionChild>
|
</TransitionChild>
|
||||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
<div className='fixed inset-0 z-50 flex items-center justify-center p-2'>
|
||||||
<TransitionChild
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='transition duration-375 motion-reduce:duration-0 ease-[cubic-bezier(0.05,0.4,0.2,1)]'
|
enter='transition duration-375 motion-reduce:duration-0 ease-[cubic-bezier(0.05,0.4,0.2,1)]'
|
||||||
@@ -56,7 +56,7 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
|
|||||||
>
|
>
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-solid border-ut-offwhite rounded bg-white shadow-xl ml-[calc(100vw-100%)]',
|
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-solid border-ut-offwhite rounded bg-white shadow-xl ml-[calc(100vw-100%-1rem)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.extensionRoot {
|
.extensionRoot {
|
||||||
@apply font-sans h-full;
|
@apply font-sans;
|
||||||
color: #303030;
|
color: #303030;
|
||||||
|
|
||||||
[data-rfd-drag-handle-context-id=':r1:'] {
|
[data-rfd-drag-handle-context-id=':r1:'] {
|
||||||
|
|||||||
@@ -2,24 +2,27 @@
|
|||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
|
|
||||||
import styles from './ExtensionRoot.module.scss';
|
import styles from './ExtensionRoot.module.scss';
|
||||||
|
|
||||||
interface Props {
|
export const styleResetClass = styles.extensionRoot;
|
||||||
testId?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper component for the extension elements that adds some basic styling to them
|
* A wrapper component for the extension elements that adds some basic styling to them
|
||||||
*/
|
*/
|
||||||
export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JSX.Element {
|
export default function ExtensionRoot(props: React.HTMLProps<HTMLDivElement>): JSX.Element {
|
||||||
|
const { className, ...others } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<div className={clsx(styles.extensionRoot, props.className)} data-testid={props.testId}>
|
<div className={clsx(styleResetClass, 'h-full', className)} {...others} />
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ExtensionRootWrapper = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>((props, ref) => (
|
||||||
|
<div className={styleResetClass}>
|
||||||
|
<div {...props} ref={ref} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
||||||
import deleteSchedule from '@pages/background/lib/deleteSchedule';
|
import deleteSchedule from '@pages/background/lib/deleteSchedule';
|
||||||
|
import duplicateSchedule from '@pages/background/lib/duplicateSchedule';
|
||||||
import renameSchedule from '@pages/background/lib/renameSchedule';
|
import renameSchedule from '@pages/background/lib/renameSchedule';
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
@@ -6,11 +8,12 @@ import useSchedules from '@views/hooks/useSchedules';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import XIcon from '~icons/material-symbols/close';
|
|
||||||
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
||||||
|
import MoreActionsIcon from '~icons/material-symbols/more-vert';
|
||||||
|
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import { usePrompt } from './DialogProvider/DialogProvider';
|
import DialogProvider, { usePrompt } from './DialogProvider/DialogProvider';
|
||||||
|
import { ExtensionRootWrapper, styleResetClass } from './ExtensionRoot/ExtensionRoot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for the ScheduleListItem component.
|
* Props for the ScheduleListItem component.
|
||||||
@@ -29,14 +32,12 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
|
|||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editorValue, setEditorValue] = useState(schedule.name);
|
const [editorValue, setEditorValue] = useState(schedule.name);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const showDialog = usePrompt();
|
const showDialog = usePrompt();
|
||||||
|
|
||||||
const editorRef = React.useRef<HTMLInputElement>(null);
|
const editorRef = React.useRef<HTMLInputElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const editor = editorRef.current;
|
const editor = editorRef.current;
|
||||||
|
|
||||||
setEditorValue(schedule.name);
|
setEditorValue(schedule.name);
|
||||||
|
|
||||||
if (isEditing && editor) {
|
if (isEditing && editor) {
|
||||||
@@ -47,35 +48,63 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
|
|||||||
|
|
||||||
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
|
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = async () => {
|
||||||
if (editorValue.trim() !== '') {
|
if (editorValue.trim() !== '' && editorValue.trim() !== schedule.name) {
|
||||||
schedule.name = editorValue.trim();
|
schedule.name = (await renameSchedule(schedule.id, editorValue.trim())) as string;
|
||||||
renameSchedule(schedule.id, schedule.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = () => {
|
const handleDelete = () => {
|
||||||
deleteSchedule(schedule.id).catch(e => setError(e.message));
|
if (schedule.id === activeSchedule.id) {
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error);
|
|
||||||
showDialog({
|
showDialog({
|
||||||
title: <span className='text-ut-red'>Something went wrong.</span>,
|
title: `Unable to delete active schedule.`,
|
||||||
description: error,
|
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<Text>Deleting the active schedule</Text>
|
||||||
|
<Text className='text-ut-burntorange'> {schedule.name} </Text>
|
||||||
|
<Text>is not allowed. Please switch to another schedule and try again.</Text>
|
||||||
|
</>
|
||||||
|
),
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
buttons: close => (
|
buttons: close => (
|
||||||
<Button variant='filled' color='ut-black' onClick={close}>
|
<Button variant='filled' color='ut-burntorange' onClick={close}>
|
||||||
I understand
|
I Understand
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
onClose: () => setError(undefined),
|
});
|
||||||
|
} else {
|
||||||
|
showDialog({
|
||||||
|
title: `Are you sure?`,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<Text>Deleting</Text>
|
||||||
|
<Text className='text-ut-burntorange'> {schedule.name} </Text>
|
||||||
|
<Text>is permanent and will remove all added courses from that schedule.</Text>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
buttons: close => (
|
||||||
|
<>
|
||||||
|
<Button variant='single' color='ut-black' onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='filled'
|
||||||
|
color='ut-red'
|
||||||
|
onClick={() => {
|
||||||
|
close();
|
||||||
|
deleteSchedule(schedule.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete Permanently
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='rounded bg-white'>
|
<div className='rounded bg-white'>
|
||||||
@@ -85,12 +114,12 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
|
|||||||
</div>
|
</div>
|
||||||
<div className='group relative flex flex-1 items-center overflow-x-hidden'>
|
<div className='group relative flex flex-1 items-center overflow-x-hidden'>
|
||||||
<div
|
<div
|
||||||
className='flex flex-grow items-center gap-1.5 overflow-x-hidden'
|
className='group/circle flex flex-grow items-center gap-1.5 overflow-x-hidden'
|
||||||
onClick={(...e) => !isEditing && onClick?.(...e)}
|
onClick={(...e) => !isEditing && onClick?.(...e)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'h-5.5 w-5.5 relative flex-shrink-0 border-2px border-current rounded-full btn-transition group-active:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition transform-gpu scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
|
'h-5.5 w-5.5 relative flex-shrink-0 border-2px border-current rounded-full btn-transition group-active/circle:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition transform-gpu scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
|
||||||
{
|
{
|
||||||
'after:(scale-0! opacity-0 ease-in-out! duration-200!)': !isActive,
|
'after:(scale-0! opacity-0 ease-in-out! duration-200!)': !isActive,
|
||||||
}
|
}
|
||||||
@@ -119,9 +148,57 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='self-end'>
|
<DialogProvider>
|
||||||
<XIcon className='invisible h-5 w-5 text-ut-red group-hover:visible' onClick={onDelete} />
|
<Menu>
|
||||||
</div>
|
<MenuButton className='invisible h-fit bg-transparent p-0 text-ut-gray btn-transition data-[open]:visible group-hover:visible'>
|
||||||
|
<MoreActionsIcon className='h-6 w-6' />
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<MenuItems
|
||||||
|
as={ExtensionRootWrapper}
|
||||||
|
className={clsx([
|
||||||
|
styleResetClass,
|
||||||
|
'w-30 cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite focus:outline-none',
|
||||||
|
'data-[closed]:(opacity-0 scale-95)',
|
||||||
|
'data-[enter]:(ease-out-expo duration-150)',
|
||||||
|
'data-[leave]:(ease-out duration-50)',
|
||||||
|
])}
|
||||||
|
transition
|
||||||
|
anchor='bottom end'
|
||||||
|
>
|
||||||
|
<MenuItem>
|
||||||
|
<Text
|
||||||
|
as='button'
|
||||||
|
variant='small'
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
className='w-full rounded bg-transparent p-2 text-left data-[focus]:bg-gray-200/40'
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<Text
|
||||||
|
as='button'
|
||||||
|
variant='small'
|
||||||
|
onClick={() => duplicateSchedule(schedule.id)}
|
||||||
|
className='w-full rounded bg-transparent p-2 text-left data-[focus]:bg-gray-200/40'
|
||||||
|
>
|
||||||
|
Duplicate
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<Text
|
||||||
|
as='button'
|
||||||
|
variant='small'
|
||||||
|
onClick={handleDelete}
|
||||||
|
className='w-full rounded bg-transparent p-2 text-left text-ut-red data-[focus]:bg-red-200/40'
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
|
</Menu>
|
||||||
|
</DialogProvider>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user