<commit_before>/**
 * @file
 *
 * @brief interactive tree view to edit configurations of instances
 *
 * @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
 */

import React from 'react'
import { ExplorerView } from 'bosket-react'

import TreeItem from '../containers/ConnectedTreeItem'
import { visibility, ARRAY_KEY_REGEX } from '../utils'

import '../css/treeview.css'

const NAMESPACES_ORDER = [ 'spec', 'dir', 'user', 'system' ]

export default class TreeView extends React.Component {
  constructor (props, ...args) {
    super(props, ...args)
    this.state = { selection: [], unfolded: [] }
  }

  componentWillReceiveProps = (nextProps) => {
    const { unfolded } = this.state
    if (unfolded.length <= 0) {
      const { instance } = nextProps
      if (instance && instance.unfolded && instance.unfolded.length > 0) {
        this.setState({ unfolded: instance.unfolded })
      }
    }
  }

  updateUnfolded = (unfolded) => {
    const { instanceId, updateInstance } = this.props
    this.setState({ unfolded })
    updateInstance(instanceId, { unfolded })
  }

  refreshPath = (path) => {
    const { getKey, instanceId } = this.props
    return getKey(instanceId, path)
  }

  refreshItem = (item, noRecursive = false) => {
    const mainPromise = this.refreshPath(item.path)
    if (!noRecursive && Array.isArray(item.children)) {
      return Promise.all([
        mainPromise,
        ...item.children.map(
          child => this.refreshPath(child.path)
        ),
      ])
    } else {
      return mainPromise
    }
  }

  handleSelect = (newSelection, item, ancestors, neighbours) => {
    this.setState({ selection: newSelection })
    // push refresh action at the back of the queue (re-renders selection first)
    setTimeout(() => this.refreshItem(item, true))
  }

  handleDrop = (target, evt, inputs) => {
    const { instanceId, moveKey } = this.props
    const { selection } = inputs

    selection.map(
      sel => moveKey(instanceId, sel.path, target.path + '/' + sel.name)
    )
    this.setState({ selection: [] })
  }

  renderItem = (item, inputs) => {
    const { kdb, instanceId, instanceVisibility } = this.props
    const data = kdb && kdb[item.path]

    const isRootPath = !item.path.includes('/')

    if (!isRootPath) { // namespaces are always shown
      const lvl = (data && data.meta && data.meta['visibility'])
        ? visibility(data.meta['visibility'])
        : visibility('user') // default visibility is user

      if (lvl < visibility(instanceVisibility)) {
        // hide this item
        return false
      }
    }

    return (
        <TreeItem
          data={data}
          item={item}
          inputs={inputs}
          instanceId={instanceId}
          pathExists={(path) => kdb && kdb[path]}
          instanceVisibility={instanceVisibility}
          refreshPath={this.refreshPath}
        />
    )
  }

  handleSearch = input => item => {
    // check name and path first
    const regex = new RegExp(`.*${input}.*`, "gi")
    if (item.name.match(regex) || item.path.match(regex)) {
      return true
    }

    // if available, check data too
    const { kdb } = this.props
    const data = kdb && kdb[item.path]
    if (data && data.value && data.value.match(regex)) {
      return true
    }

    return false
  }

  handleSort = (a, b) => {
    if (a.root) { // is a namespace -> special ordering
      const aI = NAMESPACES_ORDER.indexOf(a.name)
      const bI = NAMESPACES_ORDER.indexOf(b.name)
      return aI - bI
    }
    const matchA = a.name.match(ARRAY_KEY_REGEX)
    const matchB = b.name.match(ARRAY_KEY_REGEX)
    if (matchA && matchB) {
      const [ , , indexA ] = matchA
      const [ , , indexB ] = matchB
      return Number(indexA) - Number(indexB) // compare array key index directly (ignore prefix)
    } else if (!a.children === !b.children) {
      return a.name.localeCompare(b.name)
    }
    return a.children ? -1 : 1 // list keys with subkeys first
  }

  createOpener () {
    const tree = this
    return class Opener extends React.Component {
      onClick = (event) => {
        const { onClick, item } = this.props
        const { unfolded } = tree.state
        const newUnfolded = unfolded.filter(p => p !== item.path)
        if (newUnfolded.length === unfolded.length) {
          newUnfolded.push(item.path)
        }
        tree.updateUnfolded(newUnfolded)
        onClick(event)
        event.stopPropagation()
      }

      render () {
        const { onClick, item, children, ...rest } = this.props
        return (
          <span
            {...rest}
            tabIndex="0"
            onClick={this.onClick}
            onKeyPress={e => {
              if (e.key === 'Enter') {
                this.onClick(e)
              }
            }}
          >
            {children}
          </span>
        )
      }
    }
  }

