IKHSAN
ENID Baca dalam Bahasa Indonesia

opendesk

Public
Branch
main
Latest commit
08d9f3328 October 2026

CFCondition.vue

Open raw
<template>
  <div
    class="flex gap-2"
    :class="[
      {
        'items-center': !props.isGroup,
      },
    ]"
  >
    <div
      class="flex gap-2 w-full"
      :class="[
        {
          'items-center justify-between': !props.isGroup,
        },
      ]"
    >
      <div :class="'text-end text-base text-ink-gray-5'">
        <div v-if="props.itemIndex == 0" class="min-w-[66px] text-start">
          Where
        </div>
        <div v-else class="min-w-[66px] flex items-start">
          <Button
            variant="subtle"
            class="w-max"
            @click="toggleConjunction"
            icon-right="lucide-refresh-cw"
            :disabled="props.itemIndex > 2"
            :label="conjunction"
          />
        </div>
      </div>
      <div v-if="!props.isGroup" class="flex items-center gap-2 w-full">
        <div id="fieldname" class="flex-1 min-w-0">
          <Combobox
            class="w-full"
            trigger="button"
            :options="filterableFields.data || []"
            :model-value="props.condition[0]"
            :placeholder="__('Field')"
            @update:selected-option="updateField"
          />
        </div>
        <div id="operator" class="flex-1 min-w-0">
          <FormControl
            v-if="!props.condition[0]"
            disabled
            type="text"
            :placeholder="'operator'"
            class="w-full"
          />
          <FormControl
            v-else
            :disabled="!props.condition[0]"
            type="select"
            v-model="props.condition[1]"
            @change="updateOperator"
            :options="getOperators()"
            class="w-full"
          />
        </div>
        <div id="value" class="flex-1 min-w-0">
          <FormControl
            v-if="!props.condition[0]"
            disabled
            type="text"
            :placeholder="'condition'"
            class="w-full"
          />
          <component
            v-else
            :is="getValueControl()"
            v-model="props.condition[2]"
            @change="updateValue"
            :placeholder="'condition'"
          />
        </div>
      </div>
      <CFConditions
        v-if="props.isGroup && !(props.level == 2 || props.level == 4)"
        :conditions="props.condition"
        :isChild="true"
        :level="props.level"
        :disableAddCondition="props.disableAddCondition"
      />
      <Button
        variant="outline"
        v-if="props.isGroup && (props.level == 2 || props.level == 4)"
        @click="show = true"
        label="Open nested conditions"
      />
    </div>
    <div :class="'w-max'">
      <Dropdown align="end" :options="dropdownOptions">
        <Button variant="ghost" icon="lucide-more-horizontal" />
      </Dropdown>
    </div>
  </div>
  <Dialog v-model:open="show" size="3xl" title="Nested conditions">
    <template #default>
      <CFConditions
        :conditions="props.condition"
        :isChild="true"
        :level="props.level"
        :disableAddCondition="props.disableAddCondition"
      />
    </template>
  </Dialog>
</template>

<script setup lang="ts">
import { StarRating } from "@/components";
import { Link } from "@framework/ui";
import {
  Button,
  Combobox,
  DatePicker,
  DateRangePicker,
  DateTimePicker,
  Dialog,
  Dropdown,
  FormControl,
} from "frappe-ui";
import { computed, h, ref } from "vue";
import GroupIcon from "~icons/lucide/group";
import UnGroupIcon from "~icons/lucide/ungroup";
import CFConditions from "./CFConditions.vue";
import { filterableFields } from "./filterableFields";

const show = ref(false);
const emit = defineEmits([
  "remove",
  "unGroupConditions",
  "toggleConjunction",
  "turnIntoGroup",
]);

const props = defineProps({
  condition: {
    type: Array<any>,
    required: true,
  },
  isChild: {
    type: Boolean,
    default: false,
  },
  itemIndex: {
    type: Number,
  },
  level: {
    type: Number,
    default: 0,
  },
  isGroup: {
    type: Boolean,
    default: false,
  },
  conjunction: {
    type: String,
  },
  disableAddCondition: {
    type: Boolean,
    default: false,
  },
});

