the type construct of components display. TODO: add area rug calculator.
This commit is contained in:
parent
23d957824b
commit
dbba262044
@ -1,21 +1,106 @@
|
|||||||
import { Product } from "@/lib/product";
|
import { Product } from "@/lib/product"
|
||||||
|
import uuid from "react-native-uuid"
|
||||||
|
|
||||||
export const products = [
|
export default [
|
||||||
// Sheet goods
|
// Sheet goods
|
||||||
new Product(15, {l: 4, w : 8, u: "ft"}, { name: "Plywood 1/4\"" }),
|
{
|
||||||
new Product(20, {l: 4, w : 8, u: "ft"}, { name: "Plywood 1/2\"" }),
|
id: uuid.v4().valueOf(),
|
||||||
new Product(25, {l: 4, w : 8, u: "ft"}, { name: "Plywood 3/4\"" }),
|
pricePerUnit: 15,
|
||||||
new Product(5, {l: 4, w : 8, u: "ft"}, { name: "Thin Panel Board" }),
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
new Product(10, {l: 4, w : 8, u: "ft"}, { name: "Sheetrock" }),
|
type: "lumber",
|
||||||
new Product(15, {l: 4, w : 8, u: "ft"}, { name: "OSB / Particle" }),
|
attributes: { name: 'Plywood 1/4"' },
|
||||||
new Product(20, {l: 4, w : 8, u: "ft"}, { name: "MDF" }),
|
},
|
||||||
new Product(15, {l: 4, w : 8, u: "ft"}, { name: "Pegboard" }),
|
{
|
||||||
new Product(5, {l: 3, w : 5, u: "ft"}, { name: "Cement" }),
|
id: uuid.v4().valueOf(),
|
||||||
// trim
|
pricePerUnit: 20,
|
||||||
new Product(1, {l: 0.50, u : "ft"}, { name: "trim <= 3 inches" }),
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
new Product(1, {l: 0.75, u : "ft"}, { name: "trim > 3 inches" }),
|
type: "lumber",
|
||||||
// siding
|
attributes: { name: 'Plywood 1/2"' },
|
||||||
new Product(1, {l: 1, u: "ft"}, {name: "house siding"}),
|
},
|
||||||
new Product(1, {l: 1, u: "ft"}, {name: "metal / shelf bars"}),
|
{
|
||||||
new Product(0.5, {l: 1, u: "ft"}, {name: "gutter spouts"}),
|
id: uuid.v4().valueOf(),
|
||||||
];
|
pricePerUnit: 25,
|
||||||
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: 'Plywood 3/4"' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 5,
|
||||||
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "Thin Panel Board" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 10,
|
||||||
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "Sheetrock" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 15,
|
||||||
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "OSB / Particle" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 20,
|
||||||
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "MDF" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 15,
|
||||||
|
dimensions: { l: 4, w: 8, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "Pegboard" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 5,
|
||||||
|
dimensions: { l: 3, w: 5, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "Cement" },
|
||||||
|
},
|
||||||
|
// trim
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 1,
|
||||||
|
dimensions: { l: 0.5, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "trim <=3 inches" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 1,
|
||||||
|
dimensions: { l: 0.75, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "trim > 3 inches" },
|
||||||
|
},
|
||||||
|
// siding
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 1,
|
||||||
|
dimensions: { l: 1, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "house siding" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 1,
|
||||||
|
dimensions: { l: 1, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "metal / shelf bars" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid.v4().valueOf(),
|
||||||
|
pricePerUnit: 0.5,
|
||||||
|
dimensions: { l: 1, u: "ft" },
|
||||||
|
type: "lumber",
|
||||||
|
attributes: { name: "gutter spouts" },
|
||||||
|
},
|
||||||
|
] as Array<Product>;
|
@ -4,13 +4,13 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { products as fixtures } from "@/__fixtures__/initialProducts"
|
import fixtures from "@/__fixtures__/initialProducts"
|
||||||
import { setupStore } from '../store';
|
import { setupStore } from '../store';
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const store = setupStore({
|
const store = setupStore({
|
||||||
products: fixtures.map(p => p.asObject),
|
products: fixtures,
|
||||||
units: "ft",
|
units: "ft",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
@ -1,68 +1,87 @@
|
|||||||
import { Product } from "@/lib/product";
|
import { product_type_t } from "@/lib/dimensions";
|
||||||
|
import { PRODUCT_TYPES, Product } from "@/lib/product";
|
||||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import React from "react";
|
import { StyleSheet, TextInput, TouchableHighlight, View } from "react-native";
|
||||||
import { useState } from "react";
|
import SelectDropdown from "react-native-select-dropdown";
|
||||||
import { StyleSheet, Text, TextInput, TouchableHighlight, View } from "react-native";
|
|
||||||
|
|
||||||
export type ProductAttributeChangeFunc = (key: string, newValue: string) => any;
|
export type ProductAttributeChangeFunc = (key: string, newValue: string) => any;
|
||||||
export type ProductAttributeDeleteFunc = (key: string) => any;
|
export type ProductAttributeDeleteFunc = (key: string) => any;
|
||||||
export type ChangeAttributeFunction = (oldKey : string, newKey : string) => any;
|
export type ChangeAttributeFunction = (oldKey: string, newKey: string) => any;
|
||||||
|
export type ProductTypeChangeFunc = (
|
||||||
|
key: string,
|
||||||
|
newProductType: product_type_t
|
||||||
|
) => any;
|
||||||
|
|
||||||
export type ProductAttributeProps = {
|
export type ProductAttributeProps = {
|
||||||
attributeKey: string,
|
attributeKey: string;
|
||||||
attributeValue: string,
|
attributeValue: string;
|
||||||
onChangeAttributeKey?: ChangeAttributeFunction,
|
onProductTypeChange?: ProductTypeChangeFunc;
|
||||||
onChangeAttribute?: ProductAttributeChangeFunc,
|
onChangeAttributeKey?: ChangeAttributeFunction;
|
||||||
onDelete?: ProductAttributeChangeFunc,
|
onChangeAttribute?: ProductAttributeChangeFunc;
|
||||||
|
onDelete?: ProductAttributeChangeFunc;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProductAttributeEditor = ({ attributeKey, attributeValue, onDelete, onChangeAttributeKey, onChangeAttribute }: ProductAttributeProps) => {
|
const select_product_type_choices = PRODUCT_TYPES.map((p) => [p, p]);
|
||||||
|
|
||||||
const doChangeKey = (e: any) => {
|
export const ProductAttributeEditor = ({
|
||||||
onChangeAttributeKey && onChangeAttributeKey(attributeKey, e);
|
attributeKey,
|
||||||
}
|
attributeValue,
|
||||||
|
onDelete,
|
||||||
|
onChangeAttributeKey,
|
||||||
|
onChangeAttribute,
|
||||||
|
}: ProductAttributeProps) => {
|
||||||
|
const doChangeKey = (e: any) => {
|
||||||
|
onChangeAttributeKey && onChangeAttributeKey(attributeKey, e);
|
||||||
|
};
|
||||||
|
|
||||||
const doChangeValue = (e: any) => {
|
const doChangeValue = (e: any) => {
|
||||||
onChangeAttribute && onChangeAttribute(attributeKey, e);
|
onChangeAttribute && onChangeAttribute(attributeKey, e);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={styles.productAttributeRow}>
|
<View style={styles.productAttributeRow}>
|
||||||
<TextInput
|
<TextInput
|
||||||
defaultValue={attributeKey}
|
defaultValue={attributeKey}
|
||||||
onChangeText={doChangeKey}
|
onChangeText={doChangeKey}
|
||||||
style={styles.value}
|
style={styles.value}
|
||||||
aria-label="Edit Key"
|
aria-label="Edit Key"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
defaultValue={attributeValue}
|
defaultValue={attributeValue}
|
||||||
onChangeText={doChangeValue}
|
onChangeText={doChangeValue}
|
||||||
style={styles.value}
|
style={styles.value}
|
||||||
aria-label="Edit Value" />
|
aria-label="Edit Value"
|
||||||
<TouchableHighlight
|
/>
|
||||||
onPress={() => onDelete && onDelete(attributeKey, attributeValue)}
|
<TouchableHighlight
|
||||||
aria-label="Delete Attribute"
|
onPress={() => onDelete && onDelete(attributeKey, attributeValue)}
|
||||||
style={{ backgroundColor: "darkred", borderRadius: 5, margin: 5, padding: 5, }}>
|
aria-label="Delete Attribute"
|
||||||
<Ionicons name="trash-bin-outline" size={30} color={"white"} />
|
style={{
|
||||||
</TouchableHighlight>
|
backgroundColor: "darkred",
|
||||||
</View>
|
borderRadius: 5,
|
||||||
</View>
|
margin: 5,
|
||||||
)
|
padding: 5,
|
||||||
}
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="trash-bin-outline" size={30} color={"white"} />
|
||||||
|
</TouchableHighlight>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
productAttributeRow: {
|
productAttributeRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
},
|
},
|
||||||
key: {
|
key: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "grey",
|
borderColor: "grey",
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
padding: 10
|
padding: 10,
|
||||||
}
|
},
|
||||||
});
|
});
|
@ -1,5 +1,5 @@
|
|||||||
import { Product } from '@/lib/product';
|
import { Product, productPriceFor } from '@/lib/product';
|
||||||
import { dimensions_t } from "@/lib/dimensions_t";
|
import { dimensions_t } from "@/lib/dimensions";
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
@ -23,7 +23,7 @@ export default function ProductCalculatorSelector() {
|
|||||||
const iv = setInterval(function () {
|
const iv = setInterval(function () {
|
||||||
if (!(activeProduct && measurement)) return;
|
if (!(activeProduct && measurement)) return;
|
||||||
setPrice(
|
setPrice(
|
||||||
activeProduct.priceFor(measurement, percentDamage)
|
productPriceFor(activeProduct, measurement, percentDamage)
|
||||||
)
|
)
|
||||||
}, 50);
|
}, 50);
|
||||||
return function () {
|
return function () {
|
||||||
@ -34,7 +34,7 @@ export default function ProductCalculatorSelector() {
|
|||||||
function onMeasurementSet(dimensions: dimensions_t) {
|
function onMeasurementSet(dimensions: dimensions_t) {
|
||||||
setMeasurement(dimensions);
|
setMeasurement(dimensions);
|
||||||
activeProduct && setPrice(
|
activeProduct && setPrice(
|
||||||
activeProduct.priceFor(measurement, percentDamage)
|
productPriceFor(activeProduct, measurement, percentDamage)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||||
import { addAttribute, changeKey, deleteAttribute, deleteProduct, selectProductIds, selectProducts, updateAttribute, updateDimensions, updatePrice, updateProduct } from "@/features/product/productSlice"
|
import { addAttribute, changeKey, deleteAttribute, deleteProduct, selectProductIds, selectProducts, updateAttribute, updateDimensions, updatePrice, updateProduct } from "@/features/product/productSlice"
|
||||||
import { Id, Product } from "@/lib/product";
|
import { Id, Product } from "@/lib/product";
|
||||||
import { dimensions_t } from "@/lib/dimensions_t";
|
|
||||||
import { FlatList, SafeAreaView, StyleSheet, Text } from "react-native";
|
import { FlatList, SafeAreaView, StyleSheet, Text } from "react-native";
|
||||||
import { ProductEditorItem } from "./ProductEditorItem";
|
import { ProductEditorItem } from "./ProductEditorItem";
|
||||||
|
import { dimensions_t } from "@/lib/dimensions";
|
||||||
|
|
||||||
export const ProductEditor = ({}) => {
|
export const ProductEditor = ({}) => {
|
||||||
const products = useAppSelector(selectProducts) as Product [];
|
const products = useAppSelector(selectProducts) as Product [];
|
||||||
|
@ -1,231 +1,274 @@
|
|||||||
import { Id, Product } from "@/lib/product"
|
import { Id, Product, product_type_t } from "@/lib/product";
|
||||||
import { dimensions_t } from "@/lib/dimensions_t";
|
import { useState } from "react";
|
||||||
import { useState } from "react"
|
import {
|
||||||
import { Button, FlatList, StyleSheet, Text, TextInput, Touchable, TouchableHighlight, View } from "react-native"
|
Button,
|
||||||
|
FlatList,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Touchable,
|
||||||
|
TouchableHighlight,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
import { ProductAttributeEditor } from "./ProductAttributeEditor";
|
import { ProductAttributeEditor } from "./ProductAttributeEditor";
|
||||||
import { Dropdown } from 'react-native-element-dropdown';
|
import { Dropdown } from "react-native-element-dropdown";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Length } from "convert";
|
import { Length } from "convert";
|
||||||
|
import { dimensions_t } from "@/lib/dimensions";
|
||||||
|
|
||||||
export type ProductAddedFunc = () => any;
|
export type ProductAddedFunc = () => any;
|
||||||
export type ProductDeletedFunc = (product_id: Id) => any;
|
export type ProductDeletedFunc = (product_id: Id) => any;
|
||||||
export type AttributeAddedFunc = (product_id: Id) => any;
|
export type AttributeAddedFunc = (product_id: Id) => any;
|
||||||
export type AttributeKeyUpdatedFunc = (product_id: Id, oldKey: string, newKey: string) => any;
|
export type AttributeKeyUpdatedFunc = (
|
||||||
export type AttributeUpdatedFunc = (product_id: Id, attribute: string, value: string) => any;
|
product_id: Id,
|
||||||
|
oldKey: string,
|
||||||
|
newKey: string
|
||||||
|
) => any;
|
||||||
|
export type AttributeUpdatedFunc = (
|
||||||
|
product_id: Id,
|
||||||
|
attribute: string,
|
||||||
|
value: string
|
||||||
|
) => any;
|
||||||
export type AttributeDeletedFunc = (product_id: Id, attribute: string) => any;
|
export type AttributeDeletedFunc = (product_id: Id, attribute: string) => any;
|
||||||
export type PriceUpdatedFunc = (product_id: Id, price: number) => any;
|
export type PriceUpdatedFunc = (product_id: Id, price: number) => any;
|
||||||
export type DimensionUpdatedFunc = (product_id: Id, dimension: dimensions_t) => any;
|
export type DimensionUpdatedFunc = (
|
||||||
|
product_id: Id,
|
||||||
|
dimension: dimensions_t
|
||||||
|
) => any;
|
||||||
|
export type ProductTypeChangedFunc = (
|
||||||
|
product_id: Id,
|
||||||
|
product_type: product_type_t
|
||||||
|
) => any;
|
||||||
|
|
||||||
export type ProductEditorItemProps = {
|
export type ProductEditorItemProps = {
|
||||||
product: Product,
|
product: Product;
|
||||||
onProductAdded?: ProductAddedFunc,
|
onProductAdded?: ProductAddedFunc;
|
||||||
onProductDeleted?: ProductDeletedFunc,
|
onProductDeleted?: ProductDeletedFunc;
|
||||||
onAttributeAdded?: AttributeAddedFunc,
|
onAttributeAdded?: AttributeAddedFunc;
|
||||||
onAttributeKeyChanged?: AttributeKeyUpdatedFunc,
|
onAttributeKeyChanged?: AttributeKeyUpdatedFunc;
|
||||||
onAttributeUpdated?: AttributeUpdatedFunc,
|
onAttributeUpdated?: AttributeUpdatedFunc;
|
||||||
onAttributeDeleted?: AttributeDeletedFunc,
|
onAttributeDeleted?: AttributeDeletedFunc;
|
||||||
onPriceUpdated?: PriceUpdatedFunc,
|
onPriceUpdated?: PriceUpdatedFunc;
|
||||||
onDimensionsUpdated?: DimensionUpdatedFunc,
|
onDimensionsUpdated?: DimensionUpdatedFunc;
|
||||||
}
|
onProductTypeChanged?: ProductTypeChangedFunc;
|
||||||
|
};
|
||||||
|
|
||||||
export const ProductEditorItem = (props: ProductEditorItemProps) => {
|
export const ProductEditorItem = (props: ProductEditorItemProps) => {
|
||||||
|
const [showAttributes, setShowAttributes] = useState(false);
|
||||||
|
const product = props.product;
|
||||||
|
|
||||||
const [showAttributes, setShowAttributes] = useState(false);
|
function onProductTypeChange(id: Id, newProductType: product_type_t) {
|
||||||
const product = props.product;
|
props.onProductTypeChanged &&
|
||||||
|
props.onProductTypeChanged(product.id as Id, newProductType);
|
||||||
|
}
|
||||||
|
|
||||||
function onAttributeChanged(key: string, newValue: string) {
|
function onAttributeChanged(key: string, newValue: string) {
|
||||||
props.onAttributeUpdated && props.onAttributeUpdated(product.id, key, newValue);
|
props.onAttributeUpdated &&
|
||||||
}
|
props.onAttributeUpdated(product.id as Id, key, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
function onAttributeKeyChanged(oldKey: string, newKey: string) {
|
function onAttributeKeyChanged(oldKey: string, newKey: string) {
|
||||||
props.onAttributeKeyChanged && props.onAttributeKeyChanged(product.id, oldKey, newKey);
|
props.onAttributeKeyChanged &&
|
||||||
}
|
props.onAttributeKeyChanged(product.id as Id, oldKey, newKey);
|
||||||
|
}
|
||||||
|
|
||||||
function onAttributeDelete(key: string) {
|
function onAttributeDelete(key: string) {
|
||||||
props.onAttributeDeleted && props.onAttributeDeleted(product.id, key);
|
props.onAttributeDeleted && props.onAttributeDeleted(product.id as Id, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPricePerUnitChange(pricePerUnit: string) {
|
function onPricePerUnitChange(pricePerUnit: string) {
|
||||||
props.onPriceUpdated && props.onPriceUpdated(product.id, parseFloat(pricePerUnit) || parseInt(pricePerUnit));
|
props.onPriceUpdated &&
|
||||||
}
|
props.onPriceUpdated(
|
||||||
|
product.id as Id,
|
||||||
|
parseFloat(pricePerUnit) || parseInt(pricePerUnit)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function onUnitsChanged(newUnits: Length) {
|
function onUnitsChanged(newUnits: Length) {
|
||||||
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
|
props.onDimensionsUpdated &&
|
||||||
...(product.dimensions as dimensions_t),
|
props.onDimensionsUpdated(product.id as Id, {
|
||||||
u: newUnits,
|
...(product.dimensions as dimensions_t),
|
||||||
})
|
u: newUnits,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onChangeLength(len: string) {
|
function onChangeLength(len: string) {
|
||||||
const l = parseFloat(len) || parseInt(len);
|
const l = parseFloat(len) || parseInt(len);
|
||||||
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
|
props.onDimensionsUpdated &&
|
||||||
...(product.dimensions as dimensions_t),
|
props.onDimensionsUpdated(product.id as Id, {
|
||||||
l,
|
...(product.dimensions as dimensions_t),
|
||||||
})
|
l,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onChangeWidth(width: string) {
|
function onChangeWidth(width: string) {
|
||||||
const w = width.length == 0 ? null : parseFloat(width) || parseInt(width);
|
const w = width.length == 0 ? null : parseFloat(width) || parseInt(width);
|
||||||
props.onDimensionsUpdated && props.onDimensionsUpdated(product.id, {
|
props.onDimensionsUpdated &&
|
||||||
...(product.dimensions as dimensions_t),
|
props.onDimensionsUpdated(product.id as Id, {
|
||||||
...(w ? {w} : {}),
|
...(product.dimensions as dimensions_t),
|
||||||
})
|
...(w ? { w } : {}),
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onDeleteProduct() {
|
function onDeleteProduct() {
|
||||||
props.onProductDeleted && props.onProductDeleted(product.id);
|
props.onProductDeleted && props.onProductDeleted(product.id as Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const length = new String(product.dimensions.l || product.dimensions.l || "0") as string;
|
const length = new String(
|
||||||
const width = new String(product.dimensions.w || "") as string;
|
product.dimensions.l || product.dimensions.l || "0"
|
||||||
const dimension = product.dimensions.u || product.dimensions.u || "foot";
|
) as string;
|
||||||
|
const width = new String(product.dimensions.w || "") as string;
|
||||||
|
const dimension = product.dimensions.u || product.dimensions.u || "foot";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={styles.productListHeader}>
|
<View style={styles.productListHeader}>
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
onPress={() => setShowAttributes(!showAttributes)}
|
onPress={() => setShowAttributes(!showAttributes)}
|
||||||
aria-label="Product Item"
|
aria-label="Product Item"
|
||||||
style={styles.productItemName}
|
style={styles.productItemName}
|
||||||
>
|
>
|
||||||
<Text style={styles.productNameText}>{product.attributes.name || `Product ${product.id}`}</Text>
|
{product.attributes && (
|
||||||
</TouchableHighlight>
|
<Text style={styles.productNameText}>
|
||||||
<TouchableHighlight
|
{product.attributes.name || `Product ${product.id}`}
|
||||||
onPress={() => onDeleteProduct()}
|
</Text>
|
||||||
aria-label="delete product"
|
)}
|
||||||
style={styles.deleteProductHighlight}
|
</TouchableHighlight>
|
||||||
>
|
<TouchableHighlight
|
||||||
<Ionicons
|
onPress={() => onDeleteProduct()}
|
||||||
style={styles.deleteProductButton}
|
aria-label="delete product"
|
||||||
name="trash-outline"
|
style={styles.deleteProductHighlight}
|
||||||
/>
|
>
|
||||||
</TouchableHighlight>
|
<Ionicons style={styles.deleteProductButton} name="trash-outline" />
|
||||||
</View>
|
</TouchableHighlight>
|
||||||
{showAttributes &&
|
</View>
|
||||||
(
|
{showAttributes && (
|
||||||
<View style={styles.detailsWrapper}>
|
<View style={styles.detailsWrapper}>
|
||||||
<View style={styles.priceSpecWrapper}>
|
<View style={styles.priceSpecWrapper}>
|
||||||
<Text style={styles.priceLabel}>$</Text>
|
<Text style={styles.priceLabel}>$</Text>
|
||||||
<TextInput inputMode="decimal"
|
<TextInput
|
||||||
defaultValue={new String(product.pricePerUnit) as string}
|
inputMode="decimal"
|
||||||
aria-label="price per unit"
|
defaultValue={new String(product.pricePerUnit).valueOf()}
|
||||||
onChangeText={onPricePerUnitChange}
|
aria-label="price per unit"
|
||||||
style={styles.priceInput}
|
onChangeText={onPricePerUnitChange}
|
||||||
/>
|
style={styles.priceInput}
|
||||||
<Text style={styles.per}>per</Text>
|
/>
|
||||||
<Dropdown
|
<Text style={styles.per}>per</Text>
|
||||||
data={[
|
<Dropdown
|
||||||
{label: "feet", value: "ft"},
|
data={[
|
||||||
{label: "inches", value: "in"},
|
{ label: "feet", value: "ft" },
|
||||||
]}
|
{ label: "inches", value: "in" },
|
||||||
style={styles.unitsSelect}
|
]}
|
||||||
mode="modal"
|
style={styles.unitsSelect}
|
||||||
labelField="label"
|
mode="modal"
|
||||||
valueField="value"
|
labelField="label"
|
||||||
value={product.dimensions.u || "ft"}
|
valueField="value"
|
||||||
onChange={(item) => onUnitsChanged(item.value as Length)}
|
value={product.dimensions.u || "ft"}
|
||||||
/>
|
onChange={(item) => onUnitsChanged(item.value as Length)}
|
||||||
<TextInput
|
/>
|
||||||
inputMode="decimal"
|
<TextInput
|
||||||
defaultValue={length}
|
inputMode="decimal"
|
||||||
onChangeText={onChangeLength}
|
defaultValue={length}
|
||||||
style={styles.lengthInput}
|
onChangeText={onChangeLength}
|
||||||
aria-label="length"
|
style={styles.lengthInput}
|
||||||
/>
|
aria-label="length"
|
||||||
<Text style={{flex: 1,}}>x</Text>
|
/>
|
||||||
<TextInput
|
<Text style={{ flex: 1 }}>x</Text>
|
||||||
inputMode="decimal"
|
<TextInput
|
||||||
defaultValue={width}
|
inputMode="decimal"
|
||||||
onChangeText={onChangeWidth}
|
defaultValue={width}
|
||||||
style={styles.widthInput}
|
onChangeText={onChangeWidth}
|
||||||
aria-label="width"
|
style={styles.widthInput}
|
||||||
/>
|
aria-label="width"
|
||||||
</View>
|
/>
|
||||||
<Button title="+ Add Attribute" onPress={() => props.onAttributeAdded && props.onAttributeAdded(product.id)} />
|
</View>
|
||||||
<FlatList
|
<Button
|
||||||
style={styles.productAttributesList}
|
title="+ Add Attribute"
|
||||||
data={Object.entries(product.attributes)}
|
onPress={() =>
|
||||||
renderItem={({ item }) => (
|
props.onAttributeAdded && props.onAttributeAdded(product.id as Id)
|
||||||
<ProductAttributeEditor
|
|
||||||
attributeKey={item[0] || "some key"}
|
|
||||||
attributeValue={item[1]}
|
|
||||||
onChangeAttributeKey={onAttributeKeyChanged}
|
|
||||||
onChangeAttribute={onAttributeChanged}
|
|
||||||
onDelete={onAttributeDelete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
keyExtractor={(item, i) => `${product.id}-${i}`}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
/>
|
||||||
|
{product.attributes && (
|
||||||
|
<FlatList
|
||||||
|
style={styles.productAttributesList}
|
||||||
|
data={Object.entries(product.attributes)}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<ProductAttributeEditor
|
||||||
|
onProductTypeChange={onProductTypeChange}
|
||||||
|
attributeKey={item[0] || "some key"}
|
||||||
|
attributeValue={item[1]}
|
||||||
|
onChangeAttributeKey={onAttributeKeyChanged}
|
||||||
|
onChangeAttribute={onAttributeChanged}
|
||||||
|
onDelete={onAttributeDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
keyExtractor={(item, i) => `${product.id}-${i}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)}
|
||||||
}
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
deleteProductHighlight: {
|
deleteProductHighlight: {
|
||||||
|
padding: 5,
|
||||||
padding: 5,
|
borderWidth: 1,
|
||||||
borderWidth: 1,
|
},
|
||||||
},
|
deleteProductButton: {
|
||||||
deleteProductButton: {
|
fontSize: 20,
|
||||||
fontSize: 20,
|
},
|
||||||
},
|
detailsWrapper: {},
|
||||||
detailsWrapper: {
|
priceSpecWrapper: {
|
||||||
|
flexDirection: "row",
|
||||||
},
|
},
|
||||||
priceSpecWrapper: {
|
priceLabel: {},
|
||||||
flexDirection: "row",
|
priceInput: {
|
||||||
},
|
flex: 1,
|
||||||
priceLabel: {
|
borderWidth: 2,
|
||||||
},
|
borderColor: "lightgrey",
|
||||||
priceInput: {
|
borderStyle: "solid",
|
||||||
flex: 1,
|
},
|
||||||
borderWidth: 2,
|
per: {
|
||||||
borderColor: "lightgrey",
|
padding: 5,
|
||||||
borderStyle: "solid",
|
},
|
||||||
},
|
unitsLabel: {},
|
||||||
per: {
|
unitsSelect: {
|
||||||
padding: 5,
|
flex: 1,
|
||||||
},
|
padding: 5,
|
||||||
unitsLabel: {
|
},
|
||||||
},
|
lengthInput: {
|
||||||
unitsSelect: {
|
flex: 1,
|
||||||
flex: 1,
|
borderWidth: 2,
|
||||||
padding: 5,
|
borderColor: "lightgrey",
|
||||||
},
|
borderStyle: "solid",
|
||||||
lengthInput: {
|
},
|
||||||
flex: 1,
|
widthInput: {
|
||||||
borderWidth: 2,
|
flex: 1,
|
||||||
borderColor: "lightgrey",
|
borderWidth: 2,
|
||||||
borderStyle: "solid",
|
borderColor: "lightgrey",
|
||||||
},
|
borderStyle: "solid",
|
||||||
widthInput: {
|
},
|
||||||
flex: 1,
|
productListHeader: {
|
||||||
borderWidth: 2,
|
flexDirection: "row",
|
||||||
borderColor: "lightgrey",
|
padding: 5,
|
||||||
borderStyle: "solid",
|
},
|
||||||
},
|
productNameText: {
|
||||||
productListHeader: {
|
paddingLeft: 10,
|
||||||
flexDirection: "row",
|
paddingRight: 10,
|
||||||
padding: 5,
|
},
|
||||||
},
|
productItemName: {
|
||||||
productNameText: {
|
flex: 1,
|
||||||
paddingLeft: 10,
|
backgroundColor: "lightgrey",
|
||||||
paddingRight: 10,
|
padding: 4,
|
||||||
},
|
margin: 4,
|
||||||
productItemName: {
|
},
|
||||||
flex: 1,
|
productAttributesList: {
|
||||||
backgroundColor: "lightgrey",
|
margin: 10,
|
||||||
padding: 4,
|
padding: 10,
|
||||||
margin: 4,
|
borderWidth: 1,
|
||||||
},
|
borderStyle: "solid",
|
||||||
productAttributesList: {
|
borderColor: "black",
|
||||||
margin: 10,
|
},
|
||||||
padding: 10,
|
});
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
borderColor: "black",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
import { FlatList, ScrollView, StyleSheet, Text, TouchableHighlight } from "react-native";
|
import { ScrollView, StyleSheet } from "react-native";
|
||||||
import { ProductTile } from "./ProductTile";
|
import { ProductTile } from "./ProductTile";
|
||||||
import { Id, Product } from "@/lib/product";
|
import { Product } from "@/lib/product";
|
||||||
import { Key, useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { selectProducts } from "@/features/product/productSlice";
|
import { selectProducts } from "@/features/product/productSlice";
|
||||||
import { useAppSelector } from "@/app/store";
|
import { useAppSelector } from "@/app/store";
|
||||||
|
|
||||||
export type ProductSelectionProps = {
|
export type ProductSelectionProps = {
|
||||||
onProductSelected?: (product: Product) => any;
|
onProductSelected?: (product: Product) => any;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function ProductList({ onProductSelected }: ProductSelectionProps) {
|
|
||||||
|
|
||||||
|
export default function ProductList({
|
||||||
|
onProductSelected,
|
||||||
|
}: ProductSelectionProps) {
|
||||||
const [activeProduct, setActiveProduct] = useState(null as null | Product);
|
const [activeProduct, setActiveProduct] = useState(null as null | Product);
|
||||||
const products = useAppSelector(selectProducts).filter(p => (!!p.dimensions));
|
const products = useAppSelector(selectProducts).filter(p => !!p).filter((p) => {
|
||||||
|
console.dir(p);
|
||||||
|
return !!p.dimensions;
|
||||||
|
});
|
||||||
|
|
||||||
function doOnProductSelected(product: Product) {
|
function doOnProductSelected(product: Product) {
|
||||||
setActiveProduct(product);
|
setActiveProduct(product);
|
||||||
@ -21,8 +24,8 @@ export default function ProductList({ onProductSelected }: ProductSelectionProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView scrollToOverflowEnabled={true}>
|
<ScrollView scrollToOverflowEnabled={true} aria-label="product list">
|
||||||
{products.map(product => {
|
{products.map((product) => {
|
||||||
return (
|
return (
|
||||||
<ProductTile
|
<ProductTile
|
||||||
product={product}
|
product={product}
|
||||||
@ -33,14 +36,12 @@ export default function ProductList({ onProductSelected }: ProductSelectionProps
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|
||||||
productSelectorFlatList: {
|
productSelectorFlatList: {
|
||||||
padding: 10,
|
padding: 10,
|
||||||
margin: 10,
|
margin: 10,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
})
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Product } from "@/lib/product"
|
import { Product, priceDisplay, pricePerUnitDisplay } from "@/lib/product"
|
||||||
import { ImageBackground, StyleProp, StyleSheet, Text, TouchableHighlight, View, ViewStyle } from "react-native";
|
import { ImageBackground, StyleProp, StyleSheet, Text, TouchableHighlight, View, ViewStyle } from "react-native";
|
||||||
import { AnimatedStyle } from "react-native-reanimated";
|
import { AnimatedStyle } from "react-native-reanimated";
|
||||||
|
|
||||||
@ -23,12 +23,14 @@ const FALLBACK_IMAGE = "";
|
|||||||
|
|
||||||
export function ProductTile ({product, onProductSelected, isActive} : ProductTileProps) {
|
export function ProductTile ({product, onProductSelected, isActive} : ProductTileProps) {
|
||||||
const k = isActive ? "active" : "default";
|
const k = isActive ? "active" : "default";
|
||||||
|
|
||||||
|
const priceDisplay = pricePerUnitDisplay(product);
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
style={styles[k].highlight}
|
style={styles[k].highlight}
|
||||||
onPress={() => onProductSelected && onProductSelected(product)}>
|
onPress={() => onProductSelected && onProductSelected(product)}>
|
||||||
<Text style={styles[k].text}>{product.attributes.name || `Product ${product.id}`} ({product.pricePerUnitDisplay})</Text>
|
<Text style={styles[k].text}>{product.attributes?.name || `Product ${product.id}`} ({priceDisplay})</Text>
|
||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,16 @@
|
|||||||
import { Product } from "@/lib/product"
|
import { LumberProduct, Product } from "@/lib/product"
|
||||||
import {ProductAttributeEditor} from "../ProductAttributeEditor"
|
import {ProductAttributeEditor} from "../ProductAttributeEditor"
|
||||||
import { area } from "enheter"
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react-native';
|
import { fireEvent, render, screen } from '@testing-library/react-native';
|
||||||
import React from "react";
|
|
||||||
import { emitTypingEvents } from "@testing-library/react-native/build/user-event/type/type";
|
|
||||||
|
|
||||||
describe("Product editor tests", () => {
|
describe("Product editor tests", () => {
|
||||||
const productName = "Fun Product";
|
const productName = "Fun Product";
|
||||||
it("Product attributes can be deleted", async () => {
|
it("Product attributes can be deleted", async () => {
|
||||||
const product = new Product(
|
|
||||||
100,
|
|
||||||
{l: 100, u: "foot"},
|
|
||||||
{"name" : productName}
|
|
||||||
);
|
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const onDelete = jest.fn();
|
const onDelete = jest.fn();
|
||||||
render(
|
render(
|
||||||
<ProductAttributeEditor
|
<ProductAttributeEditor
|
||||||
attributeKey="name"
|
attributeKey="name"
|
||||||
attributeValue="product"
|
attributeValue="product"
|
||||||
product={product}
|
|
||||||
onChangeAttribute={onChange}
|
onChangeAttribute={onChange}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>);
|
/>);
|
||||||
@ -28,11 +19,14 @@ describe("Product editor tests", () => {
|
|||||||
expect(onDelete).toHaveBeenCalled();
|
expect(onDelete).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
it("Product attributes can be modified", async () => {
|
it("Product attributes can be modified", async () => {
|
||||||
const product = new Product(
|
const product : Product = {
|
||||||
100,
|
pricePerUnit: 10,
|
||||||
{l: 100, u: "foot"},
|
dimensions: {
|
||||||
{"name" : productName}
|
l: 40,
|
||||||
);
|
u: "ft",
|
||||||
|
},
|
||||||
|
type: "lumber",
|
||||||
|
}
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const onDelete = jest.fn();
|
const onDelete = jest.fn();
|
||||||
const onKeyChange = jest.fn();
|
const onKeyChange = jest.fn();
|
||||||
|
@ -2,50 +2,56 @@ import { renderWithProviders } from "@/lib/rendering";
|
|||||||
import { ProductEditor } from "@/components/ProductEditor";
|
import { ProductEditor } from "@/components/ProductEditor";
|
||||||
import { act, fireEvent, screen } from "@testing-library/react-native";
|
import { act, fireEvent, screen } from "@testing-library/react-native";
|
||||||
import { selectProducts } from "@/features/product/productSlice";
|
import { selectProducts } from "@/features/product/productSlice";
|
||||||
import { Product } from "@/lib/product";
|
import { LumberProduct, Product } from "@/lib/product";
|
||||||
|
|
||||||
describe("ProductEditor", () => {
|
describe("ProductEditor", () => {
|
||||||
const productName = "Flooring"
|
const productName = "Flooring";
|
||||||
const mockProduct = new Product(
|
const mockProduct: LumberProduct = {
|
||||||
25,
|
attributes: {
|
||||||
{ l: 4, w: 8, u: "foot" },
|
name: productName,
|
||||||
{ name: productName },
|
},
|
||||||
)
|
pricePerUnit: 10,
|
||||||
it("renders correctly", async () => {
|
dimensions: {
|
||||||
const { store } = renderWithProviders(<ProductEditor />, {
|
l: 40,
|
||||||
products: [
|
u: "ft",
|
||||||
mockProduct.asObject,
|
},
|
||||||
],
|
type: "lumber",
|
||||||
});
|
};
|
||||||
|
it("renders correctly", async () => {
|
||||||
const state1 = store.getState();
|
const { store } = renderWithProviders(<ProductEditor />, {
|
||||||
|
products: [mockProduct],
|
||||||
let products = selectProducts(state1);
|
|
||||||
|
|
||||||
expect(products).toHaveLength(1);
|
|
||||||
|
|
||||||
// Check if the product names are rendered
|
|
||||||
expect(screen.getByText(products[0].attributes.name as string)).toBeTruthy();
|
|
||||||
|
|
||||||
// Start to edit a product
|
|
||||||
fireEvent.press(screen.getByText(productName));
|
|
||||||
|
|
||||||
// Change properties of the product to make sure it's updated in the store
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.changeText(screen.getByLabelText("length"), "16");
|
|
||||||
})
|
|
||||||
products = selectProducts(store.getState());
|
|
||||||
expect(products[0].dimensions.l).toBe(16);
|
|
||||||
act(() => {
|
|
||||||
fireEvent.changeText(screen.getByLabelText("width"), "32");
|
|
||||||
})
|
|
||||||
products = selectProducts(store.getState());
|
|
||||||
|
|
||||||
expect(products[0].dimensions.w).toBe(32);
|
|
||||||
|
|
||||||
fireEvent.press(screen.getByLabelText("delete product"));
|
|
||||||
products = selectProducts(store.getState());
|
|
||||||
expect(products.length).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const state1 = store.getState();
|
||||||
|
|
||||||
|
let products = selectProducts(state1);
|
||||||
|
|
||||||
|
expect(products).toHaveLength(1);
|
||||||
|
|
||||||
|
// Check if the product names are rendered
|
||||||
|
expect(
|
||||||
|
screen.getByText(products[0].attributes.name as string)
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
// Start to edit a product
|
||||||
|
fireEvent.press(screen.getByText(productName));
|
||||||
|
|
||||||
|
// Change properties of the product to make sure it's updated in the store
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.changeText(screen.getByLabelText("length"), "16");
|
||||||
|
});
|
||||||
|
products = selectProducts(store.getState());
|
||||||
|
expect(products[0].dimensions.l).toBe(16);
|
||||||
|
act(() => {
|
||||||
|
fireEvent.changeText(screen.getByLabelText("width"), "32");
|
||||||
|
});
|
||||||
|
products = selectProducts(store.getState());
|
||||||
|
|
||||||
|
expect(products[0].dimensions.w).toBe(32);
|
||||||
|
|
||||||
|
fireEvent.press(screen.getByLabelText("delete product"));
|
||||||
|
products = selectProducts(store.getState());
|
||||||
|
expect(products.length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,14 +4,22 @@ import { ProductEditorItem } from '../ProductEditorItem';
|
|||||||
import { Product } from '@/lib/product';
|
import { Product } from '@/lib/product';
|
||||||
import { area } from 'enheter';
|
import { area } from 'enheter';
|
||||||
import { renderWithProviders } from '@/lib/rendering';
|
import { renderWithProviders } from '@/lib/rendering';
|
||||||
|
import { area_t } from '@/lib/dimensions';
|
||||||
|
|
||||||
describe('ProductEditorItem', () => {
|
describe('ProductEditorItem', () => {
|
||||||
const productName = "Product 1";
|
const productName = "Product 1";
|
||||||
const mockProduct = new Product(
|
const mockProduct : Product = {
|
||||||
25,
|
type: "area_rug",
|
||||||
{l: 4, u: 'feet'},
|
dimensions: {
|
||||||
{"name": productName},
|
l: 1,
|
||||||
)
|
w: 1,
|
||||||
|
u: "feet",
|
||||||
|
},
|
||||||
|
pricePerUnit: 0.75,
|
||||||
|
attributes: {
|
||||||
|
name: productName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onAttributeAdded = jest.fn();
|
const onAttributeAdded = jest.fn();
|
||||||
const mockOnProductDeleted = jest.fn();
|
const mockOnProductDeleted = jest.fn();
|
||||||
@ -56,7 +64,7 @@ describe('ProductEditorItem', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
fireEvent.press(screen.getByText("Product 1"));
|
fireEvent.press(screen.getByText("Product 1"));
|
||||||
expect(screen.getByLabelText("units")).toBeTruthy();
|
// expect(screen.getByLabelText("Units")).toBeTruthy();
|
||||||
expect(screen.getByLabelText("Edit Key")).toBeTruthy();
|
expect(screen.getByLabelText("Edit Key")).toBeTruthy();
|
||||||
expect(screen.getAllByLabelText("Edit Value").length).toEqual(1);
|
expect(screen.getAllByLabelText("Edit Value").length).toEqual(1);
|
||||||
|
|
||||||
|
27
components/__tests__/ProductList-test.tsx
Normal file
27
components/__tests__/ProductList-test.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { renderWithProviders } from '@/lib/rendering';
|
||||||
|
import { Product } from '@/lib/product';
|
||||||
|
import ProductList from '@/components/ProductList';
|
||||||
|
import initialProducts from '@/__fixtures__/initialProducts';
|
||||||
|
import { screen } from '@testing-library/react-native';
|
||||||
|
|
||||||
|
describe('ProductList', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { getByTestId } = renderWithProviders(<ProductList />, {
|
||||||
|
products: initialProducts,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('product list')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders products correctly', () => {
|
||||||
|
const mockProduct = initialProducts[0];
|
||||||
|
|
||||||
|
const { getByText } = renderWithProviders(<ProductList />, {
|
||||||
|
products: [mockProduct],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByText(mockProduct.attributes.name)).toBeTruthy();
|
||||||
|
expect(getByText(`$${mockProduct.pricePerUnit}`)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -151,7 +151,7 @@ const productsState = createSlice({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectProductsDatas = (state: RootState) => {
|
export const selectProducts = (state: RootState) => {
|
||||||
return state.products;
|
return state.products;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,10 +159,6 @@ export const selectUnits = (state : RootState) => {
|
|||||||
return (state.units || "ft") as Length;
|
return (state.units || "ft") as Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectProducts = createSelector([selectProductsDatas], productsData => {
|
|
||||||
return productsData.map(d => Product.fromObject(d));
|
|
||||||
})
|
|
||||||
|
|
||||||
export const selectProductIds = createSelector([selectProducts], products => {
|
export const selectProductIds = createSelector([selectProducts], products => {
|
||||||
return products.map(p => p.id);
|
return products.map(p => p.id);
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { length } from "enheter";
|
|
||||||
import { Product } from "../product";
|
import { Product } from "../product";
|
||||||
|
|
||||||
describe("Product tests", () => {
|
describe("Product tests", () => {
|
||||||
@ -20,11 +19,4 @@ describe("Product tests", () => {
|
|||||||
const comparison = standard.priceFor({l : 24, u: "inch"});
|
const comparison = standard.priceFor({l : 24, u: "inch"});
|
||||||
expect(comparison).toBeCloseTo(20, 4);
|
expect(comparison).toBeCloseTo(20, 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can convert to/from object", () => {
|
|
||||||
const standard = new Product(10, {l: 1, u : "feet"});
|
|
||||||
const obj = standard.asObject;
|
|
||||||
const back = Product.fromObject(obj);
|
|
||||||
expect(back).toEqual(standard);
|
|
||||||
})
|
|
||||||
});
|
});
|
@ -11,7 +11,6 @@ export type area_t = length_t & {
|
|||||||
|
|
||||||
export type dimensions_t = area_t | length_t;
|
export type dimensions_t = area_t | length_t;
|
||||||
|
|
||||||
export type product_type_t = "area" | "length";
|
|
||||||
|
|
||||||
export const isArea = (d: dimensions_t) => ("width" in d);
|
export const isArea = (d: dimensions_t) => ("width" in d);
|
||||||
export const isLength = (d: dimensions_t) => (!("width" in d));
|
export const isLength = (d: dimensions_t) => (!("width" in d));
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid";
|
||||||
import { dimensions_t, area_t } from "./dimensions";
|
import { dimensions_t, area_t } from "./dimensions";
|
||||||
import { matchDimensions } from "./dimensions";
|
import { matchDimensions } from "./dimensions";
|
||||||
|
import { Area, Length, Unit } from "convert";
|
||||||
|
|
||||||
export type Id = string;
|
export type Id = string;
|
||||||
|
|
||||||
@ -15,11 +16,18 @@ export type ProductAttributes = {
|
|||||||
currency?: Currency,
|
currency?: Currency,
|
||||||
[index:string]: any,
|
[index:string]: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dimensionArea(d: dimensions_t) {
|
export function dimensionArea(d: dimensions_t) {
|
||||||
return "w" in d ? d.w * d.l : 0;
|
return "w" in d ? d.w * d.l : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type product_type_t = "lumber" | "area_rug"
|
export const PRODUCT_TYPES = [
|
||||||
|
"lumber",
|
||||||
|
"area_rug",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type product_type_t = typeof PRODUCT_TYPES[number];
|
||||||
|
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
id?: Id;
|
id?: Id;
|
||||||
|
Loading…
Reference in New Issue
Block a user