From 52f675c5a58b6f4f6a1b18de32215cf3a22b8486 Mon Sep 17 00:00:00 2001 From: cade Date: Fri, 23 Aug 2024 15:36:38 -0600 Subject: [PATCH] make insights orderable --- app/_components/insight-form.tsx | 2 + app/_components/insight-menu.tsx | 48 ++++--- app/_components/insight.tsx | 119 +++++++++++++++++ app/_components/insights.tsx | 123 +++++++++--------- app/_components/module-form-section.tsx | 2 +- app/_components/session-form.tsx | 14 +- app/_mutations/reorder-insights.ts | 50 +++++++ app/_mutations/upsert-insight.ts | 13 +- app/_queries/get-insight.ts | 2 +- app/_queries/list-insights.ts | 5 +- ...0823183813_add-order-to-insights-table.sql | 2 + 11 files changed, 278 insertions(+), 102 deletions(-) create mode 100644 app/_components/insight.tsx create mode 100644 app/_mutations/reorder-insights.ts create mode 100644 supabase/migrations/20240823183813_add-order-to-insights-table.sql diff --git a/app/_components/insight-form.tsx b/app/_components/insight-form.tsx index 5377486c..428ce66e 100644 --- a/app/_components/insight-form.tsx +++ b/app/_components/insight-form.tsx @@ -66,6 +66,7 @@ interface InsightFormProps { export type InsightFormValues = InsightConfigJson & { name: string; + order?: number | null; }; const InsightForm = ({ events, insight, subjectId }: InsightFormProps) => { @@ -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, diff --git a/app/_components/insight-menu.tsx b/app/_components/insight-menu.tsx index 852087b7..835e6758 100644 --- a/app/_components/insight-menu.tsx +++ b/app/_components/insight-menu.tsx @@ -12,32 +12,30 @@ interface InsightMenuProps { } const InsightMenu = ({ insightId, subjectId }: InsightMenuProps) => ( - <> - - -
-
- -
+ + +
+
+
- - - - - - Edit - - deleteInsight(insightId)} - /> - - - - +
+
+ + + + + Edit + + deleteInsight(insightId)} + /> + + +
); export default InsightMenu; diff --git a/app/_components/insight.tsx b/app/_components/insight.tsx new file mode 100644 index 00000000..b4ada82a --- /dev/null +++ b/app/_components/insight.tsx @@ -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[0]; + isPublic?: boolean; + isReadOnly: boolean; + searchString: string; + setActiveId?: Dispatch>; + setSyncDate?: Dispatch>; + 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 ( +
+
+ {!isReadOnly && ( + } + {...sortableInsight.attributes} + {...sortableInsight.listeners} + /> + )} + + {!isReadOnly && ( + + )} +
+ +
+ ); +}; + +export default Insight; diff --git a/app/_components/insights.tsx b/app/_components/insights.tsx index 760fdc3c..3b0e6280 100644 --- a/app/_components/insights.tsx +++ b/app/_components/insights.tsx @@ -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; @@ -23,7 +23,7 @@ interface InsightsProps { const Insights = ({ events, - insights, + insights: originalInsights, isArchived, isPublic, isTeamMember, @@ -33,65 +33,62 @@ const Insights = ({ }: InsightsProps) => { const [activeId, setActiveId] = useState(null); const [syncDate, setSyncDate] = useState(null); + const [insights, setInsights] = useState([]); + 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 ( + { + if (!over || active.id === over?.id) return; - return ( -
{ + 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} + > + insight.id)} + strategy={DndSortable.verticalListSortingStrategy} > -
- - {!isReadOnly && ( - - )} -
- -
- ); - }); + {insights.map((insight) => ( + + ))} + +
+ ); }; export default Insights; diff --git a/app/_components/module-form-section.tsx b/app/_components/module-form-section.tsx index 7bd0ddcd..34d0a5bf 100644 --- a/app/_components/module-form-section.tsx +++ b/app/_components/module-form-section.tsx @@ -101,7 +101,7 @@ const ModuleFormSection = < >
} {...attributes} {...listeners} diff --git a/app/_components/session-form.tsx b/app/_components/session-form.tsx index d2b99dae..b90553db 100644 --- a/app/_components/session-form.tsx +++ b/app/_components/session-form.tsx @@ -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} > diff --git a/app/_mutations/reorder-insights.ts b/app/_mutations/reorder-insights.ts new file mode 100644 index 00000000..5b757774 --- /dev/null +++ b/app/_mutations/reorder-insights.ts @@ -0,0 +1,50 @@ +'use server'; + +import listInsights from '@/_queries/list-insights'; +import createServerSupabaseClient from '@/_utilities/create-server-supabase-client'; + +const reorderInsights = async ({ + insightIds, + subjectId, +}: { + insightIds?: string[]; + subjectId: string; +}) => { + const supabase = createServerSupabaseClient(); + + // hack because supabase doesn't support bulk updates without all columns + // this introduces the possibility of a race condition + const { data: insights } = await listInsights(subjectId); + + if (!insights?.length) return; + + if (!insightIds) { + await supabase.from('insights').upsert( + insights.map((insight, order) => ({ + ...insight, + order, + subject_id: subjectId, + })), + ); + + return; + } + + const insightIdMap = insights.reduce>( + (acc, insight) => { + acc[insight.id] = insight; + return acc; + }, + {}, + ); + + await supabase.from('insights').upsert( + insightIds.map((insightId, order) => ({ + ...insightIdMap[insightId], + order, + subject_id: subjectId, + })), + ); +}; + +export default reorderInsights; diff --git a/app/_mutations/upsert-insight.ts b/app/_mutations/upsert-insight.ts index b249eded..011a5bbe 100644 --- a/app/_mutations/upsert-insight.ts +++ b/app/_mutations/upsert-insight.ts @@ -1,21 +1,30 @@ 'use server'; import { InsightFormValues } from '@/_components/insight-form'; +import reorderInsights from '@/_mutations/reorder-insights'; import createServerSupabaseClient from '@/_utilities/create-server-supabase-client'; import { revalidatePath } from 'next/cache'; const upsertInsight = async ( context: { insightId?: string; subjectId: string }, - { name, ...config }: InsightFormValues, + { name, order, ...config }: InsightFormValues, ) => { - const { error } = await createServerSupabaseClient().from('insights').upsert({ + const supabase = createServerSupabaseClient(); + + const { error } = await supabase.from('insights').upsert({ config, id: context.insightId, name: name.trim(), + order: order ?? -1, subject_id: context.subjectId, }); if (error) return { error: error.message }; + + if (typeof order === 'undefined') { + await reorderInsights({ subjectId: context.subjectId }); + } + revalidatePath('/', 'layout'); }; diff --git a/app/_queries/get-insight.ts b/app/_queries/get-insight.ts index ae7841b7..a14e3f84 100644 --- a/app/_queries/get-insight.ts +++ b/app/_queries/get-insight.ts @@ -3,7 +3,7 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie const getInsight = (insightId: string) => createServerSupabaseClient() .from('insights') - .select('config, id, name') + .select('config, id, name, order') .eq('id', insightId) .single(); diff --git a/app/_queries/list-insights.ts b/app/_queries/list-insights.ts index 75179109..1f092039 100644 --- a/app/_queries/list-insights.ts +++ b/app/_queries/list-insights.ts @@ -5,8 +5,9 @@ import createServerSupabaseClient from '@/_utilities/create-server-supabase-clie const listInsights = (subjectId: string) => createServerSupabaseClient() .from('insights') - .select('config, id, name') - .eq('subject_id', subjectId); + .select('config, id, name, order') + .eq('subject_id', subjectId) + .order('order'); export type ListInsightsData = Awaited>['data']; diff --git a/supabase/migrations/20240823183813_add-order-to-insights-table.sql b/supabase/migrations/20240823183813_add-order-to-insights-table.sql new file mode 100644 index 00000000..a239e8b6 --- /dev/null +++ b/supabase/migrations/20240823183813_add-order-to-insights-table.sql @@ -0,0 +1,2 @@ +alter table "public"."insights" add column "order" smallint; +create index insights_subject_id_order_index on public.insights using btree (subject_id, "order");