const dropdownOptions = computed(() => {
  const options = [];

  if (!props.isGroup && props.level < 4) {
    options.push({
      label: "Turn into a group",
      icon: () => h(GroupIcon),
      onClick: () => {
        emit("turnIntoGroup");
      },
    });
  }

  if (props.isGroup) {
    options.push({
      label: "Ungroup conditions",
      icon: () => h(UnGroupIcon),
      onClick: () => {
        emit("unGroupConditions");
      },
    });
  }

  options.push({
    label: "Remove",
    icon: "lucide-trash-2",
    theme: "red",
    onClick: () => {
      emit("remove");
    },
    condition: () => !props.isGroup,
  });

  options.push({
    label: "Remove group",
    icon: "lucide-trash-2",
    theme: "red",
    onClick: () => {
      emit("remove");
    },
    condition: () => props.isGroup,
  });

  return options;
});

const typeCheck = ["Check"];
const typeLink = ["Link", "Dynamic Link"];
const typeNumber = ["Float", "Int", "Currency", "Percent"];
const typeSelect = ["Select"];
const typeString = ["Data", "Long Text", "Small Text", "Text Editor", "Text"];
const typeDate = ["Date", "Datetime"];
const typeRating = ["Rating"];

function toggleConjunction() {
  emit("toggleConjunction", props.conjunction);
}

const updateField = (field) => {
  props.condition[0] = field?.fieldname;
  resetConditionValue();
};

const resetConditionValue = () => {
  props.condition[2] = "";
};

function getValueControl() {
  const [field, operator] = props.condition;
  if (!field) return null;
  const fieldData = filterableFields.data?.find((f) => f.fieldname == field);
  if (!fieldData) return null;
  const { fieldtype, options } = fieldData;
  if (operator == "is") {
    return h(FormControl, {
      type: "select",
      options: [
        {
          label: "Set",
          value: "set",
        },
        {
          label: "Not Set",
          value: "not set",
        },
      ],
    });
  } else if (["like", "not like", "in", "not in"].includes(operator)) {
    return h(FormControl, { type: "text" });
  } else if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
    const _options =
      fieldtype == "Check" ? ["Yes", "No"] : getSelectOptions(options);
    return h(FormControl, {
      type: "select",
      options: _options.map((o) => ({
        label: o,
        value: o,
      })),
    });
  } else if (typeLink.includes(fieldtype)) {
    if (fieldtype == "Dynamic Link") {
      return h(FormControl, { type: "text" });
    }
    return h(Link, {
      class: "w-full",
      doctype: options,
      title: props.condition[2],
    });
  } else if (typeNumber.includes(fieldtype)) {
    return h(FormControl, { type: "number" });
  } else if (typeDate.includes(fieldtype) && operator == "between") {
    return h(DateRangePicker);
  } else if (typeDate.includes(fieldtype)) {
    return h(fieldtype == "Date" ? DatePicker : DateTimePicker);
  } else if (typeRating.includes(fieldtype)) {
    return h(StarRating, {
      rating: props.condition[2] || 0,
      static: false,
      class: "truncate",
      "onUpdate:modelValue": (v) => updateValue(v),
    });
  } else {
    return h(FormControl, { type: "text" });
  }
}

function updateValue(value) {
  value = value.target ? value.target.value : value;
  if (props.condition[1] === "between") {
    // DateRangePicker emits [from, to]; the text controls still emit "from,to"
    const range = Array.isArray(value) ? value : String(value).split(",");
    props.condition[2] = [range[0], range[1]];
  } else {
    props.condition[2] = value + "";
  }
}

function getSelectOptions(options) {
  return options.split("\n");
}

function updateOperator(event) {
  let oldOperatorValue = event.target._value;
  let newOperatorValue = event.target.value;
  props.condition[1] = event.target.value;
  if (!isSameTypeOperator(oldOperatorValue, newOperatorValue)) {
    props.condition[2] = getDefaultValue(props.condition[0]);
  }
  resetConditionValue();
}

