1<template>
2 <div
3 class="flex gap-2"
4 :class="[
5 {
6 'items-center': !props.isGroup,
7 },
8 ]"
9 >
10 <div
11 class="flex gap-2 w-full"
12 :class="[
13 {
14 'items-center justify-between': !props.isGroup,
15 },
16 ]"
17 >
18 <div :class="'text-end text-base text-ink-gray-5'">
19 <div v-if="props.itemIndex == 0" class="min-w-[66px] text-start">
20 Where
21 </div>
22 <div v-else class="min-w-[66px] flex items-start">
23 <Button
24 variant="subtle"
25 class="w-max"
26 @click="toggleConjunction"
27 icon-right="lucide-refresh-cw"
28 :disabled="props.itemIndex > 2"
29 :label="conjunction"
30 />
31 </div>
32 </div>
33 <div v-if="!props.isGroup" class="flex items-center gap-2 w-full">
34 <div id="fieldname" class="flex-1 min-w-0">
35 <Combobox
36 class="w-full"
37 trigger="button"
38 :options="filterableFields.data || []"
39 :model-value="props.condition[0]"
40 :placeholder="__('Field')"
41 @update:selected-option="updateField"
42 />
43 </div>
44 <div id="operator" class="flex-1 min-w-0">
45 <FormControl
46 v-if="!props.condition[0]"
47 disabled
48 type="text"
49 :placeholder="'operator'"
50 class="w-full"
51 />
52 <FormControl
53 v-else
54 :disabled="!props.condition[0]"
55 type="select"
56 v-model="props.condition[1]"
57 @change="updateOperator"
58 :options="getOperators()"
59 class="w-full"
60 />
61 </div>
62 <div id="value" class="flex-1 min-w-0">
63 <FormControl
64 v-if="!props.condition[0]"
65 disabled
66 type="text"
67 :placeholder="'condition'"
68 class="w-full"
69 />
70 <component
71 v-else
72 :is="getValueControl()"
73 v-model="props.condition[2]"
74 @change="updateValue"
75 :placeholder="'condition'"
76 />
77 </div>
78 </div>
79 <CFConditions
80 v-if="props.isGroup && !(props.level == 2 || props.level == 4)"
81 :conditions="props.condition"
82 :isChild="true"
83 :level="props.level"
84 :disableAddCondition="props.disableAddCondition"
85 />
86 <Button
87 variant="outline"
88 v-if="props.isGroup && (props.level == 2 || props.level == 4)"
89 @click="show = true"
90 label="Open nested conditions"
91 />
92 </div>
93 <div :class="'w-max'">
94 <Dropdown align="end" :options="dropdownOptions">
95 <Button variant="ghost" icon="lucide-more-horizontal" />
96 </Dropdown>
97 </div>
98 </div>
99 <Dialog v-model:open="show" size="3xl" title="Nested conditions">
100 <template #default>
101 <CFConditions
102 :conditions="props.condition"
103 :isChild="true"
104 :level="props.level"
105 :disableAddCondition="props.disableAddCondition"
106 />
107 </template>
108 </Dialog>
109</template>
110
111<script setup lang="ts">
112import { StarRating } from "@/components";
113import { Link } from "@framework/ui";
114import {
115 Button,
116 Combobox,
117 DatePicker,
118 DateRangePicker,
119 DateTimePicker,
120 Dialog,
121 Dropdown,
122 FormControl,
123} from "frappe-ui";
124import { computed, h, ref } from "vue";
125import GroupIcon from "~icons/lucide/group";
126import UnGroupIcon from "~icons/lucide/ungroup";
127import CFConditions from "./CFConditions.vue";
128import { filterableFields } from "./filterableFields";
129
130const show = ref(false);
131const emit = defineEmits([
132 "remove",
133 "unGroupConditions",
134 "toggleConjunction",
135 "turnIntoGroup",
136]);
137
138const props = defineProps({
139 condition: {
140 type: Array<any>,
141 required: true,
142 },
143 isChild: {
144 type: Boolean,
145 default: false,
146 },
147 itemIndex: {
148 type: Number,
149 },
150 level: {
151 type: Number,
152 default: 0,
153 },
154 isGroup: {
155 type: Boolean,
156 default: false,
157 },
158 conjunction: {
159 type: String,
160 },
161 disableAddCondition: {
162 type: Boolean,
163 default: false,
164 },
165});
166
167const dropdownOptions = computed(() => {
168 const options = [];
169
170 if (!props.isGroup && props.level < 4) {
171 options.push({
172 label: "Turn into a group",
173 icon: () => h(GroupIcon),
174 onClick: () => {
175 emit("turnIntoGroup");
176 },
177 });
178 }
179
180 if (props.isGroup) {
181 options.push({
182 label: "Ungroup conditions",
183 icon: () => h(UnGroupIcon),
184 onClick: () => {
185 emit("unGroupConditions");
186 },
187 });
188 }
189
190 options.push({
191 label: "Remove",
192 icon: "lucide-trash-2",
193 theme: "red",
194 onClick: () => {
195 emit("remove");
196 },
197 condition: () => !props.isGroup,
198 });
199
200 options.push({
201 label: "Remove group",
202 icon: "lucide-trash-2",
203 theme: "red",
204 onClick: () => {
205 emit("remove");
206 },
207 condition: () => props.isGroup,
208 });
209
210 return options;
211});
212
213const typeCheck = ["Check"];
214const typeLink = ["Link", "Dynamic Link"];
215const typeNumber = ["Float", "Int", "Currency", "Percent"];
216const typeSelect = ["Select"];
217const typeString = ["Data", "Long Text", "Small Text", "Text Editor", "Text"];
218const typeDate = ["Date", "Datetime"];
219const typeRating = ["Rating"];
220
221function toggleConjunction() {
222 emit("toggleConjunction", props.conjunction);
223}
224
225const updateField = (field) => {
226 props.condition[0] = field?.fieldname;
227 resetConditionValue();
228};
229
230const resetConditionValue = () => {
231 props.condition[2] = "";
232};
233
234function getValueControl() {
235 const [field, operator] = props.condition;
236 if (!field) return null;
237 const fieldData = filterableFields.data?.find((f) => f.fieldname == field);
238 if (!fieldData) return null;
239 const { fieldtype, options } = fieldData;
240 if (operator == "is") {
241 return h(FormControl, {
242 type: "select",
243 options: [
244 {
245 label: "Set",
246 value: "set",
247 },
248 {
249 label: "Not Set",
250 value: "not set",
251 },
252 ],
253 });
254 } else if (["like", "not like", "in", "not in"].includes(operator)) {
255 return h(FormControl, { type: "text" });
256 } else if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
257 const _options =
258 fieldtype == "Check" ? ["Yes", "No"] : getSelectOptions(options);
259 return h(FormControl, {
260 type: "select",
261 options: _options.map((o) => ({
262 label: o,
263 value: o,
264 })),
265 });
266 } else if (typeLink.includes(fieldtype)) {
267 if (fieldtype == "Dynamic Link") {
268 return h(FormControl, { type: "text" });
269 }
270 return h(Link, {
271 class: "w-full",
272 doctype: options,
273 title: props.condition[2],
274 });
275 } else if (typeNumber.includes(fieldtype)) {
276 return h(FormControl, { type: "number" });
277 } else if (typeDate.includes(fieldtype) && operator == "between") {
278 return h(DateRangePicker);
279 } else if (typeDate.includes(fieldtype)) {
280 return h(fieldtype == "Date" ? DatePicker : DateTimePicker);
281 } else if (typeRating.includes(fieldtype)) {
282 return h(StarRating, {
283 rating: props.condition[2] || 0,
284 static: false,
285 class: "truncate",
286 "onUpdate:modelValue": (v) => updateValue(v),
287 });
288 } else {
289 return h(FormControl, { type: "text" });
290 }
291}
292
293function updateValue(value) {
294 value = value.target ? value.target.value : value;
295 if (props.condition[1] === "between") {
296 // DateRangePicker emits [from, to]; the text controls still emit "from,to"
297 const range = Array.isArray(value) ? value : String(value).split(",");
298 props.condition[2] = [range[0], range[1]];
299 } else {
300 props.condition[2] = value + "";
301 }
302}
303
304function getSelectOptions(options) {
305 return options.split("\n");
306}
307
308function updateOperator(event) {
309 let oldOperatorValue = event.target._value;
310 let newOperatorValue = event.target.value;
311 props.condition[1] = event.target.value;
312 if (!isSameTypeOperator(oldOperatorValue, newOperatorValue)) {
313 props.condition[2] = getDefaultValue(props.condition[0]);
314 }
315 resetConditionValue();
316}
317
318function getOperators() {
319 let options = [];
320 const field = props.condition[0];
321 if (!field) return options;
322 const fieldData = filterableFields.data?.find((f) => f.fieldname == field);
323 if (!fieldData) return options;
324 const { fieldtype, fieldname } = fieldData;
325 if (typeString.includes(fieldtype)) {
326 options.push(
327 ...[
328 { label: "Equals", value: "==" },
329 { label: "Not Equals", value: "!=" },
330 { label: "Like", value: "like" },
331 { label: "Not Like", value: "not like" },
332 { label: "In", value: "in" },
333 { label: "Not In", value: "not in" },
334 { label: "Is", value: "is" },
335 ]
336 );
337 }
338 if (fieldname === "_assign") {
339 options = [
340 { label: "Like", value: "like" },
341 { label: "Not Like", value: "not like" },
342 { label: "Is", value: "is" },
343 ];
344 }
345 if (typeNumber.includes(fieldtype)) {
346 options.push(
347 ...[
348 { label: "Equals", value: "==" },
349 { label: "Not Equals", value: "!=" },
350 { label: "Like", value: "like" },
351 { label: "Not Like", value: "not like" },
352 { label: "In", value: "in" },
353 { label: "Not In", value: "not in" },
354 { label: "Is", value: "is" },
355 { label: "<", value: "<" },
356 { label: ">", value: ">" },
357 { label: "<=", value: "<=" },
358 { label: ">=", value: ">=" },
359 ]
360 );
361 }
362 if (typeSelect.includes(fieldtype)) {
363 options.push(
364 ...[
365 { label: "Equals", value: "==" },
366 { label: "Not Equals", value: "!=" },
367 { label: "In", value: "in" },
368 { label: "Not In", value: "not in" },
369 { label: "Is", value: "is" },
370 ]
371 );
372 }
373 if (typeLink.includes(fieldtype)) {
374 options.push(
375 ...[
376 { label: "Equals", value: "==" },
377 { label: "Not Equals", value: "!=" },
378 { label: "Like", value: "like" },
379 { label: "Not Like", value: "not like" },
380 { label: "In", value: "in" },
381 { label: "Not In", value: "not in" },
382 { label: "Is", value: "is" },
383 ]
384 );
385 }
386 if (typeCheck.includes(fieldtype)) {
387 options.push(...[{ label: "Equals", value: "==" }]);
388 }
389 if (["Duration"].includes(fieldtype)) {
390 options.push(
391 ...[
392 { label: "Like", value: "like" },
393 { label: "Not Like", value: "not like" },
394 { label: "In", value: "in" },
395 { label: "Not In", value: "not in" },
396 { label: "Is", value: "is" },
397 ]
398 );
399 }
400 if (typeDate.includes(fieldtype)) {
401 options.push(
402 ...[
403 { label: "Equals", value: "==" },
404 { label: "Not Equals", value: "!=" },
405 { label: "Is", value: "is" },
406 { label: ">", value: ">" },
407 { label: "<", value: "<" },
408 { label: ">=", value: ">=" },
409 { label: "<=", value: "<=" },
410 { label: "Between", value: "between" },
411 ]
412 );
413 }
414 if (typeRating.includes(fieldtype)) {
415 options.push(
416 ...[
417 { label: "Equals", value: "==" },
418 { label: "Not Equals", value: "!=" },
419 { label: "Is", value: "is" },
420 { label: ">", value: ">" },
421 { label: "<", value: "<" },
422 { label: ">=", value: ">=" },
423 { label: "<=", value: "<=" },
424 ]
425 );
426 }
427 const op = options.find((o) => o.value == props.condition[1]);
428 props.condition[1] = op?.value || options[0].value;
429 return options;
430}
431
432function getDefaultValue(field) {
433 if (typeSelect.includes(field.fieldtype)) {
434 return getSelectOptions(field.options)[0];
435 }
436 if (typeCheck.includes(field.fieldtype)) {
437 return "Yes";
438 }
439 if (typeDate.includes(field.fieldtype)) {
440 return null;
441 }
442 if (typeRating.includes(field.fieldtype)) {
443 return 0;
444 }
445 return "";
446}
447
448function isSameTypeOperator(oldOperator, newOperator) {
449 let textOperators = ["==", "!=", "in", "not in", ">", "<", ">=", "<="];
450 if (
451 textOperators.includes(oldOperator) &&
452 textOperators.includes(newOperator)
453 )
454 return true;
455 return false;
456}
457</script>
458