SpeciesFileGroup/taxonworks

View on GitHub
app/javascript/vue/components/Filter/Table/TableResults.vue

Summary

Maintainability
Test Coverage
<template>
  <HandyScroll>
    <table
      class="full_width"
      v-resize-column
      ref="element"
    >
      <thead>
        <tr
          v-if="
            headerGroups.length ||
            dataAttributeHeaders.length ||
            layout?.properties
          "
        >
          <td colspan="2" />
          <template
            v-for="header in headerGroups"
            :key="header"
          >
            <component
              :is="header.title ? 'th' : 'td'"
              :colspan="header.colspan"
              :scope="header.scope"
            >
              {{ header.title }}
            </component>
          </template>

          <template
            v-for="(properties, key) in layout?.properties"
            :key="key"
          >
            <th
              v-if="properties.length"
              :colspan="properties.length"
              scope="colgroup"
              class="cell-left-border"
            >
              {{ humanize(key) }}
            </th>
          </template>

          <td
            v-if="!headerGroups.length && !isLayoutConfig"
            :colspan="Object.keys(attributes).length"
          />

          <th
            v-if="
              dataAttributeHeaders.length &&
              (!isLayoutConfig || layout.includes.data_attributes)
            "
            :colspan="dataAttributeHeaders.length"
            scope="colgroup"
            class="cell-left-border"
          >
            Data attributes
          </th>
        </tr>
        <tr>
          <th class="w-2">
            <input
              v-model="selectIds"
              :disabled="!list.length"
              type="checkbox"
            />
          </th>
          <th class="w-2" />
          <th
            v-for="(title, attr) in attributes"
            :key="attr"
            @click="sortTable(attr)"
          >
            <div class="horizontal-left-content">
              <span>{{ title }}</span>
              <VBtn
                v-if="filterValues[attr]"
                class="margin-small-left"
                color="primary"
                small
                @click.stop="
                  () => {
                    delete filterValues[attr]
                  }
                "
              >
                X
              </VBtn>
            </div>
          </th>

          <template
            v-for="(propertiesList, key) in layout?.properties"
            :key="key"
          >
            <th
              v-for="(property, pIndex) in propertiesList"
              :key="property"
              :class="{ 'cell-left-border': pIndex === 0 }"
              @click="sortTable(`${key}.${property}`)"
            >
              <div class="horizontal-left-content">
                <span>{{ property }}</span>
                <VBtn
                  v-if="filterValues[`${key}.${property}`]"
                  class="margin-small-left"
                  color="primary"
                  small
                  @click.stop="
                    () => {
                      delete filterValues[`${key}.${property}`]
                    }
                  "
                >
                  X
                </VBtn>
              </div>
            </th>
          </template>

          <template
            v-if="
              dataAttributeHeaders.length &&
              (!isLayoutConfig || layout.includes.data_attributes)
            "
          >
            <th
              v-for="(header, index) in dataAttributeHeaders"
              :key="header"
              scope="colgroup"
              :class="{ 'cell-left-border': index === 0 }"
              @click="sortTable(`data_attributes.${header}`)"
            >
              {{ header }}
            </th>
          </template>
        </tr>
      </thead>
      <tbody @mouseout="($event) => emit('mouseout:body', $event)">
        <tr
          v-for="(item, index) in list"
          :key="item.id"
          class="contextMenuCells"
          :class="{
            even: index % 2,
            'cell-selected-border': item.id === lastRadialOpenedRow
          }"
          v-show="rowHasCurrentValues(item)"
          @mouseover="() => emit('mouseover:row', { index, item })"
        >
          <td>
            <input
              v-model="ids"
              :value="item.id"
              type="checkbox"
            />
          </td>
          <td>
            <div class="horizontal-right-content gap-small">
              <slot
                name="buttons-left"
                :item="item"
              />
              <RadialAnnotator
                :global-id="item.global_id"
                @click="() => (lastRadialOpenedRow = item.id)"
              />
              <RadialObject
                v-if="radialObject"
                :global-id="item.global_id"
                @click="() => (lastRadialOpenedRow = item.id)"
              />
              <RadialNavigation
                :global-id="item.global_id"
                :redirect="false"
                @delete="emit('remove', { item, index })"
                @click="() => (lastRadialOpenedRow = item.id)"
              />
            </div>
          </td>
          <template v-if="attributes">
            <slot
              v-for="(_, attr) in attributes"
              :key="attr"
              :name="attr"
              :value="item[attr]"
            >
              <td
                v-html="item[attr]"
                @dblclick="
                  () => {
                    scrollToTop()
                    filterValues[attr] = item[attr]
                  }
                "
              />
            </slot>
          </template>

          <template
            v-for="(properties, key) in layout?.properties"
            :key="key"
          >
            <td
              v-for="(property, pIndex) in properties"
              :key="property"
              v-html="renderItem(item, key, property)"
              :class="{ 'cell-left-border': pIndex === 0 }"
              @dblclick="
                () => {
                  scrollToTop()
                  filterValues[`${key}.${property}`] = Array.isArray(item[key])
                    ? item[key].map((obj) => obj[property])
                    : item[key][property]
                }
              "
            />
          </template>

          <template
            v-if="
              dataAttributeHeaders.length &&
              (!isLayoutConfig || layout.includes.data_attributes)
            "
          >
            <td
              v-for="(predicateName, dIndex) in dataAttributeHeaders"
              :key="predicateName"
              :class="{ 'cell-left-border': dIndex === 0 }"
              v-text="item.data_attributes[predicateName]"
            />
          </template>
        </tr>
      </tbody>
    </table>
  </HandyScroll>
