Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const ProcessEditorWrapper = () => {
.map((net) => ({
netId: net.entityId,
title: net.title,
lastUpdated: net.lastUpdated,
}));
}, [persistedNets, selectedNetId]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type PersistedNet = {
title: string;
definition: SDCPN;
userEditable: boolean;
lastUpdated: string;
};

type UseProcessSaveAndLoadParams = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ export const getPersistedNetsFromSubgraph = (
const userEditable =
!!data.queryEntitySubgraph.entityPermissions?.[net.entityId]?.update;

const lastUpdated =
net.metadata.temporalVersioning.decisionTime.start.limit;

return {
entityId: net.entityId,
title: netTitle,
definition,
userEditable,
lastUpdated,
};
});
};
Expand Down
100 changes: 45 additions & 55 deletions apps/petrinaut-website/src/main/app.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { MinimalNetMetadata, SDCPN } from "@hashintel/petrinaut";
import { convertOldFormatToSDCPN, Petrinaut } from "@hashintel/petrinaut";
import { Petrinaut } from "@hashintel/petrinaut";
import { produce } from "immer";
import { useEffect, useRef, useState } from "react";

import { useSentryFeedbackAction } from "./app/sentry-feedback-button";
import {
isOldFormatInLocalStorage,
type SDCPNInLocalStorage,
useLocalStorageSDCPNs,
} from "./app/use-local-storage-sdcpns";
Expand All @@ -19,6 +18,13 @@ const EMPTY_SDCPN: SDCPN = {
differentialEquations: [],
};

const isEmptySDCPN = (sdcpn: SDCPN) =>
sdcpn.places.length === 0 &&
sdcpn.transitions.length === 0 &&
sdcpn.types.length === 0 &&
sdcpn.parameters.length === 0 &&
sdcpn.differentialEquations.length === 0;

export const DevApp = () => {
const sentryFeedbackAction = useSentryFeedbackAction();
const { storedSDCPNs, setStoredSDCPNs } = useLocalStorageSDCPNs();
Expand All @@ -28,13 +34,15 @@ export const DevApp = () => {
const currentNet = currentNetId ? (storedSDCPNs[currentNetId] ?? null) : null;

const existingNets: MinimalNetMetadata[] = Object.values(storedSDCPNs)
.filter(
(net): net is SDCPNInLocalStorage => !isOldFormatInLocalStorage(net),
)
.map((net) => ({
netId: net.id,
title: net.title,
}));
lastUpdated: net.lastUpdated,
}))
.sort(
(a, b) =>
new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime(),
);