function getOperators() {
  let options = [];
  const field = props.condition[0];
  if (!field) return options;
  const fieldData = filterableFields.data?.find((f) => f.fieldname == field);
  if (!fieldData) return options;
  const { fieldtype, fieldname } = fieldData;
  if (typeString.includes(fieldtype)) {
    options.push(
      ...[
        { label: "Equals", value: "==" },
        { label: "Not Equals", value: "!=" },
        { label: "Like", value: "like" },
        { label: "Not Like", value: "not like" },
        { label: "In", value: "in" },
        { label: "Not In", value: "not in" },
        { label: "Is", value: "is" },
      ]
    );
  }
  if (fieldname === "_assign") {
    options = [
      { label: "Like", value: "like" },
      { label: "Not Like", value: "not like" },
      { label: "Is", value: "is" },
    ];
  }
  if (typeNumber.includes(fieldtype)) {
    options.push(
      ...[
        { label: "Equals", value: "==" },
        { label: "Not Equals", value: "!=" },
        { label: "Like", value: "like" },
        { label: "Not Like", value: "not like" },
        { label: "In", value: "in" },
        { label: "Not In", value: "not in" },
        { label: "Is", value: "is" },
        { label: "<", value: "<" },
        { label: ">", value: ">" },
        { label: "<=", value: "<=" },
        { label: ">=", value: ">=" },
      ]
    );
  }
  if (typeSelect.includes(fieldtype)) {
    options.push(
      ...[
        { label: "Equals", value: "==" },
        { label: "Not Equals", value: "!=" },
        { label: "In", value: "in" },
        { label: "Not In", value: "not in" },
        { label: "Is", value: "is" },
      ]
    );
  }
  if (typeLink.includes(fieldtype)) {
    options.push(
      ...[
        { label: "Equals", value: "==" },
        { label: "Not Equals", value: "!=" },
        { label: "Like", value: "like" },
        { label: "Not Like", value: "not like" },
        { label: "In", value: "in" },
        { label: "Not In", value: "not in" },
        { label: "Is", value: "is" },
      ]
    );
  }
  if (typeCheck.includes(fieldtype)) {
    options.push(...[{ label: "Equals", value: "==" }]);
  }
  if (["Duration"].includes(fieldtype)) {
    options.push(
      ...[
        { label: "Like", value: "like" },
        { label: "Not Like", value: "not like" },
        { label: "In", value: "in" },
        { label: "Not In", value: "not in" },
        { label: "Is", value: "is" },
      ]
    );
  }
  if (typeDate.includes(fieldtype)) {
    options.push(
      ...[
        { label: "Equals", value: "==" },
        { label: "Not Equals", value: "!=" },
        { label: "Is", value: "is" },
        { label: ">", value: ">" },
        { label: "<", value: "<" },
        { label: ">=", value: ">=" },
        { label: "<=", value: "<=" },
        { label: "Between", value: "between" },
      ]
    );
  }
  if (typeRating.includes(fieldtype)) {
    options.push(
      ...[
        { label: "Equals", value: "==" },
        { label: "Not Equals", value: "!=" },
        { label: "Is", value: "is" },
        { label: ">", value: ">" },
        { label: "<", value: "<" },
        { label: ">=", value: ">=" },
        { label: "<=", value: "<=" },
      ]
    );
  }
  const op = options.find((o) => o.value == props.condition[1]);
  props.condition[1] = op?.value || options[0].value;
  return options;
}

function getDefaultValue(field) {
  if (typeSelect.includes(field.fieldtype)) {
    return getSelectOptions(field.options)[0];
  }
  if (typeCheck.includes(field.fieldtype)) {
    return "Yes";
  }
  if (typeDate.includes(field.fieldtype)) {
    return null;
  }
  if (typeRating.includes(field.fieldtype)) {
    return 0;
  }
  return "";
}

function isSameTypeOperator(oldOperator, newOperator) {
  let textOperators = ["==", "!=", "in", "not in", ">", "<", ">=", "<="];
  if (
    textOperators.includes(oldOperator) &&
    textOperators.includes(newOperator)
  )
    return true;
  return false;
}
</script>
bytes
12,090
lines
458
Type
vue
Latest commit
28 October 2026