Skip to content

Commit 0f7dc9e

Browse files
authored
View Editing Fixes (#127)
* Fix section duplication * Chart label fix * Disable chart editing in product preview modal * Duplicate page group and pages
1 parent 5dd131b commit 0f7dc9e

File tree

7 files changed

+129
-32
lines changed

7 files changed

+129
-32
lines changed

src/components/app/ProductPreviewModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export const ProductPreviewModal = ({
230230
</div>
231231
<div className="flex flex-1 flex-col h-0 border rounded overflow-hidden">
232232
<Chart
233+
enableEditing={false}
233234
chartEntity={chartEntity}
234235
products={products}
235236
dateRange={dateRange}

src/components/entities/chart/Chart.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ export declare type ChartProps = {
7777
chartEntity: ChartEntity;
7878
compact?: boolean;
7979
dateRange: DateRange;
80+
enableEditing?: boolean;
8081
hoverDate: Date | null;
8182
instrument?: string | null;
82-
isEditing?: boolean;
8383
loading?: boolean;
8484
mission?: string | null;
8585
onDateRangeChange?: (dateRange: DateRange) => void;
@@ -122,7 +122,7 @@ export const Chart = ({
122122
mission: missionProp,
123123
compact = false,
124124
showHeader = true,
125-
isEditing = false,
125+
enableEditing = true,
126126
onDateRangeChange = () => {},
127127
onHoverDateChange = () => {},
128128
onSelectPoint = () => {},
@@ -605,9 +605,9 @@ export const Chart = ({
605605
type: "line",
606606
label:
607607
layer.label ||
608-
`${mission} ${instrument} ${layer.dataset} ${layer.fields[0]} ${
609-
layer.channels
610-
? `(${layer.channels
608+
`${mission} ${instrument} ${layer.dataset} ${layer.fields[0]}${
609+
layer.channels && layer.channels.length > 0
610+
? ` (${layer.channels
611611
.map((c) => `${c.id}: ${c.value}`)
612612
.join(", ")})`
613613
: ""
@@ -1376,19 +1376,19 @@ export const Chart = ({
13761376
<DropdownMenuContent className="w-56">
13771377
<DropdownMenuItem
13781378
onClick={() => onEdit()}
1379-
disabled={isEditing}
1379+
disabled={!enableEditing}
13801380
>
13811381
<Pencil /> Edit
13821382
</DropdownMenuItem>
13831383
<DropdownMenuItem
13841384
onClick={() => onDuplicate()}
1385-
disabled={isEditing}
1385+
disabled={!enableEditing}
13861386
>
13871387
<CopyPlus /> Duplicate
13881388
</DropdownMenuItem>
13891389
<DropdownMenuItem
13901390
onClick={() => onDelete()}
1391-
disabled={isEditing}
1391+
disabled={!enableEditing}
13921392
>
13931393
<Trash2 /> Delete
13941394
</DropdownMenuItem>

src/components/page/Entity.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ export declare type EntityProps = {
2626
className?: string;
2727
compact?: boolean;
2828
dateRange: DateRange;
29+
enableEditing?: boolean;
2930
entity: EntityType;
3031
hoverDate: Date | null; // TODO could this be Date | undefined and made optional?
3132
instrument?: string | null;
32-
isEditing?: boolean;
3333
loading?: boolean;
3434
mission?: string | null;
3535
onDateRangeChange?: (dateRange: DateRange) => void;
@@ -59,7 +59,7 @@ export const Entity = (props: EntityProps) => {
5959
className = "",
6060
showHeader = entity.showHeader ?? true,
6161
compact = false,
62-
isEditing = false,
62+
enableEditing = true,
6363
onDateRangeChange = () => {},
6464
onHoverDateChange = () => {},
6565
onSelectPoint = () => {},
@@ -78,7 +78,7 @@ export const Entity = (props: EntityProps) => {
7878
<div className={entityClass}>
7979
{isChartEntity(entity) && (
8080
<Chart
81-
isEditing={isEditing}
81+
enableEditing={enableEditing}
8282
loading={loading}
8383
chartEntity={entity}
8484
dateRange={dateRange}

src/components/page/ViewPage.tsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Section as SectionType,
2525
} from "../../types/view";
2626
import { generateUUID } from "../../utilities/generic";
27+
import { duplicateEntity, duplicateSection } from "../../utilities/view";
2728
import { useConfirm } from "../ui/AlertDialogProvider";
2829
import { DateRangePicker } from "../ui/DateRangePicker";
2930
import EntityEditor from "../ui/EntityEditor";
@@ -145,18 +146,7 @@ export const ViewPage = ({
145146
...viewPage,
146147
sections: viewPage?.sections.map((s) => {
147148
if (s.id === section.id) {
148-
const newId = generateUUID();
149-
const newEntity = { ...entity, id: newId };
150-
const newEntities: Entity[] = s.entities.concat(newEntity);
151-
const newLayout: SectionLayout[] = [
152-
...s.layout,
153-
{ i: newId, w: 4, h: 2, x: 0, y: 0 },
154-
];
155-
return {
156-
...s,
157-
entities: newEntities,
158-
layout: newLayout,
159-
};
149+
return duplicateEntity(entity, section);
160150
}
161151
return s;
162152
}),
@@ -173,7 +163,7 @@ export const ViewPage = ({
173163
}
174164
// Find section containing entity
175165
const section = viewPage.sections.find((s) =>
176-
s.entities.find((s) => s.id === entity.id)
166+
s.entities.find((e) => e.id === entity.id)
177167
);
178168
if (!section) {
179169
return;
@@ -283,13 +273,18 @@ export const ViewPage = ({
283273
);
284274

285275
const onSectionDuplicate = useCallback(
286-
async (section: SectionType) => {
276+
async (section: SectionType, insertAfter: number) => {
287277
if (!viewPage) {
288278
return;
289279
}
280+
const newSection = duplicateSection(section);
290281
const newViewPage: PageType = {
291282
...viewPage,
292-
sections: viewPage.sections.concat({ ...section, id: generateUUID() }),
283+
sections: [
284+
...viewPage.sections.slice(0, insertAfter + 1),
285+
newSection,
286+
...viewPage.sections.slice(insertAfter + 1),
287+
],
293288
};
294289
onPageChange(newViewPage);
295290
},
@@ -408,7 +403,7 @@ export const ViewPage = ({
408403
)}
409404
{!loadingInitialData &&
410405
!entityToEdit &&
411-
viewPage.sections.map((section) => (
406+
viewPage.sections.map((section, i) => (
412407
<Section
413408
products={products}
414409
section={section}
@@ -427,7 +422,7 @@ export const ViewPage = ({
427422
onEntityDuplicate={onEntityDuplicate}
428423
onEntityEdit={(entity) => setEntityToEdit(entity)}
429424
onSectionDelete={onSectionDelete}
430-
onSectionDuplicate={onSectionDuplicate}
425+
onSectionDuplicate={() => onSectionDuplicate(section, i)}
431426
onSectionChange={(newSection: SectionType) => {
432427
const newViewPage: PageType = {
433428
...viewPage,

src/components/ui/EntityEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export const EntityEditor = ({
213213
>
214214
<div className="h-[50%]">
215215
<Entity
216-
isEditing
216+
enableEditing={false}
217217
className="h-full"
218218
entity={newEntity}
219219
products={products}

src/routes/ManagementPage.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Input,
1313
Label,
1414
} from "@nasa-jpl/stellar-react";
15-
import { ArrowDown, ArrowUp, Trash2 } from "lucide-react";
15+
import { ArrowDown, ArrowUp, Copy, Trash2 } from "lucide-react";
1616
import { FormEvent, useState } from "react";
1717
import { useOutletContext } from "react-router-dom";
1818
import { z } from "zod";
@@ -22,8 +22,12 @@ import { Tooltip } from "../components/ui/Tooltip";
2222
import { Product } from "../types/api";
2323
import { ProductPreview } from "../types/page";
2424
import { PageGroup, Page as PageType, View } from "../types/view";
25-
import { downloadJSON } from "../utilities/generic";
26-
import { createViewPage, createViewPageGroup } from "../utilities/view";
25+
import { downloadJSON, generateUUID } from "../utilities/generic";
26+
import {
27+
createViewPage,
28+
createViewPageGroup,
29+
duplicateSection,
30+
} from "../utilities/view";
2731

2832
export default function ManagementPage() {
2933
// TODO type outlet context instead of duplicating
@@ -114,6 +118,28 @@ export default function ManagementPage() {
114118
setView(newView);
115119
};
116120

121+
const duplicatePageGroup = (pageGroup: PageGroup, insertAfter: number) => {
122+
const newPageGroup: PageGroup = {
123+
...pageGroup,
124+
id: generateUUID(),
125+
title: `${pageGroup.title} Copy`,
126+
url: `${pageGroup.url}_copy`,
127+
pages: pageGroup.pages.map((page) => ({
128+
...page,
129+
sections: page.sections.map(duplicateSection),
130+
})),
131+
};
132+
const newView = {
133+
...view,
134+
pageGroups: [
135+
...view.pageGroups.slice(0, insertAfter + 1),
136+
newPageGroup,
137+
...view.pageGroups.slice(insertAfter + 1),
138+
],
139+
};
140+
setView(newView);
141+
};
142+
117143
const deletePage = (pageGroupId: string, pageId: string) => {
118144
const newView = {
119145
...view,
@@ -187,6 +213,29 @@ export default function ManagementPage() {
187213
setView({ ...view, pageGroups: newPageGroups });
188214
};
189215

216+
const duplicatePage = (
217+
page: PageType,
218+
pageGroup: PageGroup,
219+
insertAfter: number
220+
) => {
221+
const newPage: PageType = {
222+
...page,
223+
id: generateUUID(),
224+
title: `${page.title} Copy`,
225+
url: `${page.url}_copy`,
226+
sections: page.sections.map(duplicateSection),
227+
};
228+
const newPageGroup: PageGroup = {
229+
...pageGroup,
230+
pages: [
231+
...pageGroup.pages.slice(0, insertAfter + 1),
232+
newPage,
233+
...pageGroup.pages.slice(insertAfter + 1),
234+
],
235+
};
236+
updatePageGroup(newPageGroup);
237+
};
238+
190239
const movePage = (
191240
page: PageType,
192241
pageGroup: PageGroup,
@@ -348,6 +397,15 @@ export default function ManagementPage() {
348397
}
349398
/>
350399
<div className="flex self-end">
400+
<Tooltip content="Duplicate Page">
401+
<Button
402+
variant="ghost"
403+
size="icon"
404+
onClick={() => duplicatePageGroup(pageGroup, i)}
405+
>
406+
<Copy size={16} />
407+
</Button>
408+
</Tooltip>
351409
{renderDeletionConfirmation("page group", () =>
352410
deletePageGroup(pageGroup.id)
353411
)}
@@ -443,6 +501,15 @@ export default function ManagementPage() {
443501
}
444502
/>
445503
<div className="flex self-end">
504+
<Tooltip content="Duplicate Page Group">
505+
<Button
506+
variant="ghost"
507+
size="icon"
508+
onClick={() => duplicatePage(page, pageGroup, j)}
509+
>
510+
<Copy size={16} />
511+
</Button>
512+
</Tooltip>
446513
{renderDeletionConfirmation("page", () =>
447514
deletePage(pageGroup.id, page.id)
448515
)}

src/utilities/view.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
MapEntity,
1414
Page,
1515
PageGroup,
16+
Section,
17+
SectionLayout,
1618
TableEntity,
1719
TextEntity,
1820
TimeSeriesPoint,
@@ -234,3 +236,35 @@ export function createViewPageGroup(params: Partial<PageGroup>): PageGroup {
234236
...params,
235237
};
236238
}
239+
240+
export function duplicateEntity(entity: Entity, section: Section): Section {
241+
const newId = generateUUID();
242+
const newEntity = structuredClone(entity);
243+
newEntity.id = newId;
244+
const newEntities: Entity[] = section.entities.concat(newEntity);
245+
const newLayout: SectionLayout[] = [
246+
...section.layout,
247+
{ i: newId, w: 4, h: 2, x: 0, y: 0 },
248+
];
249+
return {
250+
...section,
251+
entities: newEntities,
252+
layout: newLayout,
253+
};
254+
}
255+
256+
export function duplicateSection(section: Section): Section {
257+
const newSection = structuredClone(section);
258+
newSection.id = generateUUID();
259+
newSection.entities.forEach((entity) => {
260+
const newId = generateUUID();
261+
// Find matching entity within layout and map new ID
262+
newSection.layout.forEach((l) => {
263+
if (l.i === entity.id) {
264+
l.i = newId;
265+
}
266+
});
267+
entity.id = newId;
268+
});
269+
return newSection;
270+
}

0 commit comments

Comments
 (0)