mcai_workflow/components/
edit_start_parameter.rs

1use crate::{
2  components::modal::ActionButton, Button, ChoiceItem, ChoiceItemMessage, Modal, ModalMessage,
3};
4use css_in_rust_next::Style;
5use mcai_models::{ListItem, StartParameter, StartParameterType};
6use std::str::FromStr;
7use wasm_bindgen::JsCast;
8use web_sys::{Event, EventTarget, HtmlInputElement, HtmlSelectElement, InputEvent};
9use yew::{html, Callback, Component, Context, Html, Properties};
10use yew_feather::plus::Plus;
11
12#[derive(PartialEq, Properties)]
13pub struct EditStartParameterProperties {
14  pub event: Callback<EditStartParameterMessage>,
15  pub start_parameter: Option<StartParameter>,
16}
17
18pub enum EditStartParameterMessage {
19  Submit(StartParameter),
20  Cancel,
21  Delete,
22}
23
24pub enum Field {
25  Accept,
26  Label,
27  Id,
28  Kind,
29  Step,
30  WorkDir,
31  ListItemLabel,
32  ListItemId,
33}
34
35pub enum InternalMessage {
36  Update(Field, String),
37  UpdateRequired(bool),
38  UpdateChoice(usize, String, String),
39  Modal(ModalMessage),
40  AddChoice,
41  DeleteChoice(usize),
42}
43
44pub struct EditStartParameter {
45  style: Style,
46  label: String,
47  id: String,
48  kind: Option<StartParameterType>,
49  required: bool,
50  step: Option<f32>,
51  work_dir: Option<String>,
52  accept: Option<String>,
53  list_items: Vec<ListItem>,
54}
55
56impl EditStartParameter {
57  fn is_valid(&self) -> bool {
58    !self.id.is_empty()
59      && !self.label.is_empty()
60      && self.kind.is_some()
61      && !self
62        .list_items
63        .iter()
64        .any(|item| item.id.is_empty() || item.label.is_empty())
65  }
66}
67
68impl Component for EditStartParameter {
69  type Message = InternalMessage;
70  type Properties = EditStartParameterProperties;
71
72  fn create(ctx: &Context<Self>) -> Self {
73    let style = Style::create(
74      "Component",
75      concat!(include_str!("edit_start_parameter.css")),
76    )
77    .unwrap();
78
79    if let Some(start_parameter) = &ctx.props().start_parameter {
80      EditStartParameter {
81        style,
82        label: start_parameter.label.clone(),
83        id: start_parameter.id.clone(),
84        kind: Some(start_parameter.kind.clone()),
85        required: start_parameter.required,
86        step: start_parameter.step,
87        work_dir: start_parameter.work_dir.clone(),
88        accept: start_parameter.accept.clone(),
89        list_items: start_parameter.items.clone(),
90      }
91    } else {
92      EditStartParameter {
93        style,
94        label: String::new(),
95        id: String::new(),
96        kind: None,
97        required: false,
98        step: None,
99        work_dir: None,
100        accept: None,
101        list_items: vec![],
102      }
103    }
104  }
105
106  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
107    match msg {
108      InternalMessage::Update(field, value) => match field {
109        Field::Label => self.label = value,
110        Field::Id => self.id = value,
111        Field::Kind => {
112          self.kind = match value.as_str() {
113            "file" => Some(StartParameterType::File),
114            "choice" => Some(StartParameterType::Choice),
115            "string" => Some(StartParameterType::String),
116            "number" => Some(StartParameterType::Number),
117            _ => None,
118          };
119          self.accept = None;
120          self.step = None;
121          self.list_items = vec![];
122        }
123        Field::Step => self.step = Some(f32::from_str(value.as_str()).unwrap_or_default()),
124        Field::WorkDir => self.work_dir = Some(value),
125        Field::Accept => self.accept = Some(value),
126        Field::ListItemId => {}
127        Field::ListItemLabel => {}
128      },
129      InternalMessage::UpdateRequired(required) => self.required = required,
130      InternalMessage::Modal(message) => match message {
131        ModalMessage::Submit | ModalMessage::Update => {
132          let start_parameter = StartParameter {
133            accept: self.accept.clone(),
134            id: self.id.clone(),
135            label: self.label.clone(),
136            kind: self.kind.clone().unwrap(),
137            default: None,
138            value: None,
139            required: self.required,
140            icon: None,
141            step: self.step,
142            work_dir: self.work_dir.clone(),
143            items: self.list_items.clone(),
144          };
145
146          ctx
147            .props()
148            .event
149            .emit(EditStartParameterMessage::Submit(start_parameter));
150        }
151        ModalMessage::Cancel => {
152          ctx.props().event.emit(EditStartParameterMessage::Cancel);
153        }
154        ModalMessage::Delete => {
155          ctx.props().event.emit(EditStartParameterMessage::Delete);
156        }
157      },
158      InternalMessage::UpdateChoice(index, id, label) => {
159        if let Some(item) = self.list_items.get_mut(index) {
160          item.id = id;
161          item.label = label;
162        }
163      }
164      InternalMessage::AddChoice => self.list_items.push(ListItem {
165        id: String::new(),
166        label: String::new(),
167      }),
168      InternalMessage::DeleteChoice(index) => {
169        self.list_items.remove(index);
170      }
171    }
172    true
173  }
174
175  fn view(&self, ctx: &Context<Self>) -> Html {
176    let action_buttons = if ctx.props().start_parameter.is_some() {
177      vec![ActionButton::Delete, ActionButton::Update(self.is_valid())]
178    } else {
179      vec![ActionButton::Submit(self.is_valid())]
180    };
181
182    let callback_accept_extra = ctx.link().batch_callback(|e: Event| {
183      let target: Option<EventTarget> = e.target();
184      let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
185      input.map(|input| InternalMessage::Update(Field::WorkDir, input.value()))
186    });
187
188    let callback_step_extra = ctx.link().batch_callback(|e: Event| {
189      let target: Option<EventTarget> = e.target();
190      let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
191      input.map(|input| InternalMessage::Update(Field::Step, input.value()))
192    });
193
194    let extra_field = {
195      if let Some(kind) = &self.kind {
196        match kind {
197          StartParameterType::Choice => {
198            let choice_items: Html = self.list_items.iter().enumerate().map(|(index, item)|
199              html!(
200                <ChoiceItem id={item.id.clone()} label={item.label.clone()} event={ctx.link().callback(move |message| match message {
201                  ChoiceItemMessage::Update(id, label) => InternalMessage::UpdateChoice(index, id, label),
202                  ChoiceItemMessage::Delete => InternalMessage::DeleteChoice(index),
203                })}/>
204              )
205            ).collect();
206
207            html!(
208              <>
209                <label>
210                  {"List of available choices"}
211                </label>
212                { choice_items }
213                <div>
214                  <Button
215                    label="Add new choice value"
216                    icon={html!(<Plus />)}
217                    onclick={ctx.link().callback(|_| InternalMessage::AddChoice)}
218                    />
219                </div>
220              </>
221            )
222          }
223          StartParameterType::File => {
224            html!(
225              <>
226              <div class="field">
227                <label>
228                  {"Accept"}
229                </label>
230                <span>
231                  <input type="text" value={self.accept.clone()} onchange={callback_accept_extra} />
232                </span>
233              </div>
234              </>
235            )
236          }
237          StartParameterType::Number => {
238            html!(
239              <>
240              <div class="field">
241                <label>
242                  {"Step of the float value"}
243                </label>
244                <span>
245                  <input type="number"
246                    value={self.step.map(|step| step.to_string())}
247                    onchange={callback_step_extra}
248                    />
249                </span>
250              </div>
251              </>
252            )
253          }
254          _ => {
255            html!()
256          }
257        }
258      } else {
259        html!()
260      }
261    };
262
263    let callback_id = ctx.link().batch_callback(|e: InputEvent| {
264      let target: Option<EventTarget> = e.target();
265      let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
266      input.map(|input| InternalMessage::Update(Field::Id, input.value()))
267    });
268
269    let callback_kind = ctx.link().batch_callback(|e: Event| {
270      let target: Option<EventTarget> = e.target();
271      let input = target.and_then(|t| t.dyn_into::<HtmlSelectElement>().ok());
272      input.map(|input| InternalMessage::Update(Field::Kind, input.value()))
273    });
274
275    let callback_label = ctx.link().batch_callback(|e: InputEvent| {
276      let target: Option<EventTarget> = e.target();
277      let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
278      input.map(|input| InternalMessage::Update(Field::Label, input.value()))
279    });
280
281    let callback_required = ctx.link().batch_callback(|e: Event| {
282      let target: Option<EventTarget> = e.target();
283      let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
284      input.map(|input| InternalMessage::UpdateRequired(input.checked()))
285    });
286
287    let callback_work_dir = ctx.link().batch_callback(|e: Event| {
288      let target: Option<EventTarget> = e.target();
289      let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
290      input.map(|input| InternalMessage::Update(Field::WorkDir, input.value()))
291    });
292
293    let input_field = html!(
294      <>
295      <div class="field">
296        <label>
297          {"Id"}
298        </label>
299        <span>
300          <input type="text" value={self.id.clone()} oninput={callback_id} />
301        </span>
302      </div>
303      <div class="field">
304        <label>
305          {"Label"}
306        </label>
307        <span>
308          <input type="text" value={self.label.clone()} oninput={callback_label} />
309        </span>
310      </div>
311      <div class="field">
312        <label>
313          {"Kind"}
314        </label>
315        <span>
316          <select onchange={callback_kind.clone()}>
317            <option value="" selected={self.kind.is_none()}>{"-- Select a type --"}</option>
318            <option value="file" selected={self.kind == Some(StartParameterType::File)}>{"File"}</option>
319            <option value="choice" selected={self.kind == Some(StartParameterType::Choice)}>{"Choice"}</option>
320            <option value="string" selected={self.kind == Some(StartParameterType::String)}>{"String"}</option>
321            <option value="number" selected={self.kind == Some(StartParameterType::Number)}>{"Number"}</option>
322          </select>
323        </span>
324      </div>
325      <div class="field">
326        <label>
327          {"Required"}
328        </label>
329        <span>
330          <input type="checkbox" checked={self.required} onchange={callback_required} />
331        </span>
332      </div>
333      <div class="field">
334        <label>
335          {"WorkDir"}
336        </label>
337        <span>
338          <input type="text" value={self.work_dir.clone()} onchange={callback_work_dir} />
339        </span>
340      </div>
341
342      {extra_field}
343      </>
344    );
345
346    html!(
347      <Modal
348        event={ctx.link().callback(InternalMessage::Modal)}
349        height="50vh" width="19vw"
350        modal_title={"Edit Start Parameter"}
351        actions={action_buttons}>
352        <div class={self.style.clone()}>
353          <div class="editInputStartParameter">
354            {input_field}
355          </div>
356        </div>
357      </Modal>
358    )
359  }
360}