const createNewNet = (params: {
petriNetDefinition: SDCPN;
Expand All @@ -47,11 +55,35 @@ export const DevApp = () => {
lastUpdated: new Date().toISOString(),
};

setStoredSDCPNs((prev) => ({ ...prev, [newNet.id]: newNet }));
setStoredSDCPNs((prev) => {
const next = { ...prev, [newNet.id]: newNet };

// Remove the previous net if it was empty and unmodified
if (currentNetId && currentNetId !== newNet.id) {
const prevNet = prev[currentNetId];
if (prevNet && isEmptySDCPN(prevNet.sdcpn)) {
delete next[currentNetId];
}
}

return next;
});
setCurrentNetId(newNet.id);
};

const loadPetriNet = (petriNetId: string) => {
// Remove the current net if it was empty and unmodified
if (currentNetId && currentNetId !== petriNetId) {
setStoredSDCPNs((prev) => {
const prevNet = prev[currentNetId];
if (prevNet && isEmptySDCPN(prevNet.sdcpn)) {
const next = { ...prev };
delete next[currentNetId];
return next;
}
return prev;
});
}
setCurrentNetId(petriNetId);
};

Expand All @@ -62,7 +94,7 @@ export const DevApp = () => {

setStoredSDCPNs((prev) =>
produce(prev, (draft) => {
if (draft[currentNetId] && "title" in draft[currentNetId]) {
if (draft[currentNetId]) {
draft[currentNetId].title = title;
}
}),
Expand Down Expand Up @@ -92,11 +124,7 @@ export const DevApp = () => {
history,
currentIndex,
reset: resetHistory,
} = useUndoRedo(
currentNet && !isOldFormatInLocalStorage(currentNet)
? currentNet.sdcpn
: EMPTY_SDCPN,
);
} = useUndoRedo(currentNet ? currentNet.sdcpn : EMPTY_SDCPN);

const mutatePetriNetDefinition = (
definitionMutationFn: (draft: SDCPN) => void,
Expand All @@ -111,7 +139,7 @@ export const DevApp = () => {
// (e.g. multi-node drag end) each see the latest state.
setStoredSDCPNs((prev) => {
const net = prev[currentNetId];
if (!net || isOldFormatInLocalStorage(net)) {
if (!net) {
return prev;
}
const updatedSDCPN = produce(net.sdcpn, definitionMutationFn);
Expand All @@ -121,6 +149,7 @@ export const DevApp = () => {
[currentNetId]: {
...net,
sdcpn: updatedSDCPN,
lastUpdated: new Date().toISOString(),
},
};
});
Expand All @@ -134,7 +163,7 @@ export const DevApp = () => {
useEffect(() => {
if (currentNetId !== prevNetIdRef.current) {
prevNetIdRef.current = currentNetId;
if (currentNet && !isOldFormatInLocalStorage(currentNet)) {
if (currentNet) {
resetHistory(currentNet.sdcpn);
}
}
Expand Down Expand Up @@ -169,41 +198,6 @@ export const DevApp = () => {
useEffect(() => {
const sdcpnsInStorage = Object.values(storedSDCPNs);

const convertedNets: Record<string, SDCPNInLocalStorage> = {};

for (const sdcpnInStorage of sdcpnsInStorage) {
if (!isOldFormatInLocalStorage(sdcpnInStorage)) {
continue;
}

const convertedSdcpn = convertOldFormatToSDCPN(sdcpnInStorage.sdcpn);

if (!convertedSdcpn) {
throw new Error(
"Couldn't convert old format to SDCPN, but should have been able to",
);
}

convertedNets[sdcpnInStorage.sdcpn.id] = {
/**
* The id and title used to be in the SDCPN definition itself, so we add them back here.
* A legacy provision only which can probably be removed once 2025 is over.
*/
id: sdcpnInStorage.sdcpn.id,
title: sdcpnInStorage.sdcpn.title,
sdcpn: convertedSdcpn,
lastUpdated: sdcpnInStorage.lastUpdated,
};
}

if (Object.keys(convertedNets).length > 0) {
setStoredSDCPNs((existingSDCPNs) => ({
...existingSDCPNs,
...convertedNets,
}));
return;
}

if (!sdcpnsInStorage[0]) {
createNewNet({
petriNetDefinition: {
Expand All @@ -215,16 +209,12 @@ export const DevApp = () => {
},
title: "New Process",
});
} else if (isOldFormatInLocalStorage(sdcpnsInStorage[0])) {
throw new Error(
"Old format SDCPN found in storage, but should have been converted",
);
} else if (!currentNetId) {
setCurrentNetId(sdcpnsInStorage[0].id);
}
}, [currentNetId, createNewNet, setStoredSDCPNs, storedSDCPNs]);

if (!currentNet || isOldFormatInLocalStorage(currentNet)) {
if (!currentNet) {
return null;
}

Expand Down
19 changes: 2 additions & 17 deletions apps/petrinaut-website/src/main/app/use-local-storage-sdcpns.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { OldFormat, SDCPN } from "@hashintel/petrinaut";
import { isOldFormat } from "@hashintel/petrinaut";
import type { SDCPN } from "@hashintel/petrinaut";
import { useLocalStorage } from "@mantine/hooks";

const rootLocalStorageKey = "petrinaut-sdcpn";
Expand All @@ -11,21 +10,7 @@ export type SDCPNInLocalStorage = {
title: string;
};

type OldFormatInLocalStorage = {
lastUpdated: string; // ISO timestamp
sdcpn: OldFormat;
};

type LocalStorageSDCPNsStore = Record<
string,
SDCPNInLocalStorage | OldFormatInLocalStorage
>;

export const isOldFormatInLocalStorage = (
stored: OldFormatInLocalStorage | SDCPNInLocalStorage,
): stored is OldFormatInLocalStorage => {
return !("id" in stored) && isOldFormat(stored.sdcpn);
};
type LocalStorageSDCPNsStore = Record<string, SDCPNInLocalStorage>;

export const useLocalStorageSDCPNs = () => {
const [storedSDCPNs, setStoredSDCPNs] =
Expand Down
3 changes: 2 additions & 1 deletion libs/@hashintel/petrinaut/src/components/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const submenuContentStyle = css({
boxShadow:
"[0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 1px -0.5px rgba(0, 0, 0, 0.04), 0px 4px 4px -12px rgba(0, 0, 0, 0.02), 0px 12px 12px -6px rgba(0, 0, 0, 0.02)]",
minWidth: "[180px]",
overflow: "hidden",
maxHeight: "var(--available-height)",
overflowY: "auto",
zIndex: 2,
transformOrigin: "var(--transform-origin)",
'&[data-state="open"]': {
Expand Down
1 change: 1 addition & 0 deletions libs/@hashintel/petrinaut/src/core/types/sdcpn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type SDCPN = {
export type MinimalNetMetadata = {
netId: string;
title: string;
lastUpdated: string;
};

export type MutateSDCPN = (mutateFn: (sdcpn: SDCPN) => void) => void;
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { SDCPN } from "../../../core/types/sdcpn";
import type { SDCPN } from "../core/types/sdcpn";
import { removeVisualInformation } from "./remove-visual-info";
import { SDCPN_FILE_FORMAT_VERSION } from "./types";

/**
* Saves the SDCPN to a JSON file by triggering a browser download.
* The file includes format metadata (version, meta.generator).
*
* @param petriNetDefinition - The SDCPN to save
* @param title - The title of the SDCPN
* @param removeVisualInfo - If true, removes visual positioning information (x, y) from places and transitions
Expand All @@ -16,28 +19,31 @@ export function exportSDCPN({
title: string;
removeVisualInfo?: boolean;
}): void {
// Optionally remove visual information
const sdcpnToExport = removeVisualInfo
? removeVisualInformation(petriNetDefinition)
: petriNetDefinition;

// Convert SDCPN to JSON string
const jsonString = JSON.stringify({ title, ...sdcpnToExport }, null, 2);
const payload = {
...sdcpnToExport,
version: SDCPN_FILE_FORMAT_VERSION,
meta: {
generator: "Petrinaut",
},
title,
};

const jsonString = JSON.stringify(payload, null, 2);

// Create a blob from the JSON string
const blob = new Blob([jsonString], { type: "application/json" });

// Create a download link
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${title.replace(/[^a-z0-9]/gi, "_").toLowerCase()}_${new Date().toISOString().replace(/:/g, "-")}.json`;

// Trigger download
document.body.appendChild(link);
link.click();

// Cleanup
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
Loading
Loading