Skip to content

Commit

Permalink
make insights orderable
Browse files Browse the repository at this point in the history
  • Loading branch information
xvvvyz committed Aug 23, 2024
1 parent 70acfea commit 52f675c
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 102 deletions.
2 changes: 2 additions & 0 deletions app/_components/insight-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface InsightFormProps {

export type InsightFormValues = InsightConfigJson & {
name: string;
order?: number | null;
};

const InsightForm = ({ events, insight, subjectId }: InsightFormProps) => {
Expand All @@ -88,6 +89,7 @@ const InsightForm = ({ events, insight, subjectId }: InsightFormProps) => {
marginRight: config?.marginRight ?? '40',
marginTop: config?.marginTop ?? '30',
name: insight?.name ?? '',
order: insight?.order,
showBars: config?.showBars ?? false,
showDots: config?.showDots ?? true,
showLine: config?.showLine ?? false,
Expand Down
48 changes: 23 additions & 25 deletions app/_components/insight-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,30 @@ interface InsightMenuProps {
}

const InsightMenu = ({ insightId, subjectId }: InsightMenuProps) => (
<>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<div className="group mt-1 flex items-center justify-center px-2 text-fg-3 hover:text-fg-2 active:text-fg-2">
<div className="rounded-full p-2 group-hover:bg-alpha-1 group-active:bg-alpha-1">
<EllipsisVerticalIcon className="w-5" />
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<div className="group flex items-center justify-center px-1.5 text-fg-3 hover:text-fg-2 active:text-fg-2">
<div className="rounded-full p-2 group-hover:bg-alpha-1 group-active:bg-alpha-1">
<EllipsisVerticalIcon className="w-5" />
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="mr-1.5">
<DropdownMenu.Button
href={`/subjects/${subjectId}/insights/${insightId}/edit`}
scroll={false}
>
<PencilIcon className="w-5 text-fg-4" />
Edit
</DropdownMenu.Button>
<DropdownMenuDeleteItem
confirmText="Delete insight"
onConfirm={() => deleteInsight(insightId)}
/>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</>
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="mr-1.5">
<DropdownMenu.Button
href={`/subjects/${subjectId}/insights/${insightId}/edit`}
scroll={false}
>
<PencilIcon className="w-5 text-fg-4" />
Edit
</DropdownMenu.Button>
<DropdownMenuDeleteItem
confirmText="Delete insight"
onConfirm={() => deleteInsight(insightId)}
/>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);

export default InsightMenu;
119 changes: 119 additions & 0 deletions app/_components/insight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use client';

import Button from '@/_components/button';
import IconButton from '@/_components/icon-button';
import InsightMenu from '@/_components/insight-menu';
import PlotFigure from '@/_components/plot-figure';
import { ListEventsData } from '@/_queries/list-events';
import { ListInsightsData } from '@/_queries/list-insights';
import { InsightConfigJson } from '@/_types/insight-config-json';
import { useSortable } from '@dnd-kit/sortable';
import { ArrowsPointingOutIcon } from '@heroicons/react/24/outline';
import Bars2Icon from '@heroicons/react/24/outline/Bars2Icon';
import { Dispatch, SetStateAction } from 'react';
import { twMerge } from 'tailwind-merge';

interface InsightProps {
activeId: string | null;
config: InsightConfigJson;
events: ListEventsData;
insight: NonNullable<ListInsightsData>[0];
isPublic?: boolean;
isReadOnly: boolean;
searchString: string;
setActiveId?: Dispatch<SetStateAction<string | null>>;
setSyncDate?: Dispatch<SetStateAction<Date | null>>;
shareOrSubjects: 'share' | 'subjects';
subjectId: string;
syncDate: Date | null;
}

const Insight = ({
activeId,
config,
events,
insight,
isPublic,
isReadOnly,
searchString,
setActiveId,
setSyncDate,
shareOrSubjects,
subjectId,
syncDate,
}: InsightProps) => {
const sortableInsight = useSortable({ id: insight.id });

return (
<div
className={twMerge(
'min-h-12 rounded border border-alpha-1 bg-bg-3',
sortableInsight.isDragging && 'relative z-10 drop-shadow-2xl',
)}
ref={sortableInsight.setNodeRef}
style={{
transform: sortableInsight.transform
? sortableInsight.isDragging
? `translate(${sortableInsight.transform.x}px, ${sortableInsight.transform.y}px) scale(1.03)`
: `translate(${sortableInsight.transform.x}px, ${sortableInsight.transform.y}px)`
: undefined,
transition: sortableInsight.transition,
}}
>
<div className="-mb-4 flex items-stretch">
{!isReadOnly && (
<IconButton
className="m-0 h-full cursor-ns-resize touch-none px-4"
icon={<Bars2Icon className="w-5" />}
{...sortableInsight.attributes}
{...sortableInsight.listeners}
/>
)}
<Button
className={twMerge(
'm-0 flex w-full gap-4 pr-4 pt-3 leading-snug',
!isReadOnly && 'px-0',
)}
href={`/${shareOrSubjects}/${subjectId}/insights/${insight.id}${searchString}`}
scroll={false}
variant="link"
>
{insight.name}
<ArrowsPointingOutIcon className="ml-auto w-5 shrink-0" />
</Button>
{!isReadOnly && (
<InsightMenu insightId={insight.id} subjectId={subjectId} />
)}
</div>
<PlotFigure
barInterval={config.barInterval}
barReducer={config.barReducer}
defaultHeight={200}
events={events}
id={insight.id}
includeEventsFrom={config.includeEventsFrom}
includeEventsSince={config.includeEventsSince}
inputId={config.input}
inputOptions={config.inputOptions}
isPublic={isPublic}
lineCurveFunction={config.lineCurveFunction}
marginBottom={config.marginBottom}
marginLeft={config.marginLeft}
marginRight={config.marginRight}
marginTop={config.marginTop}
setActiveId={setActiveId}
setSyncDate={setSyncDate}
showBars={config.showBars}
showDots={config.showDots}
showLine={config.showLine}
showLinearRegression={config.showLinearRegression}
subjectId={subjectId}
syncDate={insight.id === activeId ? null : syncDate}
title={insight.name}
type={config.type}
/>
</div>
);
};

export default Insight;
123 changes: 60 additions & 63 deletions app/_components/insights.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use client';

import Button from '@/_components/button';
import InsightMenu from '@/_components/insight-menu';
import PlotFigure from '@/_components/plot-figure';
import Insight from '@/_components/insight';
import reorderInsights from '@/_mutations/reorder-insights';
import { ListEventsData } from '@/_queries/list-events';
import { ListInsightsData } from '@/_queries/list-insights';
import { InsightConfigJson } from '@/_types/insight-config-json';
import ArrowUpRightIcon from '@heroicons/react/24/outline/ArrowUpRightIcon';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
import * as DndCore from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import * as DndSortable from '@dnd-kit/sortable';
import { useEffect, useState } from 'react';

interface InsightsProps {
events: ListEventsData;
Expand All @@ -23,7 +23,7 @@ interface InsightsProps {

const Insights = ({
events,
insights,
insights: originalInsights,
isArchived,
isPublic,
isTeamMember,
Expand All @@ -33,65 +33,62 @@ const Insights = ({
}: InsightsProps) => {
const [activeId, setActiveId] = useState<string | null>(null);
const [syncDate, setSyncDate] = useState<Date | null>(null);
const [insights, setInsights] = useState<typeof originalInsights>([]);
const sensors = DndCore.useSensors(DndCore.useSensor(DndCore.PointerSensor));
useEffect(() => setInsights(originalInsights), [originalInsights]);

return insights.map((insight) => {
const config = insight.config as InsightConfigJson;
const isReadOnly = !isTeamMember || isArchived;
return (
<DndCore.DndContext
collisionDetection={DndCore.closestCenter}
id="insights"
modifiers={[restrictToVerticalAxis]}
onDragEnd={({ active, over }: DndCore.DragEndEvent) => {
if (!over || active.id === over?.id) return;

return (
<div
className="min-h-12 rounded border border-alpha-1 bg-bg-3 drop-shadow-2xl"
key={insight.id}
setInsights((insights) => {
const oldIndex = insights.findIndex(({ id }) => id === active.id);
const newIndex = insights.findIndex(({ id }) => id === over?.id);

const newInsights = DndSortable.arrayMove(
insights,
oldIndex,
newIndex,
);

void reorderInsights({
insightIds: newInsights.map((insight) => insight.id),
subjectId,
});

return newInsights;
});
}}
sensors={sensors}
>
<DndSortable.SortableContext
items={insights.map((insight) => insight.id)}
strategy={DndSortable.verticalListSortingStrategy}
>
<div className="-mb-4 flex items-stretch">
<Button
className={twMerge(
'm-0 flex w-full gap-4 px-4 pt-3 leading-snug',
!isReadOnly && 'pr-0',
)}
href={`/${shareOrSubjects}/${subjectId}/insights/${insight.id}${searchString}`}
scroll={false}
variant="link"
>
{insight.name}
{isReadOnly && (
<ArrowUpRightIcon className="ml-auto w-5 shrink-0" />
)}
</Button>
{!isReadOnly && (
<InsightMenu insightId={insight.id} subjectId={subjectId} />
)}
</div>
<PlotFigure
barInterval={config.barInterval}
barReducer={config.barReducer}
defaultHeight={200}
events={events}
id={insight.id}
includeEventsFrom={config.includeEventsFrom}
includeEventsSince={config.includeEventsSince}
inputId={config.input}
inputOptions={config.inputOptions}
isPublic={isPublic}
lineCurveFunction={config.lineCurveFunction}
marginBottom={config.marginBottom}
marginLeft={config.marginLeft}
marginRight={config.marginRight}
marginTop={config.marginTop}
setActiveId={setActiveId}
setSyncDate={setSyncDate}
showBars={config.showBars}
showDots={config.showDots}
showLine={config.showLine}
showLinearRegression={config.showLinearRegression}
subjectId={subjectId}
syncDate={insight.id === activeId ? null : syncDate}
title={insight.name}
type={config.type}
/>
</div>
);
});
{insights.map((insight) => (
<Insight
activeId={activeId}
config={insight.config as InsightConfigJson}
events={events}
insight={insight}
isPublic={isPublic}
isReadOnly={!isTeamMember || !!isArchived}
key={insight.id}
searchString={searchString}
setActiveId={setActiveId}
setSyncDate={setSyncDate}
shareOrSubjects={shareOrSubjects}
subjectId={subjectId}
syncDate={syncDate}
/>
))}
</DndSortable.SortableContext>
</DndCore.DndContext>
);
};

export default Insights;
2 changes: 1 addition & 1 deletion app/_components/module-form-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const ModuleFormSection = <
>
<div className="flex items-center justify-between rounded-t border border-alpha-1 bg-alpha-1">
<IconButton
className="m-0 h-full cursor-move touch-none px-4"
className="m-0 h-full cursor-ns-resize touch-none px-4"
icon={<Bars2Icon className="w-5" />}
{...attributes}
{...listeners}
Expand Down
14 changes: 6 additions & 8 deletions app/_components/session-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,13 @@ const SessionForm = ({
collisionDetection={DndCore.closestCenter}
id="modules"
modifiers={[restrictToVerticalAxis]}
onDragEnd={(event: DndCore.DragEndEvent) => {
const { active, over } = event;
onDragEnd={({ active, over }: DndCore.DragEndEvent) => {
if (!over || active.id === over.id) return;

if (over && active.id !== over.id) {
modulesArray.move(
modulesArray.fields.findIndex((f) => f.key === active.id),
modulesArray.fields.findIndex((f) => f.key === over.id),
);
}
modulesArray.move(
modulesArray.fields.findIndex((f) => f.key === active.id),
modulesArray.fields.findIndex((f) => f.key === over.id),
);
}}
sensors={sensors}
>
Expand Down
Loading

0 comments on commit 52f675c

Please sign in to comment.