diff --git a/app/javascript/src/admin/micro-clusters/Spinner.jsx b/app/javascript/src/admin/components/Spinner.jsx similarity index 100% rename from app/javascript/src/admin/micro-clusters/Spinner.jsx rename to app/javascript/src/admin/components/Spinner.jsx diff --git a/app/javascript/src/admin/micro-clusters/actions.js b/app/javascript/src/admin/components/clustering/actions.js similarity index 100% rename from app/javascript/src/admin/micro-clusters/actions.js rename to app/javascript/src/admin/components/clustering/actions.js diff --git a/app/javascript/src/admin/micro-clusters/keyDownListener.jsx b/app/javascript/src/admin/components/clustering/keyDownListener.jsx similarity index 100% rename from app/javascript/src/admin/micro-clusters/keyDownListener.jsx rename to app/javascript/src/admin/components/clustering/keyDownListener.jsx diff --git a/app/javascript/src/admin/micro-clusters/reducer.js b/app/javascript/src/admin/components/clustering/reducer.js similarity index 100% rename from app/javascript/src/admin/micro-clusters/reducer.js rename to app/javascript/src/admin/components/clustering/reducer.js diff --git a/app/javascript/src/admin/graphs/BotSignUps.jsx b/app/javascript/src/admin/graphs/BotSignUps.jsx index 06efbaa8c..135c73635 100644 --- a/app/javascript/src/admin/graphs/BotSignUps.jsx +++ b/app/javascript/src/admin/graphs/BotSignUps.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { Spinner } from "../micro-clusters/Spinner"; +import { Spinner } from "../components/Spinner"; export const BotSignUps = () => { const [data, setData] = useState(null); diff --git a/app/javascript/src/admin/graphs/CollectedInks.jsx b/app/javascript/src/admin/graphs/CollectedInks.jsx index 622de1c6b..e1dd87e8f 100644 --- a/app/javascript/src/admin/graphs/CollectedInks.jsx +++ b/app/javascript/src/admin/graphs/CollectedInks.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { Spinner } from "../micro-clusters/Spinner"; +import { Spinner } from "../components/Spinner"; export const CollectedInks = () => { const [data, setData] = useState(null); diff --git a/app/javascript/src/admin/graphs/CollectedPens.jsx b/app/javascript/src/admin/graphs/CollectedPens.jsx index 88efc090b..12167b483 100644 --- a/app/javascript/src/admin/graphs/CollectedPens.jsx +++ b/app/javascript/src/admin/graphs/CollectedPens.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { Spinner } from "../micro-clusters/Spinner"; +import { Spinner } from "../components/Spinner"; export const CollectedPens = () => { const [data, setData] = useState(null); diff --git a/app/javascript/src/admin/graphs/CurrentlyInked.jsx b/app/javascript/src/admin/graphs/CurrentlyInked.jsx index a9f402e13..2ef57abd6 100644 --- a/app/javascript/src/admin/graphs/CurrentlyInked.jsx +++ b/app/javascript/src/admin/graphs/CurrentlyInked.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { Spinner } from "../micro-clusters/Spinner"; +import { Spinner } from "../components/Spinner"; export const CurrentlyInked = () => { const [data, setData] = useState(null); diff --git a/app/javascript/src/admin/graphs/SignUps.jsx b/app/javascript/src/admin/graphs/SignUps.jsx index 960c15d2e..d3b3a9fc0 100644 --- a/app/javascript/src/admin/graphs/SignUps.jsx +++ b/app/javascript/src/admin/graphs/SignUps.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { Spinner } from "../micro-clusters/Spinner"; +import { Spinner } from "../components/Spinner"; export const SignUps = () => { const [data, setData] = useState(null); diff --git a/app/javascript/src/admin/graphs/UsageRecords.jsx b/app/javascript/src/admin/graphs/UsageRecords.jsx index f37911f8f..79749767b 100644 --- a/app/javascript/src/admin/graphs/UsageRecords.jsx +++ b/app/javascript/src/admin/graphs/UsageRecords.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { Spinner } from "../micro-clusters/Spinner"; +import { Spinner } from "../components/Spinner"; export const UsageRecords = () => { const [data, setData] = useState(null); diff --git a/app/javascript/src/admin/micro-clusters/App.jsx b/app/javascript/src/admin/micro-clusters/App.jsx index 517c6b55f..24d67d316 100644 --- a/app/javascript/src/admin/micro-clusters/App.jsx +++ b/app/javascript/src/admin/micro-clusters/App.jsx @@ -1,19 +1,13 @@ import React, { useEffect, useReducer, useContext } from "react"; import Select from "react-select"; import _ from "lodash"; -import Jsona from "jsona"; -import { getRequest } from "../../fetch"; -import { Spinner } from "./Spinner"; +import { Spinner } from "../components/Spinner"; import { DisplayMicroClusters } from "./DisplayMicroClusters"; -import { reducer, initalState } from "./reducer"; -import { - SET_LOADING_PERCENTAGE, - SET_MACRO_CLUSTERS, - SET_MICRO_CLUSTERS, - UPDATE_SELECTED_BRANDS -} from "./actions"; -import { setInBrandSelector } from "./keyDownListener"; +import { reducer, initalState } from "../components/clustering/reducer"; +import { UPDATE_SELECTED_BRANDS } from "../components/clustering/actions"; +import { setInBrandSelector } from "../components/clustering/keyDownListener"; +import { loadMacroClusters, loadMicroClusters } from "./loadClusters"; export const StateContext = React.createContext(); export const DispatchContext = React.createContext(); @@ -114,78 +108,3 @@ const BrandSelector = () => { ); }; -const loadMicroClusters = (dispatch) => { - const formatter = new Jsona(); - let data = []; - function run(page = 1) { - loadMicroClusterPage(page).then((json) => { - const next_page = json.meta.pagination.next_page; - // Remove clusters without collected inks - // Group collected inks - const pageData = formatter - .deserialize(json) - .filter((c) => c.collected_inks.length > 0) - .map((c) => { - const grouped_collected_inks = groupedInks(c.collected_inks); - return { ...c, grouped_collected_inks }; - }); - data = [...data, ...pageData]; - if (next_page) { - run(next_page); - } else { - dispatch({ type: SET_MICRO_CLUSTERS, payload: data }); - } - }); - } - run(); -}; - -const loadMicroClusterPage = (page) => { - return getRequest( - `/admins/micro_clusters.json?unassigned=true&without_ignored=true&page=${page}` - ).then((response) => response.json()); -}; - -const loadMacroClusters = (dispatch) => { - let data = []; - const formatter = new Jsona(); - function run(page = 1) { - loadMacroClusterPage(page).then((json) => { - const pagination = json.meta.pagination; - dispatch({ - type: SET_LOADING_PERCENTAGE, - payload: (pagination.current_page * 100) / pagination.total_pages - }); - const next_page = json.meta.pagination.next_page; - const pageData = formatter.deserialize(json).map((c) => { - const grouped_collected_inks = groupedInks( - c.micro_clusters.map((c) => c.collected_inks).flat() - ); - return { ...c, grouped_collected_inks }; - }); - data = [...data, ...pageData]; - if (next_page) { - run(next_page); - } else { - dispatch({ type: SET_MACRO_CLUSTERS, payload: data }); - } - }); - } - run(); -}; - -const loadMacroClusterPage = (page) => { - return getRequest(`/admins/macro_clusters.json?page=${page}`).then( - (response) => response.json() - ); -}; - -export const groupedInks = (collectedInks) => - _.values( - _.mapValues( - _.groupBy(collectedInks, (ci) => - ["brand_name", "line_name", "ink_name"].map((n) => ci[n]).join(",") - ), - (cis) => cis[0] - ) - ); diff --git a/app/javascript/src/admin/micro-clusters/CreateRow.jsx b/app/javascript/src/admin/micro-clusters/CreateRow.jsx index dc9868e80..3ca3e40ea 100644 --- a/app/javascript/src/admin/micro-clusters/CreateRow.jsx +++ b/app/javascript/src/admin/micro-clusters/CreateRow.jsx @@ -1,10 +1,15 @@ import React, { useCallback, useContext, useEffect } from "react"; import _ from "lodash"; import { postRequest, putRequest } from "../../fetch"; -import { StateContext, DispatchContext, groupedInks } from "./App"; -import { UPDATING, ADD_MACRO_CLUSTER, REMOVE_MICRO_CLUSTER } from "./actions"; +import { StateContext, DispatchContext } from "./App"; +import { groupedInks } from "./groupedInks"; +import { + UPDATING, + ADD_MACRO_CLUSTER, + REMOVE_MICRO_CLUSTER +} from "../components/clustering/actions"; import { assignCluster } from "./assignCluster"; -import { keyDownListener } from "./keyDownListener"; +import { keyDownListener } from "../components/clustering/keyDownListener"; export const CreateRow = ({ afterCreate }) => { const { updating, activeCluster } = useContext(StateContext); diff --git a/app/javascript/src/admin/micro-clusters/DisplayMacroClusters.jsx b/app/javascript/src/admin/micro-clusters/DisplayMacroClusters.jsx index fa42af901..dd4873ba7 100644 --- a/app/javascript/src/admin/micro-clusters/DisplayMacroClusters.jsx +++ b/app/javascript/src/admin/micro-clusters/DisplayMacroClusters.jsx @@ -13,8 +13,11 @@ import { NEXT_MACRO_CLUSTER, PREVIOUS_MACRO_CLUSTER, UPDATING -} from "./actions"; -import { keyDownListener, setInBrandSelector } from "./keyDownListener"; +} from "../components/clustering/actions"; +import { + keyDownListener, + setInBrandSelector +} from "../components/clustering/keyDownListener"; import { useCallback } from "react"; export const DisplayMacroClusters = ({ afterAssign }) => { diff --git a/app/javascript/src/admin/micro-clusters/DisplayMicroClusters.jsx b/app/javascript/src/admin/micro-clusters/DisplayMicroClusters.jsx index 4bd180a87..64a5bc8ec 100644 --- a/app/javascript/src/admin/micro-clusters/DisplayMicroClusters.jsx +++ b/app/javascript/src/admin/micro-clusters/DisplayMicroClusters.jsx @@ -3,14 +3,15 @@ import Jsona from "jsona"; import { getRequest } from "../../fetch"; import { DisplayMicroCluster } from "./DisplayMicroCluster"; -import { DispatchContext, groupedInks, StateContext } from "./App"; +import { DispatchContext, StateContext } from "./App"; +import { groupedInks } from "./groupedInks"; import { PREVIOUS, NEXT, REMOVE_MICRO_CLUSTER, UPDATE_MACRO_CLUSTER -} from "./actions"; -import { keyDownListener } from "./keyDownListener"; +} from "../components/clustering/actions"; +import { keyDownListener } from "../components/clustering/keyDownListener"; import { useCallback } from "react"; export const DisplayMicroClusters = () => { diff --git a/app/javascript/src/admin/micro-clusters/groupedInks.js b/app/javascript/src/admin/micro-clusters/groupedInks.js new file mode 100644 index 000000000..b9ecf55ac --- /dev/null +++ b/app/javascript/src/admin/micro-clusters/groupedInks.js @@ -0,0 +1,11 @@ +import _ from "lodash"; + +export const groupedInks = (collectedInks) => + _.values( + _.mapValues( + _.groupBy(collectedInks, (ci) => + ["brand_name", "line_name", "ink_name"].map((n) => ci[n]).join(",") + ), + (cis) => cis[0] + ) + ); diff --git a/app/javascript/src/admin/micro-clusters/groupedInks.spec.js b/app/javascript/src/admin/micro-clusters/groupedInks.spec.js new file mode 100644 index 000000000..578437657 --- /dev/null +++ b/app/javascript/src/admin/micro-clusters/groupedInks.spec.js @@ -0,0 +1,15 @@ +import { groupedInks } from "./groupedInks"; + +describe("groupedInks", () => { + it("returns only unique inks", () => { + const inks = [ + { brand_name: "brand1", line_name: "line1", ink_name: "ink1" }, + { brand_name: "brand1", line_name: "line1", ink_name: "ink1" }, + { brand_name: "brand2", line_name: "line1", ink_name: "ink1" } + ]; + expect(groupedInks(inks)).toStrictEqual([ + { brand_name: "brand1", line_name: "line1", ink_name: "ink1" }, + { brand_name: "brand2", line_name: "line1", ink_name: "ink1" } + ]); + }); +}); diff --git a/app/javascript/src/admin/micro-clusters/loadClusters.js b/app/javascript/src/admin/micro-clusters/loadClusters.js new file mode 100644 index 000000000..e22a716f5 --- /dev/null +++ b/app/javascript/src/admin/micro-clusters/loadClusters.js @@ -0,0 +1,75 @@ +import Jsona from "jsona"; + +import { getRequest } from "../../fetch"; +import { + SET_LOADING_PERCENTAGE, + SET_MACRO_CLUSTERS, + SET_MICRO_CLUSTERS +} from "../components/clustering/actions"; +import { groupedInks } from "./groupedInks"; + +export const loadMicroClusters = (dispatch) => { + const formatter = new Jsona(); + let data = []; + function run(page = 1) { + loadMicroClusterPage(page).then((json) => { + const next_page = json.meta.pagination.next_page; + // Remove clusters without collected inks + // Group collected inks + const pageData = formatter + .deserialize(json) + .filter((c) => c.collected_inks.length > 0) + .map((c) => { + const grouped_collected_inks = groupedInks(c.collected_inks); + return { ...c, grouped_collected_inks }; + }); + data = [...data, ...pageData]; + if (next_page) { + run(next_page); + } else { + dispatch({ type: SET_MICRO_CLUSTERS, payload: data }); + } + }); + } + run(); +}; + +const loadMicroClusterPage = (page) => { + return getRequest( + `/admins/micro_clusters.json?unassigned=true&without_ignored=true&page=${page}` + ).then((response) => response.json()); +}; + +export const loadMacroClusters = (dispatch) => { + let data = []; + const formatter = new Jsona(); + function run(page = 1) { + loadMacroClusterPage(page).then((json) => { + const pagination = json.meta.pagination; + dispatch({ + type: SET_LOADING_PERCENTAGE, + payload: (pagination.current_page * 100) / pagination.total_pages + }); + const next_page = json.meta.pagination.next_page; + const pageData = formatter.deserialize(json).map((c) => { + const grouped_collected_inks = groupedInks( + c.micro_clusters.map((c) => c.collected_inks).flat() + ); + return { ...c, grouped_collected_inks }; + }); + data = [...data, ...pageData]; + if (next_page) { + run(next_page); + } else { + dispatch({ type: SET_MACRO_CLUSTERS, payload: data }); + } + }); + } + run(); +}; + +const loadMacroClusterPage = (page) => { + return getRequest(`/admins/macro_clusters.json?page=${page}`).then( + (response) => response.json() + ); +}; diff --git a/app/javascript/src/admin/micro-clusters/loadClusters.spec.js b/app/javascript/src/admin/micro-clusters/loadClusters.spec.js new file mode 100644 index 000000000..4b7a0b9bc --- /dev/null +++ b/app/javascript/src/admin/micro-clusters/loadClusters.spec.js @@ -0,0 +1,210 @@ +import { loadMicroClusters, loadMacroClusters } from "./loadClusters"; + +import { rest } from "msw"; +import { setupServer } from "msw/node"; + +describe("loadMicroClusters", () => { + const server = setupServer( + rest.get("/admins/micro_clusters.json", (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: "1636", + type: "micro_cluster", + attributes: { + simplified_brand_name: "MyText", + simplified_line_name: "MyText", + simplified_ink_name: "MyText1" + }, + relationships: { + macro_cluster: { data: null }, + collected_inks: { + data: [ + { id: "10495", type: "collected_ink" }, + { id: "10496", type: "collected_ink" } + ] + } + } + } + ], + included: [ + { + id: "10495", + type: "collected_ink", + attributes: { + brand_name: "Diamine", + line_name: "", + ink_name: "Marine", + maker: "", + color: "#40E0D0" + }, + relationships: { + micro_cluster: { data: { id: "1636", type: "micro_cluster" } } + } + }, + { + id: "10496", + type: "collected_ink", + attributes: { + brand_name: "Diamine", + line_name: "", + ink_name: "Marine", + maker: "", + color: "#40E0D0" + }, + relationships: { + micro_cluster: { data: { id: "1636", type: "micro_cluster" } } + } + } + ], + meta: { + pagination: { + total_pages: 1, + current_page: 1, + next_page: null, + prev_page: null + } + } + }) + ) + ) + ); + + beforeAll(() => { + server.listen(); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => server.close()); + + it("loads and transforms the cluster data", (done) => { + const dispatch = jest.fn(); + loadMicroClusters(dispatch); + setTimeout(() => { + expect(dispatch).toHaveBeenCalled(); + const args = dispatch.mock.calls[0]; + expect(args).toHaveLength(1); + const arg = args[0]; + expect(arg.type).toBe("SET_MICRO_CLUSTERS"); + const payload = arg.payload; + expect(payload).toHaveLength(1); + const microCluster = payload[0]; + expect(microCluster.collected_inks).toHaveLength(2); + expect(microCluster.grouped_collected_inks).toHaveLength(1); + done(); + }, 500); + }); +}); + +describe("loadMacroClusters", () => { + const server = setupServer( + rest.get("/admins/macro_clusters.json", (req, res, ctx) => + res( + ctx.json({ + data: [ + { + id: "4332", + type: "macro_cluster", + attributes: { + brand_name: "brand_name", + line_name: "line_name", + ink_name: "ink_name_1", + color: "#FFFFFF" + }, + relationships: { + micro_clusters: { + data: [{ id: "1637", type: "micro_cluster" }] + } + } + } + ], + included: [ + { + id: "10497", + type: "collected_ink", + attributes: { + brand_name: "Diamine", + line_name: "", + ink_name: "Marine 1", + maker: "", + color: "#40E0D0" + }, + relationships: { + micro_cluster: { data: { id: "1637", type: "micro_cluster" } } + } + }, + { + id: "10498", + type: "collected_ink", + attributes: { + brand_name: "Diamine", + line_name: "", + ink_name: "Marine 2", + maker: "", + color: "#40E0D0" + }, + relationships: { + micro_cluster: { data: { id: "1637", type: "micro_cluster" } } + } + }, + + { + id: "1637", + type: "micro_cluster", + attributes: {}, + relationships: { + collected_inks: { + data: [ + { id: "10497", type: "collected_ink" }, + { id: "10498", type: "collected_ink" } + ] + }, + macro_cluster: { data: { id: "4332", type: "macro_cluster" } } + } + } + ], + meta: { + pagination: { + total_pages: 1, + current_page: 1, + next_page: null, + prev_page: null + } + } + }) + ) + ) + ); + + beforeAll(() => { + server.listen(); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => server.close()); + + it("loads and transforms the cluster data", (done) => { + const dispatch = jest.fn(); + loadMacroClusters(dispatch); + setTimeout(() => { + expect(dispatch.mock.calls).toHaveLength(2); + const firstCall = dispatch.mock.calls[0][0]; + expect(firstCall.type).toBe("SET_LOADING_PERCENTAGE"); + const secondCall = dispatch.mock.calls[1][0]; + expect(secondCall.type).toBe("SET_MACRO_CLUSTERS"); + const payload = secondCall.payload; + expect(payload).toHaveLength(1); + const macroCluster = payload[0]; + expect(macroCluster.micro_clusters).toHaveLength(1); + expect(macroCluster.grouped_collected_inks).toHaveLength(2); + done(); + }, 500); + }); +});