Skip to content

Commit 8231375

Browse files
committedMar 3, 2025·
Allow bulk enabling of all variants
1 parent 8cee468 commit 8231375

File tree

9 files changed

+128
-78
lines changed

9 files changed

+128
-78
lines changed
 

‎.vscode/settings.json

+6
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
{
2+
"editor.formatOnSave": false,
3+
"editor.codeActionsOnSave": {
4+
"source.fixAll.eslint": "explicit"
5+
},
6+
"editor.detectIndentation": false,
7+
"editor.tabSize": 2,
28
}

‎web/api-modules/products/controllers/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Router } from 'express';
22
import getProducts from './get-products.js';
3-
import toggleFdcVariantStatus from './toggle-fdc-variant-status.js';
3+
import { toggleFdcVariantStatus, changeFdcStatus } from './toggle-fdc-variant-status.js';
44
import addFdcVariant from './add-fdc-variant.js';
55
import updateFdcVariant from './update-fdc-variant.js';
66
import deleteFdcVariant from './delete-fdc-variant.js';
77

88
const products = Router();
99

1010
products.get('/', getProducts);
11+
products.post('/:id/fdcStatus', changeFdcStatus);
1112
products.post('/:id/variant/:variantId/toggleFdcStatus', toggleFdcVariantStatus);
1213
products.put('/:id/variant', addFdcVariant);
1314
products.post('/:id/variant/:variantId', updateFdcVariant);

‎web/api-modules/products/controllers/toggle-fdc-variant-status.js

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { toggleVariantMappingStatus } from '../../../database/variants/variants.js'
1+
import { toggleVariantMappingStatus, setAllVariantMappingStatuses } from '../../../database/variants/variants.js';
22

3-
const toggleFdcVariantStatus = async (req, res) => {
3+
export const toggleFdcVariantStatus = async (req, res) => {
44
try {
55
const { variantId } = req.params;
66

@@ -15,4 +15,18 @@ const toggleFdcVariantStatus = async (req, res) => {
1515
}
1616
};
1717

18-
export default toggleFdcVariantStatus;
18+
export const changeFdcStatus = async (req, res) => {
19+
try {
20+
const { id } = req.params;
21+
const { enabled, variants } = req.body;
22+
23+
const updatedVariantMapping = await setAllVariantMappingStatuses(id, variants, enabled);
24+
25+
return res.status(200).json(updatedVariantMapping);
26+
} catch (error) {
27+
console.error('Error updating product', error);
28+
return res.status(500).json({
29+
error: 'Error updating product'
30+
});
31+
}
32+
};

‎web/connector/index.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
22
Connector,
33
} from '@datafoodconsortium/connector';
4-
import facets from './thesaurus/facets.json' assert { type: 'json' };
5-
import measures from './thesaurus/measures.json' assert { type: 'json' };
6-
import productTypes from './thesaurus/productTypes.json' assert { type: 'json' };
7-
import vocabulary from './thesaurus/vocabulary.json' assert { type: 'json' };
4+
import facets from './thesaurus/facets.json' with { type: 'json' };
5+
import measures from './thesaurus/measures.json' with { type: 'json' };
6+
import productTypes from './thesaurus/productTypes.json' with { type: 'json' };
7+
import vocabulary from './thesaurus/vocabulary.json' with { type: 'json' };
88
import { throwError } from '../utils/index.js';
99

1010
let _connector;

‎web/database/migrations.sql

+3
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ ALTER TABLE fdc_variants
4040
ALTER COLUMN wholesale_variant_id DROP NOT NULL,
4141
ALTER COLUMN no_of_items_per_package DROP NOT NULL,
4242
ADD COLUMN "enabled" boolean NOT NULL DEFAULT false;
43+
44+
45+
create unique index idx_product_variant on fdc_variants (product_id, retail_variant_id);

‎web/database/variants/variants.js

+28-15
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,78 @@
11
import { query } from '../connect.js';
22

33
async function getVariants() {
4-
return (await query(`SELECT * FROM fdc_variants order by product_id`)).rows;
4+
return (await query('SELECT * FROM fdc_variants order by product_id')).rows;
55
}
66

77
async function getVariantsByProductId(productId) {
88
return (
9-
await query(`SELECT * FROM fdc_variants where product_id = $1`, [productId])
9+
await query('SELECT * FROM fdc_variants where product_id = $1', [productId])
1010
).rows;
1111
}
1212

1313
async function variantCount() {
1414
return Number(
15-
(await query(`SELECT count(*) FROM fdc_variants`)).rows[0].count
15+
(await query('SELECT count(*) FROM fdc_variants')).rows[0].count
1616
);
1717
}
1818

19-
async function addVariant({productId, retailVariantId, wholesaleVariantId, noOfItemsPerPackage, enabled = false}){
19+
async function addVariant({
20+
productId, retailVariantId, wholesaleVariantId, noOfItemsPerPackage, enabled = false
21+
}) {
2022
return (await query(
2123
'INSERT INTO fdc_variants (product_id, wholesale_variant_id, retail_variant_id, no_of_items_per_package, enabled) VALUES ($1,$2,$3,$4,$5) RETURNING *',
2224
[
2325
productId,
2426
wholesaleVariantId,
2527
retailVariantId,
2628
noOfItemsPerPackage,
27-
enabled,
28-
],
29-
))?.rows[0]
29+
enabled
30+
]
31+
))?.rows[0];
3032
}
3133

