Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fetch & flatten the latest published version of external portal data when publishing the parent flow #2783

Merged
merged 30 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a410c6a
update dataMerged in api
jessicamcinchak Feb 13, 2024
9308820
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 15, 2024
791089c
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 16, 2024
20697e3
add public get endpoints for flattened flow data that we can call on …
jessicamcinchak Feb 16, 2024
8db2924
delete dataMergedHotfix in editor, call api
jessicamcinchak Feb 16, 2024
a186785
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 19, 2024
80cebdc
add new /publish-preview route and bubble upaxios error msgs to publi…
jessicamcinchak Feb 19, 2024
7811c03
getFlowData should still return team_id
jessicamcinchak Feb 19, 2024
d547c6d
better error formatting on /publish-preview
jessicamcinchak Feb 20, 2024
10890ec
update dataMerged() tests
jessicamcinchak Feb 20, 2024
3d46fdb
add and update API tests, run prettier
jessicamcinchak Feb 20, 2024
42df2c6
better flattenFlow test coverage
jessicamcinchak Feb 20, 2024
26e779d
dataMerged() tests should share same mock data as flattenFlows()
jessicamcinchak Feb 20, 2024
2c04da8
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 23, 2024
616e6b2
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 26, 2024
9a3decb
pr feedback
jessicamcinchak Feb 26, 2024
5d7a3bd
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 27, 2024
9567f70
don't wrap publishedPreview route in SaveAndReturnLayout
jessicamcinchak Feb 27, 2024
25629bd
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 28, 2024
5c3b104
feat: simplified changelogs on publish (#2811)
jessicamcinchak Feb 28, 2024
76e1e9b
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
jessicamcinchak Feb 28, 2024
84d589a
Merge branch 'jess/fetch-published-portals-on-publish' of github.com:…
jessicamcinchak Feb 28, 2024
51bf520
rename frontend routes and views, not /preview yet
jessicamcinchak Feb 28, 2024
8d05bf7
rename API query param too
jessicamcinchak Feb 28, 2024
cf4143c
run prettier
jessicamcinchak Feb 28, 2024
7409749
minor doc updtae
jessicamcinchak Feb 28, 2024
67dda60
redirect /unpublished to /amber
jessicamcinchak Feb 29, 2024
e3cab8f
fix: if same portal is nested many times at different levels, ensure …
jessicamcinchak Mar 1, 2024
61a8f64
chore: map user ids to names on publish change dialog for nested flow…
jessicamcinchak Mar 4, 2024
e964a15
feat: update and redirect `/preview` to `/published` (#2854)
jessicamcinchak Mar 4, 2024
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
163 changes: 76 additions & 87 deletions api.planx.uk/helpers.test.ts
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import {
isLiveEnv,
} from "./helpers";
import { queryMock } from "./tests/graphqlQueryMock";
import { userContext } from "./modules/auth/middleware";
import { getJWT } from "./tests/mockJWT";
import {
childFlow,
draftParentFlow,
flattenedParentFlow,
} from "./tests/mocks/validateAndPublishMocks";

describe("getEnvironment function", () => {
describe("getFormattedEnvironment() function", () => {
const OLD_ENV = process.env;

beforeEach(() => {
Expand Down Expand Up @@ -69,121 +72,107 @@ describe("isLiveEnv() function", () => {
});

describe("dataMerged() function", () => {
const getStoreMock = jest.spyOn(userContext, "getStore");
getStoreMock.mockReturnValue({
user: {
sub: "123",
jwt: getJWT({ role: "teamEditor" }),
},
});

beforeEach(() => {
const unflattenedParent = {
_root: {
edges: ["Zj0ZKa0PwT", "Rur8iS88x3"],
},
"5yElH96W7I": {
data: {
text: "Option 2",
},
type: 200,
edges: ["aMlxwR7ONH"],
},
Rur8iS88x3: {
data: {
color: "#EFEFEF",
title: "End of the line",
resetButton: false,
},
type: 8,
},
SShTHaRo2k: {
data: {
flowId: "child-id",
},
type: 310,
},
Zj0ZKa0PwT: {
data: {
text: "This is a question with many options",
},
type: 100,
edges: ["c8hZwm0a9c", "5yElH96W7I", "UMsI68BuAy"],
},
c8hZwm0a9c: {
data: {
text: "Option 1",
},
type: 200,
edges: ["SShTHaRo2k"],
},
aMlxwR7ONH: {
type: 310,
data: {
flowId: "child-id",
},
queryMock.mockQuery({
name: "GetFlowData",
matchOnVariables: true,
variables: {
id: "parent-flow-with-external-portal",
},
UMsI68BuAy: {
type: 200,
data: {
text: "Option 3",
data: {
flow: {
data: draftParentFlow,
slug: "parent-flow",
team_id: 1,
team: {
slug: "testing",
},
publishedFlows: [{ data: flattenedParentFlow }],
},
},
};
});
});

const unflattenedChild = {
_root: {
edges: ["sbDyJVsyXg"],
it("flattens published external portal nodes by overwriting their type", async () => {
queryMock.mockQuery({
name: "GetFlowData",
matchOnVariables: true,
variables: {
id: "child-flow-id",
},
sbDyJVsyXg: {
type: 100,
data: {
description: "<p>Hello there 👋</p>",
text: "This is within the portal",
data: {
flow: {
data: childFlow,
slug: "child-flow",
team_id: 1,
team: {
slug: "testing",
},
publishedFlows: [{ data: childFlow }],
},
},
};
});

const result = await dataMerged("parent-flow-with-external-portal");
const nodeTypes = Object.values(result).map((node) =>
"type" in node ? node.type : undefined,
);

expect(nodeTypes.includes(ComponentType.ExternalPortal)).toBe(false);
});

it("throws an error when an external portal is not published", async () => {
queryMock.mockQuery({
name: "GetFlowData",
matchOnVariables: true,
variables: {
id: "child-id",
id: "child-flow-id",
},
data: {
flow: {
data: childFlow,
slug: "child-flow",
data: unflattenedChild,
team_id: 123,
team_id: 1,
team: {
slug: "testing",
},
publishedFlows: [],
},
},
});

await expect(
dataMerged("parent-flow-with-external-portal"),
).rejects.toThrow();
});

it("flattens any published or unpublished external portal nodes when isDraftData only is set to true", async () => {
queryMock.mockQuery({
name: "GetFlowData",
matchOnVariables: true,
variables: {
id: "parent-id",
id: "child-flow-id",
},
data: {
flow: {
slug: "parent-flow",
data: unflattenedParent,
team_id: 123,
data: childFlow,
slug: "child-flow",
team_id: 1,
team: {
slug: "testing",
},
publishedFlows: [],
},
},
});
});

it("handles multiple external portal nodes", async () => {
const result = await dataMerged("parent-id");
const nodeTypes = Object.values(result).map((node) =>
"type" in node ? node.type : undefined,
const result = await dataMerged(
"parent-flow-with-external-portal",
{},
false,
true,
);
const areAllPortalsFlattened = !nodeTypes.includes(
ComponentType.ExternalPortal,
);

// All external portals have been flattened / replaced
expect(areAllPortalsFlattened).toBe(true);
expect(result).toEqual(flattenedParentFlow);
});
});

Expand Down
57 changes: 45 additions & 12 deletions api.planx.uk/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,32 @@ import { Flow, Node } from "./types";
import { ComponentType, FlowGraph } from "@opensystemslab/planx-core/types";
import { $public, getClient } from "./client";

export interface FlowData {
slug: string;
data: Flow["data"];
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
team_id: number;
team: { slug: string };
publishedFlows: { data: Flow["data"] }[] | [];
}

// Get a flow's data (unflattened, without external portal nodes)
const getFlowData = async (id: string): Promise<Flow> => {
const { flow } = await $public.client.request<{ flow: Flow | null }>(
const getFlowData = async (id: string): Promise<FlowData> => {
const { flow } = await $public.client.request<{ flow: FlowData | null }>(
gql`
query GetFlowData($id: uuid!) {
flow: flows_by_pk(id: $id) {
slug
data
team_id
team {
slug
}
publishedFlows: published_flows(
limit: 1
order_by: { created_at: desc }
) {
data
}
}
}
`,
Expand Down Expand Up @@ -136,15 +153,32 @@ const getMostRecentPublishedFlow = async (
return mostRecent;
};

// Flatten a flow's data to include main content & portals in a single JSON representation
// XXX: getFlowData & dataMerged are currently repeated in ../editor.planx.uk/src/lib/dataMergedHotfix.ts
// in order to load frontend /preview routes for flows that are not published
/**
* Flatten a flow to create a single JSON representation of the main flow data and any external portals
* By default, requires that any external portals are published and flattens their latest published version
* When draftDataOnly = true, flattens the draft data of the main flow and the draft data of any external portals published or otherwise
*/
const dataMerged = async (
id: string,
ob: { [key: string]: Node } = {},
isPortal = false,
draftDataOnly = false,
): Promise<FlowGraph> => {
// get the primary flow data
const { slug, data } = await getFlowData(id);
// get the primary draft flow data, including its' latest published version
const response = await getFlowData(id);
const { slug, team, publishedFlows } = response;
let { data } = response;

// only flatten external portals that are published, unless we're loading draftDataOnly on an /unpublished route
if (isPortal && !draftDataOnly) {
if (publishedFlows?.[0]?.data) {
data = publishedFlows[0].data;
} else {
throw new Error(
`Publish flow ${team.slug}/${slug} before proceeding. All flows used as external portals must be published.`,
);
}
}

// recursively get and flatten internal portals & external portals
for (const [nodeId, node] of Object.entries(data)) {
Expand All @@ -153,7 +187,7 @@ const dataMerged = async (
const isExternalPortal = node.type === ComponentType.ExternalPortal;
const isMerged = ob[node.data?.flowId];

// Merge portal root as a new node in the graph
// merge external portal _root as a new node in the graph
if (isExternalPortalRoot) {
ob[id] = {
...node,
Expand All @@ -162,7 +196,7 @@ const dataMerged = async (
};
}

// Merge as internal portal, with reference to flowId
// merge external portal as an internal portal type node, with reference to flowId
else if (isExternalPortal) {
ob[nodeId] = {
type: ComponentType.InternalPortal,
Expand All @@ -171,15 +205,14 @@ const dataMerged = async (

// Recursively merge flow
if (!isMerged) {
await dataMerged(node.data?.flowId, ob);
await dataMerged(node.data?.flowId, ob, true, draftDataOnly);
}
}

// Merge all other nodes
// merge all other nodes
else ob[nodeId] = node;
}

// TODO: Don't cast here once types updated across API
return ob as FlowGraph;
};

Expand Down
3 changes: 1 addition & 2 deletions api.planx.uk/modules/flows/copyFlow/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { makeUniqueFlow, getFlowData, insertFlow } from "../../../helpers";
import { Flow } from "../../../types";
import { userContext } from "../../auth/middleware";

const copyFlow = async (
Expand All @@ -8,7 +7,7 @@ const copyFlow = async (
insert: boolean,
) => {
// Fetch the original flow
const flow: Flow = await getFlowData(flowId);
const flow = await getFlowData(flowId);

// Generate new flow data which is an exact "content" copy of the original but with unique nodeIds
const uniqueFlowData = makeUniqueFlow(flow.data, replaceValue);
Expand Down
28 changes: 25 additions & 3 deletions api.planx.uk/modules/flows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ components:
updatedFlow:
$ref: "#/components/schemas/FlowData"
required: false
FlattenFlow:
content:
application/json:
schema:
type: object
properties:
$ref: "#/components/schemas/FlowData"
paths:
/flows/{flowId}/copy:
post:
Expand Down Expand Up @@ -242,12 +249,10 @@ paths:
"500":
$ref: "#/components/responses/ErrorMessage"
/flows/{flowId}/download-schema:
post:
get:
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
summary: Download flow schema
description: Download a CSV file representing the flow's schema
tags: ["flows"]
security:
- bearerAuth: []
parameters:
- $ref: "#/components/parameters/flowId"
responses:
Expand All @@ -258,3 +263,20 @@ paths:
type: string
"500":
$ref: "#/components/responses/ErrorMessage"
/flows/{flowId}/flatten-flow:
get:
summary: Flatten a flow and its' external portals into a single graph
description: Flatten a flow and its' external portals into a single graph. The external portals must be published.
tags: ["flows"]
parameters:
- $ref: "#/components/parameters/flowId"
- in: query
name: unpublished
type: boolean
required: false
description: Optional param to flatten a flow and its' external portals in their draft state.
responses:
"200":
$ref: "#/components/responses/FlattenFlow"
"500":
$ref: "#/components/responses/ErrorMessage"
2 changes: 1 addition & 1 deletion api.planx.uk/modules/flows/downloadSchema/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type DownloadFlowSchemaController = ValidatedRequestHandler<
>;

export const downloadFlowSchemaController: DownloadFlowSchemaController =
async (_res, res, next) => {
async (_req, res, next) => {
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
try {
const { flowId } = res.locals.parsedReq.params;
const flowSchema = await getFlowSchema(flowId);
Expand Down
Loading
Loading