Add flexible crate options

Signed-off-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
Egor Savkin 2024-01-30 13:02:01 +08:00
parent bc81035555
commit 4527189994
11 changed files with 187 additions and 17 deletions

View File

@ -3,7 +3,6 @@ import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils"; import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItem} from "./ProductCartItem"; import {ProductCartItem} from "./ProductCartItem";
import {FakePlaceholder} from "./FakePlaceholder"; import {FakePlaceholder} from "./FakePlaceholder";
import {FillExtData} from "./options/utils";
import {hp_to_slots} from "./count_resources"; import {hp_to_slots} from "./count_resources";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
@ -29,12 +28,10 @@ export function Cart({crate_index}) {
const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp); const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp);
const products = crate.items.map((item, index) => { const products = crate.items.map((item, index) => {
const ext_data = FillExtData(crate.items, index);
return ( return (
<ProductCartItem <ProductCartItem
card_index={index} card_index={index}
crate_index={crate_index} crate_index={crate_index}
ext_data={ext_data}
first={index === 0} first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots} last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
key={item.id}/> key={item.id}/>

View File

@ -7,6 +7,7 @@ import {useShopStore} from "./shop_store";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {CrateFanTray} from "./CrateFanTray"; import {CrateFanTray} from "./CrateFanTray";
import {CrateOptions} from "./CrateOptions";
/** /**
@ -45,7 +46,7 @@ export function Crate({crate_index}) {
<CrateWarnings crate_index={crate_index} /> <CrateWarnings crate_index={crate_index} />
<CrateFanTray crate_index={crate_index}/> <CrateOptions crate_index={crate_index}/>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,43 @@
import React from 'react';
import {useShopStore} from "./shop_store";
import {ProcessOptions, ProcessOptionsToData} from "./options/Options";
export function CrateOptions({crate_index}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const optionsLogic = useShopStore((state) => state.crate_options);
const updateOptions = useShopStore((state) => state.updateCrateOptions);
const options_data = useShopStore((state) => state.crates[crate_index].options_data || {});
console.log(options_data)
const options = ProcessOptions({
options: optionsLogic,
data: options_data,
id: "crate_options" + crate_id,
target: {
construct: ((outvar, value) => {
// #!options_log
console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
}),
update: ((outvar, value) => {
// #!options_log
console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
updateOptions(crate_id, {[outvar]: value});
})
}
});
console.log(options)
return (
<div className="crate-bar">
{options}
</div>
)
}

View File

@ -19,6 +19,7 @@ export function Shop() {
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const addCardFromBacklog = useShopStore((state) => state.addCardFromBacklog); const addCardFromBacklog = useShopStore((state) => state.addCardFromBacklog);
const initExtData = useShopStore((state) => state.initExtData);
const moveCard = useShopStore((state) => state.moveCard); const moveCard = useShopStore((state) => state.moveCard);
const deleteCard = useShopStore((state) => state.deleteCard); const deleteCard = useShopStore((state) => state.deleteCard);
const cardIndexById = useShopStore((state) => state.cardIndexById); const cardIndexById = useShopStore((state) => state.cardIndexById);
@ -38,6 +39,7 @@ export function Shop() {
useEffect(() => { useEffect(() => {
addCardFromBacklog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1, true); addCardFromBacklog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1, true);
initExtData();
}, []); }, []);
// #!render_count // #!render_count

View File

@ -3,10 +3,11 @@ import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {SummaryCrateHeader} from "./SummaryCrateHeader"; import {SummaryCrateHeader} from "./SummaryCrateHeader";
import {SummaryCrateCard} from "./SummaryCrateCard"; import {SummaryCrateCard} from "./SummaryCrateCard";
import {SummaryCratePricedOptions} from "./SummaryCratePricedOptions";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {SummaryCrateFanTray} from "./SummaryCrateFanTray";
export function SummaryCrate({crate_index}) { export function SummaryCrate({crate_index}) {
// #!render_count // #!render_count
@ -26,7 +27,8 @@ export function SummaryCrate({crate_index}) {
{range(0, crate_len).map((index, _i) => {range(0, crate_len).map((index, _i) =>
<SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} /> <SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} />
)} )}
<SummaryCrateFanTray crate_index={crate_index}/>
<SummaryCratePricedOptions crate_index={crate_index}/>
</tbody> </tbody>
) )
} }

View File

@ -0,0 +1,48 @@
import {formatMoney} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {ProcessOptionsToData} from "./options/Options";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCratePricedOptions({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const currency = useShopStore((state) => state.currency);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const optionsPrices = useShopStore((state) => state.crate_prices);
const updateOptions = useShopStore((state) => state.updateCrateOptions);
const options_data = useShopStore((state) => state.crates[crate_index].options_data || {});
const options = ProcessOptionsToData({options: optionsPrices, data: options_data});
console.log(options, options_data, optionsPrices)
// #!render_count
console.log("SummaryCratePricedOptions renders: ", renderCount)
return options.map((option, i) => (
<tr key={"summary_crate_" + crate_id +"option_" + option.id}>
<td className="item-card-name">
<span style={{
'display': 'inline-block', 'width': '16px',
}}>&nbsp;</span>
<div>{option.title}</div>
</td>
<td className="price">
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(option.price)}`}
<button onClick={() => updateOptions(crate_id, option.disable_patch)}>
<img src="/images/shop/icon-remove.svg" className="d-block"/>
</button>
<div style={{'width': '45px', 'height': '20px'}} className="d-inline"></div>
</div>
</td>
</tr>
));
}

View File

@ -49,7 +49,7 @@ export function ProcessOptionsToData({options, data}) {
options: option_item, options: option_item,
data: data, data: data,
})) }))
).flat(); ).filter((item, i) => !!item).flat();
} else if (options_t === "object") { } else if (options_t === "object") {
if (true_type_of(options.title) === "string") { if (true_type_of(options.title) === "string") {
return options; return options;
@ -57,6 +57,7 @@ export function ProcessOptionsToData({options, data}) {
return ProcessOptionsToData({options: json_logic_apply(options, data), data: data}); return ProcessOptionsToData({options: json_logic_apply(options, data), data: data});
} }
} else { } else {
throw Error("Incompatible type for the option: " + options_t) //throw Error("Incompatible type for the option: " + options_t)
return null;
} }
} }

View File

@ -23,6 +23,15 @@ class Switch extends Component {
this.props.target.update(this.props.outvar, new_checked); this.props.target.update(this.props.outvar, new_checked);
} }
static getDerivedStateFromProps(props, current_state) {
if (current_state.checked !== props.data[props.outvar]) {
return {
checked: props.data[props.outvar]
}
}
return null
}
render() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (

View File

@ -4,7 +4,7 @@ import {componentsList} from "./components/components";
// https://stackoverflow.com/a/70511311 // https://stackoverflow.com/a/70511311
export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
export function FillExtData(data, index) { export function FillExtCardData(data, index) {
return { return {
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0, has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0,
has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0, has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
@ -12,6 +12,12 @@ export function FillExtData(data, index) {
} }
} }
export function FillExtCrateData(crate) {
return {
crate_mode: crate.crate_mode
}
}
export function FilterOptions(options, data) { export function FilterOptions(options, data) {
let options_t = true_type_of(options); let options_t = true_type_of(options);
let target = {}; let target = {};

View File

@ -2,10 +2,10 @@
import {createWithEqualityFn} from "zustand/traditional"; import {createWithEqualityFn} from "zustand/traditional";
import {data as shared_data, itemsUnfoldedList} from "./utils"; import {data as shared_data, itemsUnfoldedList} from "./utils";
import {true_type_of} from "./options/utils"; import {FillExtCrateData, true_type_of} from "./options/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources"; import {FillResources} from "./count_resources";
import {FillExtData} from "./options/utils"; import {FillExtCardData} from "./options/utils";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings"; import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate"; import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate";
import {CratesToJSON, JSONToCrates} from "./json_porter"; import {CratesToJSON, JSONToCrates} from "./json_porter";
@ -38,6 +38,49 @@ const useCrateOptions = ((set, get) => ({
crate_options: shared_data.crateOptions.options, crate_options: shared_data.crateOptions.options,
crate_prices: shared_data.crateOptions.prices, crate_prices: shared_data.crateOptions.prices,
fillExtCrateData: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
const previous_options = crate.options_data || {};
return {
...crate,
options_data: {
...previous_options,
ext_data: FillExtCrateData(crate)
}
}
}
else return crate;
})
})),
_updateCrateOption: (crate_id, new_options) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
const previous_options = crate.options_data || {};
console.log(crate_id, new_options, {
...crate,
options_data: {
...previous_options,
...new_options
}
})
return {
...crate,
options_data: {
...previous_options,
...new_options
}
}
}
else return crate;
})
})),
updateCrateOptions: (crate_id, new_options) => {
get().fillExtCrateData(crate_id);
get()._updateCrateOption(crate_id, new_options);
}
})); }));
const useOrderOptions = ((set, get) => ({ const useOrderOptions = ((set, get) => ({
@ -45,14 +88,20 @@ const useOrderOptions = ((set, get) => ({
orderPrices: shared_data.crateOptions.prices, orderPrices: shared_data.crateOptions.prices,
order_options_data: {}, order_options_data: {},
// in case of future needs
fillOrderExtData: _ => {}, fillOrderExtData: _ => {},
_updateOrderOption: (new_options) => set(state => ({ _updateOrderOptions: (new_options) => set(state => ({
order_options_data: { order_options_data: {
...state.order_options_data, ...state.order_options_data,
...new_options ...new_options
} }
})) })),
updateOrderOptions: (new_options) => {
get().fillOrderExtData();
get()._updateOrderOptions(new_options);
}
})); }));
const useLayout = ((set, get) => ({ const useLayout = ((set, get) => ({
@ -400,7 +449,7 @@ const useCart = ((set, get) => ({
itemsCopy = itemsCopy.map((item, index) => { itemsCopy = itemsCopy.map((item, index) => {
if (!item.options) return item; if (!item.options) return item;
if (!item.options_data) item.options_data = {}; if (!item.options_data) item.options_data = {};
item.options_data.ext_data = FillExtData(itemsCopy, index); item.options_data.ext_data = FillExtCardData(itemsCopy, index);
return item; return item;
}); });
return { return {
@ -430,12 +479,14 @@ const useCart = ((set, get) => ({
const crate_id = "crate" + get().crates.length; const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id) get()._newCrate(crate_id)
get().fillExtData(crate_id); get().fillExtData(crate_id);
get().fillExtCrateData(crate_id);
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
}, },
setCrateMode: (id, mode) => { setCrateMode: (id, mode) => {
get()._setCrateMode(id, mode) get()._setCrateMode(id, mode)
get().fillExtData(id); get().fillExtData(id);
get().fillExtCrateData(id);
get().fillWarnings(id); get().fillWarnings(id);
get().setActiveCrate(id); get().setActiveCrate(id);
}, },
@ -497,6 +548,14 @@ const useCart = ((set, get) => ({
fanTrayAvailableByIndex: (crate_index) => { fanTrayAvailableByIndex: (crate_index) => {
return get().fanTrayAvailableForMode(get().crates[crate_index].crate_mode); return get().fanTrayAvailableForMode(get().crates[crate_index].crate_mode);
},
initExtData: () => {
get().fillOrderExtData();
get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id);
get().fillExtCrateData(crate.id);
})
} }
})) }))

View File

@ -34,14 +34,16 @@ const shop_data = {
{"==": [{"var": "ext_data.crate_mode"}, "rack",]}, {"==": [{"var": "ext_data.crate_mode"}, "rack",]},
{type: "Switch", args: { {type: "Switch", args: {
title: "Add fan tray", title: "Add fan tray",
outvar: "nuc", outvar: "fan_tray",
tip: "Add 1U 84hp fan tray (to be mounted under the crate) to improve cooling. " + tip: "Add 1U 84hp fan tray (to be mounted under the crate) to improve cooling. " +
"Fans need 220VAC 50/60Hz power. 3 fans, 167m³/h air flow.", "Fans need 220VAC 50/60Hz power. 3 fans, 167m³/h air flow.",
fallback: false, fallback: false
}} }}
]}, ]},
], ],
prices: [{"if": [{"and": [{"var": "fan_tray"}, {"==": [{"var": "ext_data.crate_mode"}, "rack",]}]}, {title: "Add fan tray", price: 470}]}] prices: [{"if": [
{"and": [{"var": "fan_tray"}, {"==": [{"var": "ext_data.crate_mode"}, "rack",]}]},
{title: "Add fan tray", price: 470, disable_patch: {"fan_tray": false}, id: "fan_tray"}]}]
}, },
orderOptions: { orderOptions: {