mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
refactor: simplify tree class + add typing
This commit is contained in:
parent
cdaf8a7bcb
commit
f9ddae3237
3 changed files with 103 additions and 110 deletions
|
@ -1,9 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Obj } from '@/types/commons'
|
import type { TreeChildNode, AnyTreeNode } from '@/helpers/data/tree'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
tree: Obj
|
tree: AnyTreeNode
|
||||||
flush?: boolean
|
flush?: boolean
|
||||||
last?: boolean
|
last?: boolean
|
||||||
toggleText?: string
|
toggleText?: string
|
||||||
|
@ -16,10 +16,14 @@ const props = withDefaults(
|
||||||
)
|
)
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default: (props: any) => any
|
default: (props: {
|
||||||
|
[K in keyof TreeChildNode as TreeChildNode[K] extends Function
|
||||||
|
? never
|
||||||
|
: K]: TreeChildNode[K]
|
||||||
|
}) => any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function getClasses(node: Obj, i: number) {
|
function getClasses(node: AnyTreeNode, i: number) {
|
||||||
const children = node.height > 0
|
const children = node.height > 0
|
||||||
const opened = children && node.data?.opened
|
const opened = children && node.data?.opened
|
||||||
const last =
|
const last =
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
|
||||||
|
type TreeNodeData = {
|
||||||
|
name: string
|
||||||
|
parent: string | null
|
||||||
|
to: RouteLocationRaw
|
||||||
|
opened: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Node that can have a parent and children.
|
* A Node that can have a parent and children.
|
||||||
*/
|
*/
|
||||||
export class Node {
|
class TreeNode {
|
||||||
constructor(data) {
|
data: TreeNodeData | null = null
|
||||||
this.data = data
|
depth: number = 0
|
||||||
this.depth = 0
|
height: number = 0
|
||||||
this.height = 0
|
parent: AnyTreeNode | null = null
|
||||||
this.parent = null
|
id: string = 'root'
|
||||||
// this.id = null
|
children: TreeChildNode[] = []
|
||||||
// this.children = null
|
_remove: boolean = false
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invokes the specified `callback` for this node and each descendant in pre-order
|
* Invokes the specified `callback` for this node and each descendant in pre-order
|
||||||
|
@ -18,17 +26,17 @@ export class Node {
|
||||||
* The specified function is passed the current descendant, the zero-based traversal
|
* The specified function is passed the current descendant, the zero-based traversal
|
||||||
* index, and this node.
|
* index, and this node.
|
||||||
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/eachBefore.js.
|
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/eachBefore.js.
|
||||||
*
|
|
||||||
* @param {function} callback
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
*/
|
||||||
eachBefore(callback) {
|
eachBefore(
|
||||||
const nodes = []
|
callback: (node: AnyTreeNode, index: number, root: TreeRootNode) => void,
|
||||||
|
) {
|
||||||
|
const root = this as TreeRootNode
|
||||||
|
const nodes: AnyTreeNode[] = []
|
||||||
let index = -1
|
let index = -1
|
||||||
let node = this
|
let node: AnyTreeNode | undefined = root
|
||||||
|
|
||||||
while (node) {
|
while (node) {
|
||||||
callback(node, ++index, this)
|
callback(node, ++index, root)
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
nodes.push(...node.children)
|
nodes.push(...node.children)
|
||||||
}
|
}
|
||||||
|
@ -45,14 +53,14 @@ export class Node {
|
||||||
* The specified function is passed the current descendant, the zero-based traversal
|
* The specified function is passed the current descendant, the zero-based traversal
|
||||||
* index, and this node.
|
* index, and this node.
|
||||||
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/eachAfter.js.
|
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/eachAfter.js.
|
||||||
*
|
|
||||||
* @param {function} callback
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
*/
|
||||||
eachAfter(callback) {
|
eachAfter(
|
||||||
const nodes = []
|
callback: (node: AnyTreeNode, index: number, root: TreeRootNode) => void,
|
||||||
const next = []
|
) {
|
||||||
let node = this
|
const root = this as TreeRootNode
|
||||||
|
const nodes: AnyTreeNode[] = []
|
||||||
|
const next: AnyTreeNode[] = []
|
||||||
|
let node: AnyTreeNode | undefined = root
|
||||||
|
|
||||||
while (node) {
|
while (node) {
|
||||||
next.push(node)
|
next.push(node)
|
||||||
|
@ -64,132 +72,113 @@ export class Node {
|
||||||
|
|
||||||
let index = 0
|
let index = 0
|
||||||
for (let i = next.length - 1; i >= 0; i--) {
|
for (let i = next.length - 1; i >= 0; i--) {
|
||||||
callback(next[i], index++, this)
|
callback(next[i], index++, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a deep copied and filtered tree of itself.
|
* Returns a deep copied and filtered tree of itself.
|
||||||
* Specified filter function is passed each nodes in post-order traversal and must
|
* Specified filter function is passed each nodes in post-order traversal and must
|
||||||
* return `true` or `false` like a regular filter function.
|
* return `true` or `false` like a regular filter function.
|
||||||
*
|
|
||||||
* @param {Function} callback - filter callback function to invoke on each nodes
|
|
||||||
* @param {Object} args
|
|
||||||
* @param {String} [args.idKey='name'] - the key name where we can find the node identity.
|
|
||||||
* @param {String} [args.parentIdKey='name'] - the key name where we can find the parent identity.
|
|
||||||
* @return {Node}
|
|
||||||
*/
|
*/
|
||||||
filter(callback) {
|
filter(
|
||||||
|
callback: (node: AnyTreeNode, index: number, root: TreeRootNode) => boolean,
|
||||||
|
) {
|
||||||
|
const root = this as TreeRootNode
|
||||||
// Duplicates this tree and iter on nodes from leaves to root (post-order traversal)
|
// Duplicates this tree and iter on nodes from leaves to root (post-order traversal)
|
||||||
return hierarchy(this).eachAfter((node, i) => {
|
return hierarchy(root).eachAfter((node, i) => {
|
||||||
// Since we create a new hierarchy from another, nodes's `data` contains the
|
// Since we create a new hierarchy from another, nodes's `data` contains the
|
||||||
// whole dupplicated node. Overwrite node's `data` by node's original `data`.
|
// whole dupplicated node. Overwrite node's `data` by node's original `data`.
|
||||||
node.data = node.data.data
|
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
// Removed flagged children
|
// Removed flagged children
|
||||||
node.children = node.children.filter((child) => !child.remove)
|
node.children = node.children.filter((child) => !child._remove)
|
||||||
if (!node.children.length) delete node.children
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform filter callback on non-root nodes
|
// Perform filter callback on non-root nodes
|
||||||
const match = node.data ? callback(node, i, this) : true
|
const match =
|
||||||
|
node instanceof TreeChildNode ? callback(node, i, root) : true
|
||||||
// Flag node if there's no match in node nor in its children
|
// Flag node if there's no match in node nor in its children
|
||||||
if (!match && !node.children) {
|
if (!match && !node.children.length) {
|
||||||
node.remove = true
|
node._remove = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TreeRootNode extends TreeNode {
|
||||||
|
data: null = null
|
||||||
|
parent: null = null
|
||||||
|
id: 'root' = 'root'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TreeChildNode extends TreeNode {
|
||||||
|
data: TreeNodeData
|
||||||
|
parent: AnyTreeNode
|
||||||
|
id: string
|
||||||
|
|
||||||
|
constructor(data: TreeNodeData, parent: AnyTreeNode) {
|
||||||
|
super()
|
||||||
|
this.data = data
|
||||||
|
this.parent = parent
|
||||||
|
this.id = data.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyTreeNode = TreeRootNode | TreeChildNode
|
||||||
/**
|
/**
|
||||||
* Generates a new hierarchy from the specified tabular `dataset`.
|
* Generates a new hierarchy from the specified tabular `dataset`.
|
||||||
* The specified `dataset` must be an array of objects that contains at least a
|
* The specified `dataset` must be an array of objects that contains at least a
|
||||||
* `name` property and an optional `parent` property referencing its parent `name`.
|
* `name` property and an optional `parent` property referencing its parent `name`.
|
||||||
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/stratify.js#L16.
|
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/stratify.js#L16.
|
||||||
*
|
|
||||||
* @param {Array} dataset
|
|
||||||
* @param {Object} args
|
|
||||||
* @param {String} [args.idKey='name'] - the key name where we can find the node identity.
|
|
||||||
* @param {String} [args.parentIdKey='name'] - the key name where we can find the parent identity.
|
|
||||||
* @return {Node}
|
|
||||||
*/
|
*/
|
||||||
export function stratify(
|
export function stratify(dataset: TreeNodeData[]) {
|
||||||
dataset,
|
const root = new TreeRootNode()
|
||||||
{ idKey = 'name', parentIdKey = 'parent' } = {},
|
const nodesMap: Map<TreeChildNode['id'], TreeChildNode> = new Map()
|
||||||
) {
|
|
||||||
const root = new Node(null, true)
|
|
||||||
root.children = []
|
|
||||||
const nodesMap = new Map()
|
|
||||||
|
|
||||||
// Creates all nodes that will be arranged in a hierarchy
|
// Creates all nodes that will be arranged in a hierarchy
|
||||||
const nodes = dataset.map((d) => {
|
dataset.map((d) => {
|
||||||
const node = new Node(d)
|
const parent = d.parent ? nodesMap.get(d.parent) || root : root
|
||||||
node.id = d[idKey]
|
const node = new TreeChildNode(d, parent)
|
||||||
|
parent.children.push(node)
|
||||||
nodesMap.set(node.id, node)
|
nodesMap.set(node.id, node)
|
||||||
if (d[parentIdKey]) {
|
|
||||||
node.parent = d[parentIdKey]
|
|
||||||
}
|
|
||||||
return node
|
return node
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build a hierarchy from nodes
|
|
||||||
nodes.forEach((node, i) => {
|
|
||||||
const parentId = node.parent
|
|
||||||
if (parentId) {
|
|
||||||
const parent = nodesMap.get(parentId)
|
|
||||||
if (!parent) throw new Error('Missing parent node: ' + parentId)
|
|
||||||
if (parent.children) parent.children.push(node)
|
|
||||||
else parent.children = [node]
|
|
||||||
node.parent = parent
|
|
||||||
} else {
|
|
||||||
node.parent = root
|
|
||||||
root.children.push(node)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
root.eachBefore((node) => {
|
root.eachBefore((node) => {
|
||||||
// Compute node depth
|
// Compute node depth
|
||||||
if (node.parent) {
|
if (node.parent) {
|
||||||
node.depth = node.parent.depth + 1
|
node.depth = node.parent.depth + 1
|
||||||
// Remove parent key if parent is root (node with no data)
|
// Remove parent key if parent is root (node with no data)
|
||||||
if (!node.parent.data) delete node.parent
|
|
||||||
}
|
}
|
||||||
computeNodeHeight(node)
|
computeNodeHeight(node)
|
||||||
})
|
})
|
||||||
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a root node from the specified hierarchical `data`.
|
* Constructs a root node from the specified hierarchical `data`.
|
||||||
* The specified `data` must be an object representing the root node and its children.
|
* The specified `data` must be an object representing the root node and its children.
|
||||||
* If given a `Node` object this will return a deep copy of it.
|
* If given a `TreeRootNode` object this will return a deep copy of it.
|
||||||
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/index.js#L15.
|
* Code taken and adapted from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/index.js#L15.
|
||||||
*
|
*
|
||||||
* @param {Node|Object} data - object representing a root node (a simple { id, children } object or a `Node`)
|
* @param data - object representing a root node (a simple { id, children } object or a `TreeNode`)
|
||||||
* @return {Node}
|
|
||||||
*/
|
*/
|
||||||
export function hierarchy(data) {
|
export function hierarchy(data: TreeRootNode) {
|
||||||
const root = new Node(data)
|
function deepCopyNodes(nodes: TreeChildNode[], parent: AnyTreeNode) {
|
||||||
const nodes = []
|
return nodes.map((node) => {
|
||||||
let node = root
|
const copy = new TreeChildNode(node.data, parent)
|
||||||
|
copy.depth = parent.depth + 1
|
||||||
while (node) {
|
copy.children = deepCopyNodes(node.children, copy)
|
||||||
if (node.data.children) {
|
return copy
|
||||||
node.children = node.data.children.map((child_) => {
|
|
||||||
const child = new Node(child_)
|
|
||||||
child.id = child_.id
|
|
||||||
child.parent = node === root ? null : node
|
|
||||||
child.depth = node.depth + 1
|
|
||||||
nodes.push(child)
|
|
||||||
return child
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
node = nodes.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const root = new TreeRootNode()
|
||||||
|
root.children = deepCopyNodes(data.children, root)
|
||||||
root.eachBefore(computeNodeHeight)
|
root.eachBefore(computeNodeHeight)
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
@ -197,13 +186,12 @@ export function hierarchy(data) {
|
||||||
/**
|
/**
|
||||||
* Compute the node height by iterating on parents
|
* Compute the node height by iterating on parents
|
||||||
* Code taken from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/index.js#L62.
|
* Code taken from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/index.js#L62.
|
||||||
*
|
|
||||||
* @param {Node} node
|
|
||||||
*/
|
*/
|
||||||
function computeNodeHeight(node) {
|
function computeNodeHeight(node: TreeNode) {
|
||||||
|
let node_: TreeNode | null = node
|
||||||
let height = 0
|
let height = 0
|
||||||
do {
|
do {
|
||||||
node.height = height
|
node_.height = height
|
||||||
node = node.parent
|
node_ = node_.parent
|
||||||
} while (node && node.height < ++height)
|
} while (node_ && node_.height < ++height)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useStoreGetters } from '@/store/utils'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import RecursiveListGroup from '@/components/RecursiveListGroup.vue'
|
import RecursiveListGroup from '@/components/RecursiveListGroup.vue'
|
||||||
|
import type { TreeRootNode } from '@/helpers/data/tree'
|
||||||
|
|
||||||
const { domains, mainDomain, domainsTree } = useStoreGetters()
|
const { domains, mainDomain, domainsTree } = useStoreGetters()
|
||||||
|
|
||||||
|
@ -11,12 +12,13 @@ const search = ref('')
|
||||||
|
|
||||||
const tree = computed(() => {
|
const tree = computed(() => {
|
||||||
// FIXME rm ts type when moved to pinia or else
|
// FIXME rm ts type when moved to pinia or else
|
||||||
if (!domainsTree.value) return
|
const tree = domainsTree.value as TreeRootNode | undefined
|
||||||
|
if (!tree) return
|
||||||
const search_ = search.value.toLowerCase()
|
const search_ = search.value.toLowerCase()
|
||||||
if (search_) {
|
if (search_) {
|
||||||
return domainsTree.value.filter((node) => node.id.includes(search_))
|
return tree.filter((node) => node.id.includes(search_))
|
||||||
}
|
}
|
||||||
return domainsTree.value
|
return tree
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasFilteredItems = computed(() => {
|
const hasFilteredItems = computed(() => {
|
||||||
|
@ -42,6 +44,7 @@ const hasFilteredItems = computed(() => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<RecursiveListGroup
|
<RecursiveListGroup
|
||||||
|
v-if="tree"
|
||||||
:tree="tree"
|
:tree="tree"
|
||||||
:toggle-text="$t('domain.toggle_subdomains')"
|
:toggle-text="$t('domain.toggle_subdomains')"
|
||||||
class="mb-5"
|
class="mb-5"
|
||||||
|
@ -52,9 +55,7 @@ const hasFilteredItems = computed(() => {
|
||||||
<h5 class="me-3">
|
<h5 class="me-3">
|
||||||
<BLink :to="data.to" class="text-body text-decoration-none">
|
<BLink :to="data.to" class="text-body text-decoration-none">
|
||||||
<span class="fw-bold">
|
<span class="fw-bold">
|
||||||
{{
|
{{ data.name.replace(parent.data?.name ?? '', '') }}
|
||||||
data.name.replace(parent?.data ? parent.data.name : null, '')
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
<span v-if="parent?.data" class="text-secondary">
|
<span v-if="parent?.data" class="text-secondary">
|
||||||
{{ parent.data.name }}
|
{{ parent.data.name }}
|
||||||
|
|
Loading…
Reference in a new issue