From abb472f0ea9d5f880d56edb1b8c2a1c39d373d53 Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Tue, 5 Dec 2023 17:35:31 +0800 Subject: [PATCH] Minimal working warnings and resources TODO: * Add all other warnings for cards * Add crate level warnings * Add reminders to the bottom of the crate * Refactor crate mode selection (???) * Add resource consumers, instead of a bunch of nbrOccupiedSLots Signed-off-by: Egor Savkin --- static/js/shop/ProductCartItem.jsx | 25 +-- static/js/shop/Resources.jsx | 11 +- static/js/shop/Shop.jsx | 284 +---------------------------- static/js/shop/Warnings.jsx | 16 +- static/js/shop/count_resources.js | 56 +++--- static/js/shop/options/Options.jsx | 1 + static/js/shop/warnings.js | 81 +++++++- static/js/shop_data.js | 63 ++----- 8 files changed, 142 insertions(+), 395 deletions(-) diff --git a/static/js/shop/ProductCartItem.jsx b/static/js/shop/ProductCartItem.jsx index 7831f294..2a693d75 100644 --- a/static/js/shop/ProductCartItem.jsx +++ b/static/js/shop/ProductCartItem.jsx @@ -1,11 +1,10 @@ import React, {PureComponent} from 'react' import PropTypes from "prop-types"; import {Draggable} from "@hello-pangea/dnd"; -import {OverlayTrigger} from "react-bootstrap"; import {DialogPopup} from "./options/DialogPopup.jsx"; import {productStyle} from "./utils"; import {Resources} from "./Resources.jsx"; -import {Warnings} from "./Warnings"; +import {Warnings} from "./Warnings.jsx"; /** * Component that renders a product. @@ -82,20 +81,12 @@ export class ProductCartItem extends PureComponent { first, last, ext_data, - resources, onCardUpdate, } = this.props; - let warning, options, options_data; - if (data && data.warnings) { - const warningsKeys = Object.keys(data.warnings); - if (warningsKeys && warningsKeys.length > 0) { - // we display only the first warning - warning = data.warnings[warningsKeys[0]]; - } - } - - + let options, options_data; + const warnings = data && data.show_warnings; + const resources = data && data.counted_resources; if (data && data.options) { options = data.options; @@ -104,10 +95,6 @@ export class ProductCartItem extends PureComponent { options_data.ext_data = ext_data; } - let render_progress; - if (data) { - - } return ( @@ -132,8 +119,8 @@ export class ProductCartItem extends PureComponent { {/* warning container */}
- {warning && - () + {warnings && warnings.length > 0 && + () } {options && ( diff --git a/static/js/shop/Resources.jsx b/static/js/shop/Resources.jsx index 4304ce90..34b0cf03 100644 --- a/static/js/shop/Resources.jsx +++ b/static/js/shop/Resources.jsx @@ -1,5 +1,6 @@ import {OverlayTrigger} from "react-bootstrap"; import React from "react"; +import {v4 as uuidv4} from "uuid"; const resourcesWidthStyle = (occupied, max) => { @@ -10,7 +11,7 @@ const resourcesWidthStyle = (occupied, max) => { function EEMRenderer({occupied, max}) { return ( -
+
) @@ -18,7 +19,7 @@ function EEMRenderer({occupied, max}) { function ClockRenderer({occupied, max}) { return ( -
+
) @@ -32,15 +33,15 @@ const resource_progress_renderers = { function EEMTipRender({occupied, max}) { - return (

{`${occupied}/${max} EEM connectors used`}

); + return (

{`${occupied}/${max} EEM connectors used`}

); } function IDCTipRender({occupied, max}) { - return (

{`${occupied}/${max} IDC connectors used`}

); + return (

{`${occupied}/${max} IDC connectors used`}

); } function ClockTipRender({occupied, max}) { - return (

{`${occupied}/${max} clock connectors used`}

); + return (

{`${occupied}/${max} clock connectors used`}

); } const resource_tip = { diff --git a/static/js/shop/Shop.jsx b/static/js/shop/Shop.jsx index 70caffc0..a841f10f 100644 --- a/static/js/shop/Shop.jsx +++ b/static/js/shop/Shop.jsx @@ -14,6 +14,8 @@ import {Crate} from "./Crate.jsx"; import {Cart} from "./Cart.jsx"; import {OrderSummary} from "./OrderSummary.jsx"; import {OrderForm} from "./OrderForm.jsx"; +import {TriggerWarnings} from "./warnings"; +import {FillResources} from "./count_resources"; /** * Component that render the entire shop @@ -465,18 +467,18 @@ export class Shop extends PureComponent { crateRules, } = this.state; - const itemsCloned = Array.from(newItems); - const itemsData = []; + let itemsCloned = Array.from(newItems); const rules = {}; itemsCloned.forEach((elem, idx) => { - if (!(idx in itemsData)) itemsData[idx] = elem; + if (!(idx in itemsCloned)) itemsCloned[idx] = elem; if (idx in this.state.columns.cart.itemsData && this.state.columns.cart.itemsData[idx].options_data) { itemsCloned[idx].options_data = this.state.columns.cart.itemsData[idx].options_data; } - itemsData[idx].warnings = {}; }); + itemsCloned = FillResources(itemsCloned); + itemsCloned = TriggerWarnings(itemsCloned); // check number of slot in crate const nbrOccupied = nbrOccupiedSlotsInCrate(newItems); @@ -486,278 +488,6 @@ export class Shop extends PureComponent { rules[crateRules.compactSlot.type] = {...crateRules.compactSlot}; } - - // check the number of EEM connectors available for all Kasli - const idxK = itemsCloned.reduce((prev, next, i) => { - if (next.type === 'kasli' || next.type === 'vhdcicarrier') { - prev.push(i); - } - return prev; - }, []); - for (let i = 0; i <= idxK.length - 1; i++) { - let slots; - let nbUsedSlot = 0; - let nbrCurrentClock = 0; - let idx = idxK[i]; - - if (i !== idxK.length - 1) { - slots = itemsCloned.slice(idx + 1, idxK[i + 1]); - } else { - slots = itemsCloned.slice(idx + 1); - } - - if (i == 0) { - const slots_need_resource = itemsCloned.slice(0, idx); - const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources)); - - if (idx_need != -1) { - if (idx_need in itemsData) { - if ('warnings' in itemsData[idx_need]) { - itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources}; - } else { - itemsData[idx_need].warnings = {}; - itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources}; - } - } else { - itemsData[idx_need] = {...itemsCloned[idx_need]}; - itemsData[idx_need].warnings = {}; - itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources}; - } - } - } - - const process_slots = (item) => { - if (!item.options_data - || item.options_data.ext_pwr === false - || item.options_data.mono_eem === false - ) - return item.slotOccupied; - else if (item.options_data.ext_pwr === true) - return 0; - else if (item.options_data.mono_eem === true || item.options_data.n_eem === "1 EEM") - return 1; - else if (item.options_data.n_eem === "3 EEM") - return 3; - - return item.slotOccupied; - } - - nbUsedSlot = slots - .filter(item => item.type !== 'idc-bnc') - .reduce((prev, next) => { - return prev + process_slots(next); - }, 0); - - nbrCurrentClock = slots - .reduce((prev, next) => { - return next.type === 'clocker' ? prev + ((next.options_data && next.options_data.ext_clk === true) ? 0 : next.clockOccupied) : prev; - }, 0); - - if (idx in itemsData) { - itemsData[idx].nbrCurrentSlot = nbUsedSlot; - itemsData[idx].nbrCurrentClock = nbrCurrentClock; - if (!('warnings' in itemsData[idx])) { - itemsData[idx].warnings = {}; - } - } else { - itemsData[idx] = {...itemsCloned[idx]}; - itemsData[idx].nbrCurrentSlot = nbUsedSlot; - itemsData[idx].nbrCurrentClock = nbrCurrentClock; - itemsData[idx].warnings = {}; - } - - if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) { - if (itemsCloned[idx].rules.maxSlot.message) { - rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot}; - } - itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning}; - } - - if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) { - rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock}; - itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning}; - } - - if (itemsCloned.length > (idx + 1)) { - const ddkali = itemsCloned[idx + 1]; - if (ddkali.type === 'kasli' || ddkali.type === 'vhdcicarrier') { - rules[ddkali.rules.follow.type] = {...ddkali.rules.follow}; - } - } - } - - if (idxK.length === 0) { - const slots_need_resource = itemsCloned.slice(0); - const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources)); - - if (idx_need != -1) { - if (idx_need in itemsData) { - if ('warnings' in itemsData[idx_need]) { - itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources}; - } else { - itemsData[idx_need].warnings = {}; - itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources}; - } - } else { - itemsData[idx_need] = {...itemsCloned[idx_need]}; - itemsData[idx_need].warnings = {}; - itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources}; - } - } - } - - - // check number of clock connector available - const idxC = itemsCloned.reduce((prev, next, i) => { - if (next.type === 'kasli' || next.type === 'clocker') { - prev.push(i); - } - return prev; - }, []); - for (let i = 0; i <= idxC.length - 1; i++) { - let slots; - let nbrCurrentClock = 0; - let idx = idxC[i]; - - if (i !== idxC.length - 1) { - slots = itemsCloned.slice(idx + 1, idxC[i + 1]); - } else { - slots = itemsCloned.slice(idx + 1); - } - - nbrCurrentClock = slots.reduce((prev, next) => { - return prev + ((next.options_data && next.options_data.ext_clk && next.options_data.ext_clk.checked) ? 0 : next.clockOccupied); - }, 0); - - if (idx in itemsData) { - if (itemsData[idx].nbrCurrentClock && itemsData[idx].type !== "clocker") { - itemsData[idx].nbrCurrentClock += nbrCurrentClock; - } else { - itemsData[idx].nbrCurrentClock = nbrCurrentClock; - } - } else { - itemsData[idx] = {...itemsCloned[idx]}; - itemsData[idx].nbrCurrentClock = nbrCurrentClock; - itemsData[idx].warnings = {}; - } - - if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) { - rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock}; - itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning}; - } - } - - - if (itemsCloned.find(elem => elem.type === 'urukul')) { - if (this.state.items['urukul'].rules.info) { - rules[this.state.items['urukul'].rules.info.type] = {...this.state.items['urukul'].rules.info}; - } - } - - - // check if IDC-BNC is correctly positionned (after Zotino or HD68) - const idxIDCBNC = itemsCloned.reduce((prev, next, i) => { - if (next.type === 'idc-bnc') { - prev.push(i); - } - return prev; - }, []); - for (var i = idxIDCBNC.length - 1; i >= 0; i--) { - const ce = idxIDCBNC[i]; - let shouldWarning = false; - - if (ce == 0) { - shouldWarning = true; - } else if (ce >= 1) { - const pe = idxIDCBNC[i] - 1; - if (itemsCloned[pe].type !== 'zotino' && - itemsCloned[pe].type !== 'hd68' && - itemsCloned[pe].type !== 'idc-bnc') { - shouldWarning = true; - } - } - - if (shouldWarning) { - itemsData[ce] = {...itemsCloned[ce]}; - itemsData[ce].warnings = {}; - itemsData[ce].warnings.wrong = {...itemsCloned[ce].rules.wrong}; - } - } - - - // check number of IDC-BNC adapters for a Zotino and HD68-IDC - const idxZH = itemsCloned.reduce((prev, next, i) => { - if (next.type === 'zotino' || next.type === 'hd68') { - prev.push(i); - } - return prev; - }, []); - for (let i = 0; i <= idxZH.length - 1; i++) { - let slots; - let nbUsedSlot = 0; - let idx = idxZH[i]; - - if (i !== idxZH.length - 1) { - slots = itemsCloned.slice(idx + 1, idxZH[i + 1]); - } else { - slots = itemsCloned.slice(idx + 1); - } - - let stopCount = false; - nbUsedSlot = slots.reduce((prev, next, ci, ca) => { - if (ci === 0 && next.type === 'idc-bnc') { - return prev + 1; - } else if (ca[0].type === 'idc-bnc' && ci > 0 && ca[ci - 1].type === 'idc-bnc') { - if (next.type !== 'idc-bnc') { stopCount = true; } - return prev + (next.type === 'idc-bnc' && !stopCount ? 1 : 0); - } - return prev; - }, 0); - - if (idx in itemsData) { - itemsData[idx].nbrCurrentSlot = nbUsedSlot; - if (!('warnings' in itemsData[idx])) { - itemsData[idx].warnings = {}; - } - } else { - itemsData[idx] = {...itemsCloned[idx]}; - itemsData[idx].nbrCurrentSlot = nbUsedSlot; - itemsData[idx].warnings = {}; - } - - if (nbUsedSlot > 0) { - if (itemsCloned[idx].rules.maxSlot.message) { - rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot}; - } - } - if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) { - itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning}; - } - - // check if HD68-IDC has at least 1 IDC-BNC adapter - if (itemsCloned[idx].type === 'hd68') { - let shouldWarning = false; - - if (idx < itemsCloned.length - 1) { - if (itemsCloned[idx + 1].type !== 'idc-bnc') { - shouldWarning = true; - } - } else if (idx === itemsCloned.length - 1) { - shouldWarning = true; - } - - if (shouldWarning) { - if (idx in itemsData) { - itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter}; - } else { - itemsData[idx] = {...itemsCloned[idx]}; - itemsData[idx].warnings = {}; - itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter}; - } - } - } - } - // update state with rules this.setState({ ...this.state, @@ -765,7 +495,7 @@ export class Shop extends PureComponent { ...this.state.columns, cart: { ...this.state.columns.cart, - itemsData: itemsData, + itemsData: itemsCloned, } }, rules: { diff --git a/static/js/shop/Warnings.jsx b/static/js/shop/Warnings.jsx index 1a8bb0c2..f3849f2b 100644 --- a/static/js/shop/Warnings.jsx +++ b/static/js/shop/Warnings.jsx @@ -1,9 +1,10 @@ import {OverlayTrigger} from "react-bootstrap"; import React from "react"; - +import {MaxLevel} from "./warnings"; export function Warnings({warnings}) { + const max_level = MaxLevel(warnings); return ( (
- -

- {warnings.message} -

+ {warnings.map((warning, _i) => { + return ( +

+ {warning.message} +

+ ) + })}
) } rootClose > - +
) } \ No newline at end of file diff --git a/static/js/shop/count_resources.js b/static/js/shop/count_resources.js index ff39334f..f80bc0ab 100644 --- a/static/js/shop/count_resources.js +++ b/static/js/shop/count_resources.js @@ -1,5 +1,5 @@ -export const count_item_occupied_eem = (item) => { +const count_item_occupied_eem = (item) => { if (!item.options_data || item.options_data.ext_pwr === false || item.options_data.mono_eem === false @@ -15,49 +15,38 @@ export const count_item_occupied_eem = (item) => { return item.slotOccupied || 0; } -export const count_item_occupied_clock = (item) => { +const count_item_occupied_clock = (item) => { return (item.options_data && item.options_data.ext_clk === true) ? 0 : (item.clockOccupied || 0); } -export const count_item_occupied_idc = (item) => { +const count_item_occupied_idc = (item) => { return item.idcOccupied || 0; } -function EEMCounter(data, index) { - let count = 0; - for (let i = index + 1; i < data.length; i++) { - if (!!data[i].resources.find((value, _i) => value.name === "eem")) break; - count += count_item_occupied_eem(data[i]); - } - return count; +export const item_occupied_counters = { + "eem": count_item_occupied_eem, + "clk": count_item_occupied_clock, + "idc": count_item_occupied_idc, } -function ClockCounter(data, index) { - let count = 0; - for (let i = index + 1; i < data.length; i++) { - if (!!data[i].resources.find((value, _i) => value.name === "clk")) break; - count += count_item_occupied_clock(data[i]); +function CounterFactory(name) { + return (data, index) => { + let count = 0; + for (let i = index + 1; i < data.length; i++) { + if (data[i].resources && !!data[i].resources.find((value, _i) => value.name === name)) break; + count += item_occupied_counters[name](data[i]); + } + return count; } - return count; } -function IDCCounter(data, index) { - let count = 0; - for (let i = index + 1; i < data.length; i++) { - if (!!data[i].resources.find((value, _i) => value.name === "idc")) break; - count += count_item_occupied_idc(data[i]); - } - return count; -} - - const resource_counters = { - "eem": EEMCounter, - "clk": ClockCounter, - "idc": IDCCounter + "eem": CounterFactory("eem"), + "clk": CounterFactory("clk"), + "idc": CounterFactory("idc") } -export function CountResources(data, index) { +function CountResources(data, index) { if (!data[index].resources) return null; let result = []; data[index].resources.forEach((item, _) => { @@ -68,4 +57,11 @@ export function CountResources(data, index) { }); }); return result; +} + +export function FillResources(data) { + return data.map((element, index) => { + element.counted_resources = CountResources(data, index); + return element; + }) } \ No newline at end of file diff --git a/static/js/shop/options/Options.jsx b/static/js/shop/options/Options.jsx index 8364792d..09937708 100644 --- a/static/js/shop/options/Options.jsx +++ b/static/js/shop/options/Options.jsx @@ -1,3 +1,4 @@ +import React from "react"; import {apply as json_logic_apply} from "json-logic-js"; import {componentsList} from "./components/components"; import {true_type_of} from "./utils"; diff --git a/static/js/shop/warnings.js b/static/js/shop/warnings.js index 6a7b6627..907301f6 100644 --- a/static/js/shop/warnings.js +++ b/static/js/shop/warnings.js @@ -5,11 +5,11 @@ * Second - resources indicator should be separate component */ -import {count_item_occupied_eem, count_item_occupied_clock, count_item_occupied_idc} from "./count_resources"; +import {item_occupied_counters} from "./count_resources"; const Levels = { - "reminder": {priority: 0, icon: '/images/shop/icon-reminder.svg'}, - "warning": {priority: 1, icon: '/images/shop/icon-warning.svg'}, + "reminder": {priority: 1, icon: '/images/shop/icon-reminder.svg'}, + "warning": {priority: 2, icon: '/images/shop/icon-warning.svg'}, } const find_in_counters = (counters, name) => { @@ -17,7 +17,15 @@ const find_in_counters = (counters, name) => { } const find_previous_source = (data, index, source) => { - return data.slice(0, index).find((element) => {element.resources.find((value) => value.name === source)}) + return data.slice(0, index).find((element) => { + return element.resources && find_in_counters(element.resources, source) + }) +} + +const find_next_source_index = (data, index, source) => { + return data.slice(index + 1).findIndex((element) => { + return element.resources && find_in_counters(element.resources, source) + }) + index + 1 } const not_enough_resource_trigger = (name) => { @@ -29,20 +37,31 @@ const not_enough_resource_trigger = (name) => { const no_source_trigger = (name) => { return (data, index, _counters) => { - const occupied = count_item_occupied_eem(data[index]); + const occupied = item_occupied_counters[name](data[index]); if (occupied > 0) - return !!find_previous_source(data, index, name); + return !find_previous_source(data, index, name); return false; } } +const wiring_constraint = (name) => { + return (data, index, _counters) => { + const next = find_next_source_index(data, index, name); + return next - index === 1; + } +} + +const crate_overload = (data, index, _counters) => { + // TODO +} + const Types = { "eem_resource": { level: "warning", trigger: not_enough_resource_trigger("eem"), message: "Insufficient EEM connectors" }, - "no_eem_source" : { + "no_eem_source": { level: "warning", trigger: no_source_trigger("eem"), message: 'This card needs a card that provides a EEM connector (e.g. Kasli) at its left.' @@ -52,7 +71,7 @@ const Types = { trigger: not_enough_resource_trigger("idc"), message: "Insufficient IDC connectors." }, - "no_idc_source" : { + "no_idc_source": { level: "warning", trigger: no_source_trigger("idc"), message: 'Should be after a Zotino or a HD68-IDC or with another IDC-BNC.' @@ -62,10 +81,52 @@ const Types = { trigger: not_enough_resource_trigger("clk"), message: "Insufficient clock connectors." }, - "no_clk_source" : { + "no_clk_source": { level: "warning", trigger: no_source_trigger("clk"), - message: 'This card needs either a card that provides a clock source (e.g. Kasli or Clocker) at its left or use external clock source.' + message: 'This card needs either a card that provides a clock source (e.g. Kasli or Clocker) at its left or use an external clock source.' }, + "eem_wiring_constraint": { + level: "reminder", + trigger: wiring_constraint("eem"), + message: "Due to wiring constraints, the carrier can only connect to EEM cards immediately at its right, without crossing another carrier." + }, + "crate_overload": { + level: "warning", + trigger: crate_overload, + message: 'This card needs either a card that provides a clock source (e.g. Kasli or Clocker) at its left or use an external clock source.' + }, + "default": { + level: "warning", + trigger: (_a, _b, _c) => { + return true; + }, + message: 'This item has unimplemented warning' + } } + +export function TriggerWarnings(data) { + return data.map((element, index) => { + if (!element.warnings) return element; + element.show_warnings = element.warnings + .map((warning, _) => { + if (!!Types[warning]) + return Types[warning].trigger(data, index, element.counted_resources) ? {trigger: undefined, ...Types[warning]} : null; + else + return Types.default.trigger(data, index, element.counted_resources); + }) + .filter((warning, _) => { + return !!warning + }); + return element; + }) +} + +export function MaxLevel(warnings) { + let mx = {priority: 0, icon: null}; + for (const warning of warnings) { + if (Levels[warning.level].priority > mx.priority) mx = Levels[warning.level]; + } + return mx; +} \ No newline at end of file diff --git a/static/js/shop_data.js b/static/js/shop_data.js index 7e9d9d55..92fbff23 100644 --- a/static/js/shop_data.js +++ b/static/js/shop_data.js @@ -91,38 +91,15 @@ const shop_data = { ] } ], - rules: { - maxSlot: { - type: 'kasli-max-slot', - icon: '/shop/icon-reminder.svg', - name: 'Kasli', - message: 'Insufficient EEM connectors.', - }, - maxSlotWarning: { - type: 'kasli-max-slot-warning', - icon: '/shop/icon-warning.svg', - name: 'Kasli', - message: 'Insufficient EEM connectors', - }, - maxClock: { - type: 'kasli-max-clock', - icon: '/shop/icon-reminder.svg', - name: 'Kasli', - message: 'Insufficient clock connectors. Kasli has at most 4 clock connections.', - }, - maxClockWarning: { - type: 'kasli-max-clock-warning', - icon: '/shop/icon-warning.svg', - name: 'Kasli', - message: 'Insufficient clock connectors.', - }, - follow: { - type: 'kasli-follow', - icon: '/shop/icon-reminder.svg', - name: 'Kasli', - message: 'Due to wiring constraints, a Kasli can only connect to EEM cards immediately at its right, without crossing another carrier.', - }, - }, + resources: [ + {name: "eem", max: 5}, + {name: "clk", max: 4}, + ], + warnings: [ + "eem_resource", + "clk_resource", + "eem_wiring_constraint" + ] }, 'kaslisoc': { id: 'kaslisoc', @@ -338,14 +315,9 @@ const shop_data = { ] } ], - rules: { - resources: { - type: 'bnc-dio', - icon: '/shop/icon-warning.svg', - name: 'BNC-DIO', - message: 'This card needs a card that provides a EEM connector (e.g. Kasli) at its left.', - }, - }, + warnings: [ + "no_eem_source" + ] }, 'sma-dio': { id: 'sma-dio', @@ -411,14 +383,9 @@ const shop_data = { nbrClockMax: 0, slotOccupied: 1, clockOccupied: 0, - rules: { - resources: { - type: 'sma-dio', - icon: '/shop/icon-warning.svg', - name: 'SMA-DIO', - message: 'This card needs a card that provides a EEM connector (e.g. Kasli) at its left.', - }, - }, + warnings: [ + "no_eem_source" + ] }, 'mcx-dio': { id: 'mcx-dio',