  render () {
    const { data } = this.props
    const { selection, unfolded } = this.state
    const tree = this
    const strategies = {
      click: [ function unfoldOnSelectionByPath (item) {
        if (!this.isSelected(item) && item.children) {
          const newUnfolded = unfolded.filter(p => p !== item.path)
          if (newUnfolded.length === unfolded.length) {
            newUnfolded.push(item.path)
            tree.updateUnfolded(newUnfolded)
            const newVal = this.state.get().unfolded.filter(i => i !== item)
            if(newVal.length === this.state.get().unfolded.length)
                newVal.push(item)
            this.state.set({ unfolded: newVal })<commit_msg>web-ui: fix search not re-filtering when data updated<commit_after>/**
 * @file
 *
 * @brief interactive tree view to edit configurations of instances
 *
 * @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
 */

import React from 'react'
import { ExplorerView } from 'bosket-react'

import TreeItem from '../containers/ConnectedTreeItem'
import { visibility, ARRAY_KEY_REGEX } from '../utils'

import '../css/treeview.css'

const NAMESPACES_ORDER = [ 'spec', 'dir', 'user', 'system' ]

export default class TreeView extends React.Component {
  constructor (props, ...args) {
    super(props, ...args)
    this.state = { selection: [], unfolded: [], data: props.data }
  }

  componentWillReceiveProps = (nextProps) => {
    if (this.props.data !== nextProps.data) {
      this.setState({ data: nextProps.data })
    }

    if (this.props.kdb !== nextProps.kdb) { // kdb updated
      // force re-render of tree view
      this.setState({ data: this.state.data.slice() })
    }

    const { unfolded } = this.state
    if (unfolded.length <= 0) {
      const { instance } = nextProps
      if (instance && instance.unfolded && instance.unfolded.length > 0) {
        this.setState({ unfolded: instance.unfolded })
      }
    }
  }

  updateUnfolded = (unfolded) => {
    const { instanceId, updateInstance } = this.props
    this.setState({ unfolded })
    updateInstance(instanceId, { unfolded })
  }

  refreshPath = (path) => {
    const { getKey, instanceId } = this.props
    return getKey(instanceId, path)
  }

  refreshItem = (item, noRecursive = false) => {
    const mainPromise = this.refreshPath(item.path)
    if (!noRecursive && Array.isArray(item.children)) {
      return Promise.all([
        mainPromise,
        ...item.children.map(
          child => this.refreshPath(child.path)
        ),
      ])
    } else {
      return mainPromise
    }
  }

  handleSelect = (newSelection, item, ancestors, neighbours) => {
    this.setState({ selection: newSelection })
    // push refresh action at the back of the queue (re-renders selection first)
    setTimeout(() => this.refreshItem(item, true))
  }

  handleDrop = (target, evt, inputs) => {
    const { instanceId, moveKey } = this.props
    const { selection } = inputs

    selection.map(
      sel => moveKey(instanceId, sel.path, target.path + '/' + sel.name)
    )
    this.setState({ selection: [] })
  }

  renderItem = (item, inputs) => {
    const { kdb, instanceId, instanceVisibility } = this.props
    const data = kdb && kdb[item.path]

    const isRootPath = !item.path.includes('/')

    if (!isRootPath) { // namespaces are always shown
      const lvl = (data && data.meta && data.meta['visibility'])
        ? visibility(data.meta['visibility'])
        : visibility('user') // default visibility is user

      if (lvl < visibility(instanceVisibility)) {
        // hide this item
        return false
      }
    }

    return (
        <TreeItem
          data={data}
          item={item}
          inputs={inputs}
          instanceId={instanceId}
          pathExists={(path) => kdb && kdb[path]}
          instanceVisibility={instanceVisibility}
          refreshPath={this.refreshPath}
        />
    )
  }

  handleSearch = input => item => {
    // check name and path first
    const regex = new RegExp(`.*${input}.*`, "gi")
    if (item.name.match(regex) || item.path.match(regex)) {
      return true
    }

    // if available, check data too
    const { kdb } = this.props
    const data = kdb && kdb[item.path]
    if (data && data.value && data.value.match(regex)) {
      return true
    }

    return false
  }

  handleSort = (a, b) => {
    if (a.root) { // is a namespace -> special ordering
      const aI = NAMESPACES_ORDER.indexOf(a.name)
      const bI = NAMESPACES_ORDER.indexOf(b.name)
      return aI - bI
    }
    const matchA = a.name.match(ARRAY_KEY_REGEX)
    const matchB = b.name.match(ARRAY_KEY_REGEX)
    if (matchA && matchB) {
      const [ , , indexA ] = matchA
      const [ , , indexB ] = matchB
      return Number(indexA) - Number(indexB) // compare array key index directly (ignore prefix)
    } else if (!a.children === !b.children) {
      return a.name.localeCompare(b.name)
    }
    return a.children ? -1 : 1 // list keys with subkeys first
  }

  createOpener () {
    const tree = this
    return class Opener extends React.Component {
      onClick = (event) => {
        const { onClick, item } = this.props
        const { unfolded } = tree.state
        const newUnfolded = unfolded.filter(p => p !== item.path)
        if (newUnfolded.length === unfolded.length) {
          newUnfolded.push(item.path)
        }
        tree.updateUnfolded(newUnfolded)
        onClick(event)
        event.stopPropagation()
      }

      render () {
        const { onClick, item, children, ...rest } = this.props
        return (
          <span
            {...rest}
            tabIndex="0"
            onClick={this.onClick}
            onKeyPress={e => {
              if (e.key === 'Enter') {
                this.onClick(e)
              }
            }}
          >
            {children}
          </span>
        )
      }
    }
  }

  render () {
    const { data, selection, unfolded } = this.state
    const tree = this
    const strategies = {
      click: [ function unfoldOnSelectionByPath (item) {
        if (!this.isSelected(item) && item.children) {
          const newUnfolded = unfolded.filter(p => p !== item.path)
          if (newUnfolded.length === unfolded.length) {
            newUnfolded.push(item.path)
            tree.updateUnfolded(newUnfolded)
            const newVal = this.state.get().unfolded.filter(i => i !== item)
            if(newVal.length === this.state.get().unfolded.length)
                newVal.push(item)
            this.state.set({ unfolded: newVal })
{'fraction_non_alphanumeric': 0.13493174658732937, 'fraction_numerical': 0.0008750437521876093, 'mean_word_length': 2.2076901487510523, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 51, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5929336', 'n_tokens_mistral': 3533, 'n_tokens_neox': 3444, 'n_words': 814}