1use askama::Template;
5use serde::{Deserialize, Serialize};
6
7use crate::field::{
8 FieldCx, FieldRegistry, FieldType, FieldVm, OptionsError, ResolvedOptions, SubmittedField,
9};
10use crate::html::Markup;
11use laterite_core::validation::Mode;
12use laterite_core::AttrValue;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum MediaMode {
18 #[default]
20 File,
21 Image,
23}
24
25#[derive(Debug, Deserialize, Default)]
26struct MediaOptions {
27 #[serde(default)]
28 mode: MediaMode,
29 #[serde(default = "one")]
32 max: u32,
33}
34
35fn one() -> u32 {
36 1
37}
38
39#[derive(Debug, Clone)]
40pub(crate) struct MediaResolved {
41 mode: MediaMode,
42 max: u32,
43}
44
45#[derive(Serialize, Deserialize, Default)]
46struct MediaData {
47 pick_url: String,
48 resolve_url: String,
49 mode: MediaMode,
50 max: u32,
51 select: String,
53 replace: String,
54 clear: String,
55 use_selected: String,
56 title: String,
57}
58
59#[derive(Template)]
60#[template(path = "fields/media.html")]
61struct MediaTmpl<'a> {
62 name: &'a str,
63 id: &'a str,
64 value: &'a str,
65 pick_url: &'a str,
66 resolve_url: &'a str,
67 mode: &'a str,
68 max: u32,
69 multiple: bool,
70 words: &'a MediaData,
71}
72
73pub struct MediaField;
75
76fn ids_in(value: &str) -> Vec<i64> {
78 let value = value.trim();
79 if let Ok(serde_json::Value::Array(items)) = serde_json::from_str::<serde_json::Value>(value) {
80 return items.iter().filter_map(|v| v.as_i64()).collect();
81 }
82 value
83 .split(',')
84 .filter_map(|p| p.trim().parse::<i64>().ok())
85 .collect()
86}
87
88impl FieldType for MediaField {
89 fn view_key(&self) -> &'static str {
90 "media"
91 }
92
93 fn option_keys(&self, _raw: &serde_json::Value) -> Option<Vec<String>> {
94 Some(vec!["mode".to_string(), "max".to_string()])
95 }
96
97 fn resolve_options(
98 &self,
99 raw: &serde_json::Value,
100 _types: &FieldRegistry,
101 ) -> Result<ResolvedOptions, OptionsError> {
102 let opts: MediaOptions = if raw.is_null() {
103 MediaOptions::default()
104 } else {
105 serde_json::from_value(raw.clone()).map_err(|e| OptionsError(e.to_string()))?
106 };
107 Ok(ResolvedOptions::new(MediaResolved {
108 mode: opts.mode,
109 max: opts.max,
110 }))
111 }
112
113 fn view_model(&self, cx: &FieldCx<'_>) -> FieldVm {
114 let resolved = cx.opts.get::<MediaResolved>();
115 let data = MediaData {
116 pick_url: format!("{}/media/pick", cx.base),
117 resolve_url: format!("{}/media/resolve", cx.base),
118 mode: resolved.map(|r| r.mode).unwrap_or_default(),
119 max: resolved.map(|r| r.max).unwrap_or(1),
120 select: cx.i18n.t(&laterite_core::t!("Select")),
121 replace: cx.i18n.t(&laterite_core::t!("Replace")),
122 clear: cx.i18n.t(&laterite_core::t!("Clear")),
123 use_selected: cx.i18n.t(&laterite_core::t!("Use selected")),
124 title: cx.i18n.t(&laterite_core::t!("Choose from the library")),
125 };
126 FieldVm {
127 view_key: "media".to_string(),
128 name: cx.name.to_string(),
129 id: cx.id.to_string(),
130 label: cx.label.to_string(),
131 required: cx.required,
132 value: cx.value.clone(),
133 data: serde_json::to_value(data).unwrap_or_default(),
134 }
135 }
136
137 fn render_default(&self, vm: &FieldVm) -> Markup {
138 let data: MediaData = serde_json::from_value(vm.data.clone()).unwrap_or_default();
139 let value: String = ids_in(vm.value.as_text())
140 .iter()
141 .map(|i| i.to_string())
142 .collect::<Vec<_>>()
143 .join(",");
144 Markup::from_template(&MediaTmpl {
145 name: &vm.name,
146 id: &vm.id,
147 value: &value,
148 pick_url: &data.pick_url,
149 resolve_url: &data.resolve_url,
150 mode: match data.mode {
151 MediaMode::Image => "image",
152 MediaMode::File => "file",
153 },
154 max: data.max,
155 multiple: data.max != 1,
156 words: &data,
157 })
158 .unwrap_or_default()
159 }
160
161 fn to_attr(
163 &self,
164 field: &SubmittedField<'_>,
165 opts: &ResolvedOptions,
166 _mode: Mode,
167 ) -> Result<Option<AttrValue>, String> {
168 let max = opts.get::<MediaResolved>().map(|r| r.max).unwrap_or(1);
169 Ok(field.value().map(|v| {
170 let mut ids = ids_in(v);
171 if max > 1 {
172 ids.truncate(max as usize);
173 }
174 if max == 1 {
175 match ids.first() {
176 Some(id) => AttrValue::Int(*id),
177 None => AttrValue::Null,
178 }
179 } else {
180 AttrValue::Json(serde_json::Value::Array(
181 ids.into_iter().map(serde_json::Value::from).collect(),
182 ))
183 }
184 }))
185 }
186
187 fn to_control(&self, stored: Option<&str>, _opts: &ResolvedOptions) -> String {
188 ids_in(stored.unwrap_or_default())
189 .iter()
190 .map(|i| i.to_string())
191 .collect::<Vec<_>>()
192 .join(",")
193 }
194
195 fn linked_media(&self, submitted: Option<&str>, opts: &ResolvedOptions) -> Option<Vec<i64>> {
196 let max = opts.get::<MediaResolved>().map(|r| r.max).unwrap_or(1);
197 let mut ids = ids_in(submitted.unwrap_or_default());
198 if max >= 1 {
199 ids.truncate(max as usize);
200 }
201 Some(ids)
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn ids_come_from_a_list_or_a_comma_string() {
211 assert_eq!(ids_in("3, 7"), vec![3, 7]);
212 assert_eq!(ids_in("[3,7]"), vec![3, 7]);
213 assert_eq!(ids_in(""), Vec::<i64>::new());
214 assert_eq!(ids_in("x"), Vec::<i64>::new());
215 }
216}