Skip to content

Commit

Permalink
fix(ui): fix longstanding drag-and-drop duplication issue (#502)
Browse files Browse the repository at this point in the history
* fix(ui): fixed color switching on list reordering

* chore: remove lock file

* chore: add back lock file

* feat(ui): fix color duplication issue and prevent scrolling beyond parent

* feat(ui): add to storybook

* fix(ui): remove white background while dragging

* chore: remove dnd pangea from package.json

* chore: rebuild lock file

* chore: remove nested li element issue

* fix(ui): allow grabbing cursor while dragging

* fix(ui): address chromatic errors

* fix(ui): address chromatic errors

* fix(ui): address linting issues and pass tests

* fix(ui): create hook for modifying the cursor globally

* chore: add check for storybook env

* chore: add back unused import to AddAllButton

* fix: make cursor grabbing hook more explicit

* chore: move sortable list item into sortable list file

* fix: remove isStorybook prop from ScheduleListItem

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
  • Loading branch information
Preston-Cook and doprz authored Feb 4, 2025
1 parent c2328e4 commit 4752f58
Show file tree
Hide file tree
Showing 16 changed files with 2,925 additions and 2,634 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
"prepare": "husky"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.0",
"@hello-pangea/dnd": "^17.0.0",
"@octokit/rest": "^21.0.2",
"@phosphor-icons/react": "^2.1.7",
"@sentry/react": "^8.38.0",
Expand Down
4,890 changes: 2,547 additions & 2,343 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

22 changes: 7 additions & 15 deletions src/stories/components/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { UserSchedule } from '@shared/types/UserSchedule';
import { generateRandomId } from '@shared/util/random';
import type { Meta, StoryObj } from '@storybook/react';
import List from '@views/components/common/List';
import type { ScheduleDropdownProps } from '@views/components/common/ScheduleDropdown';
import ScheduleDropdown from '@views/components/common/ScheduleDropdown';
import ScheduleListItem from '@views/components/common/ScheduleListItem';
import { SortableList } from '@views/components/common/SortableList';
import useSchedules, { getActiveSchedule, switchSchedule } from '@views/hooks/useSchedules';
import type { Serialized } from 'chrome-extension-toolkit';
import React, { useEffect } from 'react';
Expand Down Expand Up @@ -48,29 +48,21 @@ const meta: Meta<typeof ScheduleDropdown> = {
return (
<div className='w-80'>
<ScheduleDropdown {...args}>
<List
<SortableList
className='gap-spacing-3'
draggables={schedules}
itemKey={s => s.id}
onReordered={reordered => {
onChange={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);

// don't care about the promise
UserScheduleStore.set('schedules', reordered);
UserScheduleStore.set('activeIndex', activeIndex);
}}
gap={10}
>
{(schedule, handleProps) => (
<ScheduleListItem
schedule={schedule}
onClick={() => {
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>
renderItem={schedule => (
<ScheduleListItem schedule={schedule} onClick={() => switchSchedule(schedule.id)} />
)}
</List>
/>
</ScheduleDropdown>
</div>
);
Expand Down
19 changes: 19 additions & 0 deletions src/stories/components/ImportantLinks.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react';
import ImportantLinks from '@views/components/calendar/ImportantLinks';

const meta = {
title: 'Components/Common/ImportantLinks',
component: ImportantLinks,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof ImportantLinks>;
export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {},
};
35 changes: 16 additions & 19 deletions src/stories/components/List.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting } from '@shared/types/CourseMeeting';
import Instructor from '@shared/types/Instructor';
import { tailwindColorways } from '@shared/util/storybook';
import type { Meta, StoryObj } from '@storybook/react';
import List from '@views/components/common/List';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock';
import type { BaseItem } from '@views/components/common/SortableList';
import { SortableList } from '@views/components/common/SortableList';
import React from 'react';

const numberOfCourses = 5;

// TODO: move into utils
/**
* Generates an array of courses.
*
Expand Down Expand Up @@ -73,36 +72,34 @@ const generateCourses = (count: number): Course[] => {
};

const exampleCourses = generateCourses(numberOfCourses);
const generateCourseBlocks = (course: Course, dragHandleProps: DraggableProvidedDragHandleProps) => (
<PopupCourseBlock key={course.uniqueId} course={course} colors={course.colors} dragHandleProps={dragHandleProps} />
);

type CourseWithId = Course & BaseItem;

const meta = {
title: 'Components/Common/List',
component: List,
component: SortableList,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
gap: { control: 'number' },
},
} satisfies Meta<typeof List<Course>>;
} satisfies Meta<typeof SortableList<CourseWithId>>;
export default meta;

type Story = StoryObj<Meta<typeof List<Course>>>;
type Story = StoryObj<Meta<typeof SortableList<CourseWithId>>>;

export const Default: Story = {
args: {
draggables: exampleCourses,
children: generateCourseBlocks,
itemKey: item => item.uniqueId,
gap: 12,
onReordered: () => {},
draggables: exampleCourses.map(course => ({
id: course.uniqueId,
...course,
getConflicts: course.getConflicts,
})),
onChange: () => {},
renderItem: course => <PopupCourseBlock key={course.id} course={course} colors={course.colors} />,
},
render: args => (
<div className='w-sm'>
<List {...args} />
<div className='h-3xl w-3xl transform-none'>
<SortableList {...args} />
</div>
),
};
48 changes: 18 additions & 30 deletions src/views/components/PopupMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { openReportWindow } from '@shared/util/openReportWindow';
import Divider from '@views/components/common/Divider';
import List from '@views/components/common/List';
import Text from '@views/components/common/Text/Text';
import { useEnforceScheduleLimit } from '@views/hooks/useEnforceScheduleLimit';
import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules';
Expand All @@ -20,6 +19,7 @@ import { SmallLogo } from './common/LogoIcon';
import PopupCourseBlock from './common/PopupCourseBlock';
import ScheduleDropdown from './common/ScheduleDropdown';
import ScheduleListItem from './common/ScheduleListItem';
import { SortableList } from './common/SortableList';

/**
* Renders the main popup component.
Expand Down Expand Up @@ -56,6 +56,7 @@ export default function PopupMain(): JSX.Element {
}, []);

const [activeSchedule, schedules] = useSchedules();

// const [isRefreshing, setIsRefreshing] = useState(false);
const [funny, setFunny] = useState<string>('');

Expand Down Expand Up @@ -103,29 +104,20 @@ export default function PopupMain(): JSX.Element {
<Divider orientation='horizontal' size='100%' />
<div className='px-5 pb-2.5 pt-3.75'>
<ScheduleDropdown>
<List
<SortableList
draggables={schedules}
itemKey={schedule => schedule.id}
onReordered={reordered => {
onChange={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);

// don't care about the promise
UserScheduleStore.set('schedules', reordered);
UserScheduleStore.set('activeIndex', activeIndex);
}}
gap={10}
>
{(schedule, handleProps) => (
<ScheduleListItem
schedule={schedule}
onClick={() => {
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>
renderItem={schedule => (
<ScheduleListItem schedule={schedule} onClick={() => switchSchedule(schedule.id)} />
)}
</List>
/>
<div className='bottom-0 right-0 mt-2.5 w-full flex justify-end'>
<Button
variant='filled'
Expand All @@ -149,24 +141,20 @@ export default function PopupMain(): JSX.Element {
)}
<div className='flex-1 self-stretch overflow-y-auto px-5'>
{activeSchedule?.courses?.length > 0 && (
<List
draggables={activeSchedule.courses}
onReordered={reordered => {
activeSchedule.courses = reordered;
<SortableList
draggables={activeSchedule.courses.map(course => ({
id: course.uniqueId,
...course,
getConflicts: course.getConflicts,
}))}
onChange={reordered => {
activeSchedule.courses = reordered.map(({ id: _id, ...course }) => course);
replaceSchedule(getActiveSchedule(), activeSchedule);
}}
itemKey={e => e.uniqueId}
gap={10}
>
{(course, handleProps) => (
<PopupCourseBlock
key={course.uniqueId}
course={course}
colors={course.colors}
dragHandleProps={handleProps}
/>
renderItem={course => (
<PopupCourseBlock key={course.id} course={course} colors={course.colors} />
)}
</List>
/>
)}
</div>
<div className='w-full flex flex-col items-center gap-1.25 p-5 pt-3.75'>
Expand Down
23 changes: 7 additions & 16 deletions src/views/components/calendar/CalendarSchedules.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import createSchedule from '@pages/background/lib/createSchedule';
import { Plus } from '@phosphor-icons/react';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { getSpacingInPx } from '@shared/types/Spacing';
import { Button } from '@views/components/common/Button';
import List from '@views/components/common/List';
import ScheduleListItem from '@views/components/common/ScheduleListItem';
import { SortableList } from '@views/components/common/SortableList';
import Text from '@views/components/common/Text/Text';
import { useEnforceScheduleLimit } from '@views/hooks/useEnforceScheduleLimit';
import useSchedules, { getActiveSchedule, switchSchedule } from '@views/hooks/useSchedules';
Expand Down Expand Up @@ -41,29 +40,21 @@ export function CalendarSchedules() {
/>
</div>
<div className='w-full flex flex-col'>
<List
gap={getSpacingInPx('spacing-3')}
<SortableList
className='gap-spacing-3'
draggables={schedules}
itemKey={s => s.id}
onReordered={reordered => {
onChange={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);

// don't care about the promise
UserScheduleStore.set('schedules', reordered);
UserScheduleStore.set('activeIndex', activeIndex);
}}
>
{(schedule, handleProps) => (
<ScheduleListItem
schedule={schedule}
onClick={() => {
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>
renderItem={schedule => (
<ScheduleListItem schedule={schedule} onClick={() => switchSchedule(schedule.id)} />
)}
</List>
/>
</div>
</div>
);
Expand Down
Loading

0 comments on commit 4752f58

Please sign in to comment.