32-
async function updateVariant(variantId, {retailVariantId, wholesaleVariantId, noOfItemsPerPackage}){
34+
async function updateVariant(variantId, { retailVariantId, wholesaleVariantId, noOfItemsPerPackage }) {
3335
return (await query(
3436
'UPDATE fdc_variants SET wholesale_variant_id = $2, retail_variant_id = $3, no_of_items_per_package = $4 WHERE id = $1 RETURNING *',
3537
[
3638
variantId,
3739
wholesaleVariantId,
3840
retailVariantId,
39-
noOfItemsPerPackage,
40-
],
41-
))?.rows[0]
41+
noOfItemsPerPackage
42+
]
43+
))?.rows[0];
4244
}
4345

4446
async function toggleVariantMappingStatus(variantId) {
45-
return (await query(`UPDATE fdc_variants SET enabled = NOT enabled WHERE id = $1 RETURNING *`, [variantId]))?.rows[0];
47+
return (await query('UPDATE fdc_variants SET enabled = NOT enabled WHERE id = $1 RETURNING *', [variantId]))?.rows[0];
48+
}
49+
50+
async function setAllVariantMappingStatuses(productId, variants, status) {
51+
return (await query(`INSERT into fdc_variants (product_id, retail_variant_id, enabled)
52+
(SELECT * FROM json_to_recordset($1)
53+
AS x("productId" bigint, "variantId" bigint, "status" boolean))
54+
on CONFLICT(product_id, retail_variant_id)
55+
DO UPDATE SET
56+
enabled = EXCLUDED.enabled
57+
RETURNING *;`, [JSON.stringify(variants.map((variantId) => ({ productId, variantId, status })))]))?.rows;
4658
}
4759

4860
async function deleteVariant(variantId) {
49-
return (await query(`DELETE from fdc_variants WHERE id = $1`, [variantId]))?.rows[0];
61+
return (await query('DELETE from fdc_variants WHERE id = $1', [variantId]))?.rows[0];
5062
}
5163

5264
async function getPagedVariants(lastId, limit) {
5365
return (
5466
await query(
55-
`SELECT * FROM fdc_variants where product_id > $1 order by product_id limit $2`,
67+
'SELECT * FROM fdc_variants where product_id > $1 order by product_id limit $2',
5668
[lastId, limit]
5769
)
5870
).rows;
5971
}
6072