</template>

<script setup>
import { computed, ref, watch } from 'vue'
import { sortArray } from '@/helpers/arrays.js'
import { vResizeColumn } from '@/directives/resizeColumn.js'
import { humanize } from '@/helpers/strings'
import VBtn from '@/components/ui/VBtn/index.vue'
import HandyScroll from 'vue-handy-scroll'
import RadialNavigation from '@/components/radials/navigation/radial.vue'
import RadialAnnotator from '@/components/radials/annotator/annotator.vue'
import RadialObject from '@/components/radials/object/radial.vue'

const props = defineProps({
  list: {
    type: Array,
    default: () => []
  },

  modelValue: {
    type: Array,
    default: () => []
  },

  attributes: {
    type: Object,
    default: () => {}
  },

  headerGroups: {
    type: Array,
    default: () => []
  },

  layout: {
    type: Object,
    default: () => {}
  },

  radialObject: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits([
  'onSort',
  'update:modelValue',
  'mouseover:row',
  'mouseout:body'
])

const element = ref(null)
const ascending = ref(false)
const lastRadialOpenedRow = ref(null)
const isLayoutConfig = computed(() => !!Object.keys(props.layout || {}).length)

const selectIds = computed({
  get: () =>
    props.list.length === props.modelValue.length && props.list.length > 0,
  set: (value) =>
    emit('update:modelValue', value ? props.list.map((item) => item.id) : [])
})

const ids = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})

const dataAttributeHeaders = computed(() => {
  const predicateNames = []

  props.list.forEach((item) => {
    Object.keys(item.data_attributes || {}).forEach((name) => {
      if (!predicateNames.includes(name)) {
        predicateNames.push(name)
      }
    })
  })

  return predicateNames.sort()
})

const filterValues = ref({})

function rowHasCurrentValues(item) {
  return Object.entries(filterValues.value).every(([properties, value]) => {
    const itemValue = getValue(item, properties)

    return Array.isArray(itemValue)
      ? itemValue.some((i) => value.includes(i))
      : itemValue === value
  })
}

function renderItem(item, listType, property) {
  const value = item[listType]

  return Array.isArray(value)
    ? value.map((obj) => obj[property]).join('; ')
    : value && value[property]
}

function getValue(item, property) {
  const properties = property.split('.')

  return properties.reduce((acc, curr) => {
    return Array.isArray(acc) ? acc.map((item) => item[curr]) : acc[curr]
  }, item)
}

watch(
  () => props.list,
  () => {
    HandyScroll.EventBus.emit('update', { sourceElement: element.value })
  },
  { immediate: true }
)

watch(
  () => props.layout,
  () => {
    HandyScroll.EventBus.emit('update', { sourceElement: element.value })
  },
  { deep: true }
)

function sortTable(sortProperty) {
  emit('onSort', sortArray(props.list, sortProperty, ascending.value))
  ascending.value = !ascending.value
}

function scrollToTop() {
  window.scrollTo(0, 0)
}
</script>

<style scoped>
.cell-left-border {
  border-left: 3px #eaeaea solid;
}

.cell-selected-border {
  outline: 2px solid var(--color-primary) !important;
  outline-offset: -2px;
}
</style>