Skip to content

Commit

Permalink
feat(import-app): chunk mapping, rounding and do not group on nothing
Browse files Browse the repository at this point in the history
  • Loading branch information
Plopix committed Feb 13, 2024
1 parent f3dc05b commit ecf54e0
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export interface FormSubmission {
doPublish: boolean;
channel?: string;
validFlowStage?: string;
roundPrices?: boolean;
invalidFlowStage?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,73 @@ import {
type JSONImage,
type JSONProduct,
type JSONProductVariant,
JSONContentChunk,
} from '@crystallize/import-utilities';
import { v4 as uuidv4 } from 'uuid';
import type { NumericComponentConfig, Shape } from '@crystallize/schema';
import type { ContentChunkComponentConfig, NumericComponentConfig, Shape, ShapeComponent } from '@crystallize/schema';
import { FIELD_MAPPINGS } from '~/contracts/ui-types';
import { FormSubmission } from '~/contracts/form-submission';

const contentForComponent = (component: ShapeComponent, key: string, content: string): any => {
if (component.type === 'boolean') {
return !!content;
}

if (component.type === 'datetime') {
return new Date(content).toISOString();
}

if (component.type === 'gridRelations') {
return [...content.split(',').map((name) => ({ name }))];
}

if (component.type === 'images') {
return [...content.split(',').map((src) => ({ src }))];
}

if (component.type === 'itemRelations') {
return [...content.split(',').map((externalReference) => ({ externalReference }))];
}

if (component.type === 'location') {
return {
lat: Number.parseFloat(content.split(',')[0]),
long: Number.parseFloat(content.split(',')[1]),
};
}

if (component.type === 'numeric') {
const unit = (component.config as NumericComponentConfig).units?.[0];
return {
number: Number.parseFloat(content),
...(unit ? { unit } : {}),
};
}

if (component.type === 'paragraphCollection') {
return {
html: content,
};
}

if (component.type === 'singleLine' || component.type === 'richText') {
return content;
}

if (component.type === 'contentChunk') {
const keyParts = key.split('.');
const chunkId = keyParts[1];

const subcomp = (component.config as ContentChunkComponentConfig)?.components?.find((c) => c.id === chunkId);
return [
{
[chunkId]: contentForComponent(subcomp, keyParts.slice(1).join('.'), content),
},
];
}
throw new Error(`Component type "${component.type} is not yet supported for import"`);
};

const mapComponents = (
row: Record<string, any>,
mapping: Record<string, string>,
Expand All @@ -19,7 +80,8 @@ const mapComponents = (
return Object.entries(mapping)
.filter(([key]) => key.split('.')[0] === prefix)
.reduce((acc: Record<string, JSONComponentContent>, [key, value]) => {
const componentId = key.split('.')[1];
const keyParts = key.split('.');
const componentId = keyParts[1];
const component = shape[prefix]?.find((cmp) => cmp.id === componentId);
const content: string = row[value];

Expand All @@ -32,55 +94,40 @@ const mapComponents = (
return acc;
}

switch (component?.type) {
case 'boolean':
acc[componentId] = !!content;
break;
case 'datetime':
acc[componentId] = new Date(content).toISOString();
break;
case 'gridRelations':
acc[componentId] = [...content.split(',').map((name) => ({ name }))];
break;
case 'images':
acc[componentId] = [...content.split(',').map((src) => ({ src }))];
break;
case 'itemRelations':
acc[componentId] = [...content.split(',').map((externalReference) => ({ externalReference }))];
break;
case 'location':
acc[componentId] = {
lat: Number.parseFloat(content.split(',')[0]),
long: Number.parseFloat(content.split(',')[1]),
};
break;
case 'numeric':
acc[componentId] = {
number: Number.parseFloat(content),
unit: component.config ? (component.config as NumericComponentConfig).units?.[0] : undefined,
};
break;
case 'paragraphCollection':
acc[componentId] = {
html: content,
};
break;
case 'singleLine':
case 'richText':
acc[componentId] = content;
break;
default:
throw new Error(`Component type "${component.type} is not yet supported for import"`);
if (acc[componentId] && component.type === 'contentChunk') {
// that's normal, we can have multiple content chunks
// but the import will only fill the 1st one
const existingChunks = acc[componentId] as JSONContentChunk;
const existingChunkEntries = existingChunks[0];
const newChunkEntries = contentForComponent(component, keyParts.slice(1).join('.'), content)[0];
acc[componentId] = [
{
...existingChunkEntries,
...newChunkEntries,
},
];
return acc;
}

acc[componentId] = contentForComponent(component, keyParts.slice(1).join('.'), content);

return acc;
}, {});
};

const mapVariant = (row: Record<string, any>, mapping: Record<string, string>, shape: Shape): JSONProductVariant => {
type MapVariantOptions = {
roundPrices?: boolean;
};
const mapVariant = (
row: Record<string, any>,
mapping: Record<string, string>,
shape: Shape,
options?: MapVariantOptions,
): JSONProductVariant => {
const name = row[mapping[FIELD_MAPPINGS.item.name.key]];
const sku = row[mapping['variant.sku']];
const images = row[mapping['variant.images']];
const price = row[mapping['variant.price']] ? Number.parseFloat(row[mapping['variant.price']]) : undefined;
let price = row[mapping['variant.price']] ? Number.parseFloat(row[mapping['variant.price']]) : undefined;
const stock = row[mapping['variant.stock']] ? Number.parseFloat(row[mapping['variant.stock']]) : undefined;
const externalReference = row[mapping[FIELD_MAPPINGS.productVariant.externalReference.key]];

Expand All @@ -91,6 +138,9 @@ const mapVariant = (row: Record<string, any>, mapping: Record<string, string>, s
return acc;
}, {});

if (options?.roundPrices && price) {
price = Math.round(price * 100) / 100;
}
const variant: JSONProductVariant = {
name,
sku,
Expand All @@ -113,7 +163,7 @@ const mapVariant = (row: Record<string, any>, mapping: Record<string, string>, s
};

export const specFromFormSubmission = async (submission: FormSubmission, shapes: Shape[]) => {
const { shapeIdentifier, folderPath, rows, mapping, groupProductsBy, subFolderMapping } = submission;
const { shapeIdentifier, folderPath, rows, mapping, groupProductsBy, subFolderMapping, roundPrices } = submission;

const shape = shapes.find((s) => s.identifier === shapeIdentifier);
if (!shape) {
Expand Down Expand Up @@ -164,7 +214,11 @@ export const specFromFormSubmission = async (submission: FormSubmission, shapes:
};

if (shape.type === 'product') {
const variants = rows.map((row) => mapVariant(row, mapping, shape));
const variants = rows.map((row) =>
mapVariant(row, mapping, shape, {
roundPrices: !!roundPrices,
}),
);
const mapProduct = (obj: Record<string, JSONProduct>, row: Record<string, any>, i: number) => {
const productName = row[mapping['item.name']];
let product: JSONProduct = {
Expand All @@ -176,7 +230,7 @@ export const specFromFormSubmission = async (submission: FormSubmission, shapes:
...findParent(row),
};

if (groupProductsBy) {
if (groupProductsBy && row[groupProductsBy]) {
if (obj[row[groupProductsBy]]) {
product = obj[row[groupProductsBy]];
product.variants = product.variants.concat(variants[i]);
Expand All @@ -185,7 +239,6 @@ export const specFromFormSubmission = async (submission: FormSubmission, shapes:
} else {
obj[uuidv4()] = product;
}

return obj;
};
items.push(...Object.values(rows.reduce(mapProduct, {})));
Expand Down
1 change: 1 addition & 0 deletions components/crystallize-import/src/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function Index() {
shapes,
folders,
flows,
subFolderMapping: [],
selectedShape: shapes[0],
selectedFolder: folders[0],
headers: [],
Expand Down
2 changes: 1 addition & 1 deletion components/crystallize-import/src/routes/api.preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ type Results = {
errors: any[];
}[];
};

export const action = async ({ request }: ActionFunctionArgs) => {
const api = await CrystallizeAPI(request);
const [validationRules, shapes] = await Promise.all([api.fetchValidationsSchema(), api.fetchShapes()]);
try {
const submission: FormSubmission = await request.json();
const spec = await specFromFormSubmission(submission, shapes);
const items = spec.items ?? [];

const results = items.reduce(
(memo: Results, item) => {
const validate = validationRules?.[item.shape]?.validate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const Submit = () => {
: `No rows to import`;

const publishRef = useRef<HTMLInputElement>(null);
const roundRef = useRef<HTMLInputElement>(null);
const validFlowRef = useRef<HTMLSelectElement>(null);
const invalidFlowRef = useRef<HTMLSelectElement>(null);
return (
Expand All @@ -55,6 +56,7 @@ export const Submit = () => {
subFolderMapping: state.subFolderMapping,
validFlowStage: validFlowRef.current?.value ?? undefined,
invalidFlowStage: invalidFlowRef.current?.value ?? undefined,
roundPrices: roundRef.current?.checked ?? false,
};
const res = await fetch('/api/submit', {
method: 'POST',
Expand Down Expand Up @@ -89,6 +91,10 @@ export const Submit = () => {
<input type="checkbox" ref={publishRef} />
Publish
</label>
<label>
<input type="checkbox" ref={roundRef} />
Round Price (2 decimals)
</label>
{state.flows.length > 0 && (
<>
<label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BsTrashFill } from 'react-icons/bs';
import { useImport } from '../../provider';
import { useState } from 'react';
import { FieldMapping, FIELD_MAPPINGS } from '../../../../contracts/ui-types';
import { ComponentChoiceComponentConfig, Shape } from '@crystallize/schema';

interface ColumnMapperProps {
title: string;
Expand Down Expand Up @@ -54,24 +55,29 @@ export const ColumnHeader = ({ title }: ColumnHeaderProps) => {
),
);
}

state.selectedShape.components?.map(({ id, name, type }) =>
shapeFields.push({
key: `components.${id}`,
description: name,
type,
}),
);
state.selectedShape.variantComponents?.map(({ id, name, type }) =>
shapeFields.push({
key: `variantComponents.${id}`,
description: name,
type,
}),
);
const processComponents = (components: Shape['components'], prefix: string) => {
components?.forEach(({ id, name, type, config }) => {
if (type === 'contentChunk' && config && 'components' in config) {
config.components?.forEach(({ id: subid, name: subname, type: subtype }) =>
shapeFields.push({
key: `${prefix}.${id}.${subid}`,
description: `${name} - ${subname}`,
type: subtype,
}),
);
return;
}
shapeFields.push({
key: `${prefix}.${id}`,
description: name,
type,
});
});
};
processComponents(state.selectedShape.components, 'components');
processComponents(state.selectedShape.variantComponents, 'variantComponents');

const selectedShapeField = shapeFields.find(({ key }) => state.mapping[key] === title);

const availableShapeFields = shapeFields.filter(({ key }) => !state.mapping[key]);
const basicItemFields = availableShapeFields.filter(({ key }) => key.startsWith('item.'));
const productVariantFields = availableShapeFields.filter(({ key }) => key.startsWith('variant.'));
Expand Down

0 comments on commit ecf54e0

Please sign in to comment.