6173
function indexedByProductId(variants) {
6274
return variants.reduce((accumulator, row) => {
63-
const productId = row.productId;
75+
const { productId } = row;
6476
return {
6577
...accumulator,
6678
[productId]: accumulator[productId]
@@ -93,6 +105,7 @@ export {
93105
combineFdcProductsWithTheirFdcConfiguration,
94106
addFdcConfigurationToFdcProducts as addVariantsToProducts,
95107
toggleVariantMappingStatus,
108+
setAllVariantMappingStatuses,
96109
addVariant,
97110
updateVariant,
98111
deleteVariant

‎web/frontend/components/ProductCard.jsx

+61-50
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,35 @@ import { ExpandMoreIcon } from './ExpandMoreIcon';
1414
import { VariantMappingComponent } from './VariantMapping';
1515
import { VariantComponent } from './Variant';
1616

17-
export function ProductCard({ product, variantMappingEnabled }) {
17+
export default function ProductCard({ product, variantMappingEnabled }) {
1818
const queryClient = useQueryClient();
1919

2020
function updateOrReplace(fdcVariants, updatedVariant, variables) {
21-
const method = variables.fetchInit.method;
21+
const { method } = variables.fetchInit;
2222
if (method === 'PUT') {
2323
return [...fdcVariants, updatedVariant];
24-
} else if (method === 'POST') {
25-
return fdcVariants.map(variant => {
26-
return variant.id === updatedVariant.id ? updatedVariant : variant;
27-
})
28-
} else if (method === 'DELETE') {
29-
return fdcVariants.filter(variant => variant.id !== variables.variantId);
30-
} else {
31-
throw new Error(`dont know how to handle ${method$}`);
24+
} if (method === 'POST' && Array.isArray(updatedVariant)) {
25+
return updatedVariant;
26+
} if (method === 'POST') {
27+
return fdcVariants.map((variant) => (variant.id === updatedVariant.id ? updatedVariant : variant));
28+
} if (method === 'DELETE') {
29+
return fdcVariants.filter((variant) => variant.id !== variables.variantId);
3230
}
31+
throw new Error(`dont know how to handle ${method}`);
3332
}
3433

35-
const { mutateAsync: mutateMapping, isFetching: productsLoading } = useAppMutation({
34+
const { mutateAsync: mutateMapping, isLoading, isFetching: productsLoading } = useAppMutation({
3635
reactQueryOptions: {
3736
onSuccess: (updatedVariant, variables) => {
3837
queryClient.setQueryData('/api/products', (query) => {
39-
const updatedProducts = query?.products?.map(existingProduct => {
38+
const updatedProducts = query?.products?.map((existingProduct) => {
4039
if (existingProduct.id === product.id) {
4140
return {
4241
...existingProduct,
4342
fdcVariants: updateOrReplace(existingProduct.fdcVariants, updatedVariant, variables)
44-
}
45-
} else {
46-
return existingProduct;
43+
};
4744
}
45+
return existingProduct;
4846
});
4947

5048
return {
@@ -56,70 +54,83 @@ export function ProductCard({ product, variantMappingEnabled }) {
5654
}
5755
});
5856

59-
const {
60-
mutateAsync: updateVariantMappings,
61-
status: variantMappingUpdateStatus,
62-
isFetching: variantMappingsBeingUpdated
63-
} = useAppMutation({
64-
reactQueryOptions: {
65-
onSuccess: async () => {
66-
await queryClient.invalidateQueries('/api/products');
67-
}
68-
}
69-
});
70-
7157
const isFdcProduct = !!product.fdcVariants.find(({ enabled }) => enabled);
7258
const hasVariantMapped = !!product.fdcVariants[0];
59+
const allVariantsEnabled = product.fdcVariants.length > 0 &&
60+
product.variants.length === product.fdcVariants.filter(({ enabled }) => enabled).length;
61+
62+
const colour = hasVariantMapped && isFdcProduct ? 'green' : isFdcProduct ? 'red' : 'gray';
63+
64+
const toggleAllVariants = async () => {
65+
await mutateMapping({
66+
url: `/api/products/${product.id}/fdcStatus`,
67+
fetchInit: {
68+
method: 'POST',
69+
headers: {
70+
'Content-Type': 'application/json'
71+
},
72+
body: JSON.stringify({ enabled: !allVariantsEnabled, variants: product.variants.map(({ id }) => id) })
73+
}
74+
75+
});
76+
};
7377

7478
return (
7579
<Accordion key={product.id} slotProps={{ transition: { unmountOnExit: true } }}>
7680
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
7781
<Stack direction="row" justifyContent="space-between" width="100%">
78-
<Typography variant="h6">{product.title}</Typography>
82+
<Typography variant="h6">
83+
{product.title}
84+
{' '}
85+
-
86+
{' '}
87+
<span style={{ color: colour, fontSize: 15, verticalAlign: 'text-top' }}>{isFdcProduct ? 'Has FDC Enabled Variants' : 'Not FDC Product'}</span>
88+
</Typography>
7989
<Stack spacing="20px" direction="row" alignItems="center">
90+
{!variantMappingEnabled && (
8091
<FormControlLabel
81-
style={{ pointerEvents: "none" }}
82-
control={
92+
style={{ pointerEvents: 'none' }}
93+
control={(
8394
<Checkbox
8495
style={{
8596
width: '50px',
8697
pointerEvents: 'auto'
8798
}}
88-
disabled={true}
89-
checked={isFdcProduct}
99+
disabled={productsLoading || isLoading}
100+
onClick={toggleAllVariants}
101+
checked={allVariantsEnabled}
90102
/>
91-
}
92-
sx={{
93-
'& .MuiFormControlLabel-label': {
94-
color: hasVariantMapped && isFdcProduct ? 'green !important' : isFdcProduct ? 'red !important' : 'gray'
95-
}
96-
}}
97-
label={isFdcProduct ? 'Has FDC Enabled Variants' : 'Not FDC Product'}
103+
)}
104+
label="Toggle FDC status"
98105
labelPlacement="start"
99106
/>
107+
)}
100108
</Stack>
101109

102110
</Stack>
103111
</AccordionSummary>
104112
<AccordionDetails>
105113
<Stack spacing="12px">
106-
{variantMappingEnabled ?
114+
{variantMappingEnabled ? (
107115
<VariantMappingComponent
108-
key={product.id + '_variant' + (product.fdcVariants.length ? '' : '_missing')}
116+
key={`${product.id}_variant${product.fdcVariants.length ? '' : '_missing'}`}
109117
mutateMapping={mutateMapping}
110118
product={product}
111119
variant={product.fdcVariants[0]}
112-
loadingInProgress={variantMappingsBeingUpdated || variantMappingUpdateStatus === 'loading' || productsLoading}
120+
loadingInProgress={isLoading || productsLoading}
113121
/>
122+
)
114123
:
115-
product.variants.map((variant, i) =>
116-
(<VariantComponent
117-
key={`${product.id}_variant_${i}'`}
118-
product={product}
119-
variant={variant}
120-
fdcVariants={product.fdcVariants}
121-
mutateMapping={mutateMapping}
122-
/>))
124+
product.variants.map((variant) =>
125+
(
126+
<VariantComponent
127+
key={`${product.id}_variant_${variant.id}'`}
128+
product={product}
129+
variant={variant}
130+
fdcVariants={product.fdcVariants}
131+
mutateMapping={mutateMapping}
132+
/>
133+
))
123134
}
124135
</Stack>
125136
</AccordionDetails>

‎web/frontend/components/index.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export { ProductCard } from './ProductCard';
21
export * from './providers';

‎web/frontend/pages/index.jsx

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Button, Stack, TextField } from '@mui/material';
22
import { Loading } from '@shopify/app-bridge-react';
33
import { Card, SkeletonBodyText } from '@shopify/polaris';
44
import React, { useState } from 'react';
5-
import { ProductCard } from '../components/ProductCard';
5+
import ProductCard from '../components/ProductCard';
66
import { useAppQuery } from '../hooks';
77

88
export default function HomePage() {
@@ -21,8 +21,7 @@ export default function HomePage() {
2121
const matchesSearch =
2222
product.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
2323
product.variants?.some((variant) =>
24-
variant.title.toLowerCase().includes(searchQuery.toLowerCase())
25-
);
24+
variant.title.toLowerCase().includes(searchQuery.toLowerCase()));
2625
return matchesFDC && matchesSearch;
2726
});
2827

@@ -93,7 +92,11 @@ export default function HomePage() {
9392
) : (
9493
<Stack spacing="12px" px="60px" py="12px">
9594
{filteredProducts?.map((product) => (
96-
<ProductCard key={product.id} product={product} variantMappingEnabled={data?.variantMappingEnabled} />
95+
<ProductCard
96+
key={product.id}
97+
product={product}
98+
variantMappingEnabled={data?.variantMappingEnabled}
99+
/>
97100
))}
98101
</Stack>
99102
)}

0 commit comments

Comments
 (0)
Please sign in to comment.