resources/assets/js/components/playlist/smart-playlist/SmartPlaylistRule.vue

Summary

Maintainability
Test Coverage
<template>
  <FormRow>
    <div class="w-full flex gap-2 relative">
      <SelectBox v-model="selectedModel" name="model[]">
        <option v-for="m in models" :key="m.name" :value="m">{{ m.label }}</option>
      </SelectBox>

      <SelectBox v-model="selectedOperator" name="operator[]">
        <option v-for="option in availableOperators" :key="option.operator" :value="option">{{ option.label }}</option>
      </SelectBox>

      <span class="inline-flex flex-1 items-center gap-3">
        <RuleInput
          v-for="input in availableInputs"
          :key="input.id"
          v-model="input.value"
          :type="(selectedOperator?.type || selectedModel?.type)!"
          :value="input.value"
          class="!flex-1"
          @update:model-value="onInput"
        />

        <span v-if="valueSuffix" class="suffix">{{ valueSuffix }}</span>
      </span>

      <Btn
        class="absolute -right-[14px] aspect-square top-1 scale-[60%] hover:scale-75 active:scale-[60%]"
        danger
        rounded
        small
        title="Remove this rule"
        @click.prevent="removeRule"
      >
        <Icon :icon="faMinus" />
      </Btn>
    </div>
  </FormRow>
</template>

<script lang="ts" setup>
import { faMinus } from '@fortawesome/free-solid-svg-icons'
import { computed, defineAsyncComponent, ref, toRefs, watch } from 'vue'
import models from '@/config/smart-playlist/models'
import inputTypes from '@/config/smart-playlist/inputTypes'

import FormRow from '@/components/ui/form/FormRow.vue'
import SelectBox from '@/components/ui/form/SelectBox.vue'
import Btn from '@/components/ui/form/Btn.vue'

const RuleInput = defineAsyncComponent(() => import('@/components/playlist/smart-playlist/SmartPlaylistRuleInput.vue'))

const props = defineProps<{ rule: SmartPlaylistRule }>()
const { rule } = toRefs(props)

const mutatedRule = Object.assign({}, rule.value) as SmartPlaylistRule

const selectedModel = ref<SmartPlaylistModel>()
const selectedOperator = ref<SmartPlaylistOperator>()

const model = models.find(({ name }) => name === mutatedRule.model.name)

if (!model) {
  throw new Error(`Invalid smart playlist model: ${mutatedRule.model.name}`)
}

mutatedRule.model = selectedModel.value = model

const availableOperators = computed<SmartPlaylistOperator[]>(() => {
  return selectedModel.value ? inputTypes[selectedModel.value.type] : []
})

const operator = availableOperators.value.find(({ operator }) => operator === mutatedRule.operator)

if (!operator) {
  throw new Error(`Invalid smart playlist operator: ${mutatedRule.operator}`)
}

selectedOperator.value = operator

const isOriginalOperatorSelected = computed(() => {
  return selectedModel.value?.name === mutatedRule.model.name &&
    selectedOperator.value?.operator === mutatedRule.operator
})

const availableInputs = computed<{ id: string, value: any }[]>(() => {
  if (!selectedOperator.value) {
    return []
  }

  const inputs: Array<{ id: string, value: string }> = []

  for (let i = 0, inputCount = selectedOperator.value.inputs || 1; i < inputCount; ++i) {
    inputs.push({
      id: `${mutatedRule.model.name}_${selectedOperator.value.operator}_${i}`,
      value: isOriginalOperatorSelected.value ? mutatedRule.value[i] : ''
    })
  }

  return inputs
})

watch(availableOperators, () => {
  if (selectedModel.value?.name === mutatedRule.model.name) {
    selectedOperator.value = availableOperators.value.find(({ operator }) => operator === mutatedRule.operator)!
  } else {
    selectedOperator.value = availableOperators.value[0]
  }
})

const valueSuffix = computed(() => selectedOperator.value?.unit || selectedModel.value?.unit)

const emit = defineEmits<{
  (e: 'input', rule: SmartPlaylistRule): void,
  (e: 'remove'): void
}>()

const onInput = () => {
  emit('input', {
    id: mutatedRule.id,
    model: selectedModel.value!,
    operator: selectedOperator.value?.operator!,
    value: availableInputs.value.map(input => input.value)
  })
}

const removeRule = () => emit('remove')
</script>