mcai_workflow/components/
edit_start_parameter.rs1use 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}