This is an automated email from the ASF dual-hosted git repository.

likyh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 10f3308b feat(config-ui): added component miller-columns (#3683)
10f3308b is described below

commit 10f3308bdb208e95ea4c9467e24f4757207f13ff
Author: 青湛 <[email protected]>
AuthorDate: Wed Nov 9 16:06:08 2022 +0800

    feat(config-ui): added component miller-columns (#3683)
---
 config-ui/src/App.js                               |  12 ++
 .../components/checkbox/checkbox.tsx               |  45 +++++
 .../miller-columns/components/checkbox/index.ts    |  20 ++
 .../miller-columns/components/checkbox/styled.ts   | 104 +++++++++++
 .../miller-columns/components/checkbox/types.ts    |  23 +++
 .../components/miller-columns/components/index.ts  |  20 ++
 .../miller-columns/components/item/index.ts        |  20 ++
 .../miller-columns/components/item/item-all.tsx    |  45 +++++
 .../miller-columns/components/item/item.tsx        |  67 +++++++
 .../miller-columns/components/item/styled.ts       |  40 ++++
 .../src/components/miller-columns/hooks/index.ts   |  22 +++
 .../components/miller-columns/hooks/use-columns.ts |  65 +++++++
 .../miller-columns/hooks/use-item-map.ts           |  79 ++++++++
 .../miller-columns/hooks/use-miller-columns.ts     | 205 +++++++++++++++++++++
 .../components/miller-columns/hooks/use-test.ts    | 116 ++++++++++++
 config-ui/src/components/miller-columns/index.ts   |  20 ++
 .../components/miller-columns/miller-columns.tsx   |  84 +++++++++
 config-ui/src/components/miller-columns/styled.ts  |  46 +++++
 config-ui/src/components/miller-columns/types.ts   |  53 ++++++
 19 files changed, 1086 insertions(+)

diff --git a/config-ui/src/App.js b/config-ui/src/App.js
index 1fdf9447..c4729c45 100644
--- a/config-ui/src/App.js
+++ b/config-ui/src/App.js
@@ -42,6 +42,7 @@ import BlueprintDetail from 
'@/pages/blueprints/blueprint-detail'
 import BlueprintSettings from '@/pages/blueprints/blueprint-settings'
 import { IncomingWebhook as IncomingWebhookConnection } from 
'@/pages/connections/incoming-webhook'
 import MigrationAlertDialog from '@/components/MigrationAlertDialog'
+import { MillerColumns, useTest } from '@/components/miller-columns'
 
 function App(props) {
   const {
@@ -55,6 +56,8 @@ function App(props) {
     handleMigrationDialogClose
   } = useDatabaseMigrations()
 
+  const { items, ids, setIds } = useTest()
+
   return (
     <Router>
       <Route exact path='/'>
@@ -105,6 +108,15 @@ function App(props) {
       <Route exact path='/connections/incoming-webhook'>
         <IncomingWebhookConnection />
       </Route>
+      <Route exact path='/miller-columns'>
+        <MillerColumns
+          height={624}
+          firstColumnTitle='Organizations/Owners'
+          items={items}
+          selectedItemIds={ids}
+          onSelectedItemIds={(ids) => setIds(ids)}
+        />
+      </Route>
       <Route exact path='/offline'>
         <Offline />
       </Route>
diff --git 
a/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx 
b/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx
new file mode 100644
index 00000000..a3394e58
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import React from 'react'
+import classNames from 'classnames'
+
+import { CheckStatus } from './types'
+import * as S from './styled'
+
+interface Props {
+  status?: CheckStatus
+  children?: React.ReactNode
+  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+}
+
+export const Checkbox = ({ children, status, onClick }: Props) => {
+  const checkboxCls = classNames('checkbox', {
+    'checkbox-checked': status === CheckStatus.checked,
+    'checkbox-indeterminate': status === CheckStatus.indeterminate
+  })
+
+  return (
+    <S.Wrapper>
+      <span className={checkboxCls} onClick={onClick}>
+        <span className='checkbox-inner'></span>
+      </span>
+      {children && <span className='text'>{children}</span>}
+    </S.Wrapper>
+  )
+}
diff --git 
a/config-ui/src/components/miller-columns/components/checkbox/index.ts 
b/config-ui/src/components/miller-columns/components/checkbox/index.ts
new file mode 100644
index 00000000..4941e126
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './checkbox'
+export * from './types'
diff --git 
a/config-ui/src/components/miller-columns/components/checkbox/styled.ts 
b/config-ui/src/components/miller-columns/components/checkbox/styled.ts
new file mode 100644
index 00000000..1eda3718
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/styled.ts
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import styled from '@emotion/styled'
+
+export const Wrapper = styled.label`
+  display: inline-flex;
+  align-items: center;
+  cursor: pointer;
+
+  .checkbox {
+    position: relative;
+    margin-right: 8px;
+
+    &.checkbox-checked {
+      .checkbox-inner {
+        background-color: #7497f7;
+        border-color: #7497f7;
+
+        &::after {
+          content: ' ';
+          transform: rotate(45deg) scale(1) translate(-50%, -50%);
+          opacity: 1;
+          transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
+        }
+      }
+    }
+
+    &.checkbox-indeterminate {
+      .checkbox-inner {
+        background-color: #fff;
+        border-color: #d9d9d9;
+
+        &::after {
+          content: ' ';
+          left: 50%;
+          width: 8px;
+          height: 8px;
+          background-color: #7497f7;
+          border: 0;
+          transform: translate(-50%, -50%) scale(1);
+          opacity: 1;
+        }
+      }
+    }
+
+    .checkbox-input {
+      position: absolute;
+      z-index: 1;
+      width: 100%;
+      height: 100%;
+      cursor: pointer;
+      opacity: 0;
+    }
+
+    .checkbox-inner {
+      position: relative;
+      top: 0;
+      left: 0;
+      display: block;
+      width: 14px;
+      height: 14px;
+      background-color: #fff;
+      border: 1px solid #70727f;
+      border-radius: 2px;
+      transition: all 0.3s;
+
+      &::after {
+        content: ' ';
+        position: absolute;
+        top: 50%;
+        left: 21.5%;
+        display: table;
+        width: 5.71428571px;
+        height: 9.14285714px;
+        border: 2px solid #fff;
+        border-top: 0;
+        border-left: 0;
+        transform: rotate(45deg) scale(0) translate(-50%, -50%);
+        opacity: 0;
+        transition: all 0.1s cubic-bezier(0.71, -0.46, 0.88, 0.6), opacity 
0.1s;
+      }
+    }
+  }
+
+  .text {
+    font-size: 14px;
+  }
+`
diff --git 
a/config-ui/src/components/miller-columns/components/checkbox/types.ts 
b/config-ui/src/components/miller-columns/components/checkbox/types.ts
new file mode 100644
index 00000000..eb33ae2d
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export enum CheckStatus {
+  nochecked = 'nochecked',
+  checked = 'checked',
+  indeterminate = 'indeterminate'
+}
diff --git a/config-ui/src/components/miller-columns/components/index.ts 
b/config-ui/src/components/miller-columns/components/index.ts
new file mode 100644
index 00000000..25c3b950
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './checkbox'
+export * from './item'
diff --git a/config-ui/src/components/miller-columns/components/item/index.ts 
b/config-ui/src/components/miller-columns/components/item/index.ts
new file mode 100644
index 00000000..d8175878
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './item'
+export * from './item-all'
diff --git 
a/config-ui/src/components/miller-columns/components/item/item-all.tsx 
b/config-ui/src/components/miller-columns/components/item/item-all.tsx
new file mode 100644
index 00000000..a0dc0cb9
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/item-all.tsx
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import React from 'react'
+
+import type { ColumnType } from '../../types'
+
+import { Checkbox, CheckStatus } from '../checkbox'
+
+import * as S from './styled'
+
+interface Props {
+  column: ColumnType
+  checkStatus?: CheckStatus
+  onSelectAllItem?: (column: ColumnType) => void
+}
+
+export const ItemAll = ({ column, checkStatus, onSelectAllItem }: Props) => {
+  const handleCheckboxClick = (e: React.MouseEvent<HTMLDivElement>) => {
+    e.stopPropagation()
+    onSelectAllItem?.(column)
+  }
+
+  return (
+    <S.Wrapper selected={false}>
+      <Checkbox status={checkStatus} onClick={handleCheckboxClick} />
+      <span>All</span>
+    </S.Wrapper>
+  )
+}
diff --git a/config-ui/src/components/miller-columns/components/item/item.tsx 
b/config-ui/src/components/miller-columns/components/item/item.tsx
new file mode 100644
index 00000000..ca71d17c
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/item.tsx
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import React from 'react'
+
+import { ItemType, RowStatus } from '../../types'
+
+import { Checkbox, CheckStatus } from '../checkbox'
+
+import * as S from './styled'
+
+interface Props {
+  item: ItemType
+  status?: RowStatus
+  checkStatus?: CheckStatus
+  checkedCount?: number
+  onExpandItem?: (it: ItemType) => void
+  onSelectItem?: (it: ItemType) => void
+}
+
+export const Item = ({
+  item,
+  status = RowStatus.noselected,
+  checkStatus = CheckStatus.nochecked,
+  checkedCount = 0,
+  onExpandItem,
+  onSelectItem
+}: Props) => {
+  const handleRowClick = () => {
+    onExpandItem?.(item)
+  }
+
+  const handleCheckboxClick = (e: React.MouseEvent<HTMLDivElement>) => {
+    e.stopPropagation()
+    onSelectItem?.(item)
+  }
+
+  return (
+    <S.Wrapper
+      selected={status === RowStatus.selected}
+      onClick={handleRowClick}
+    >
+      <Checkbox status={checkStatus} onClick={handleCheckboxClick} />
+      <span className='title'>{item.title}</span>
+      {!!item.total && (
+        <span className='count'>
+          ({checkedCount}/{item.total})
+        </span>
+      )}
+    </S.Wrapper>
+  )
+}
diff --git a/config-ui/src/components/miller-columns/components/item/styled.ts 
b/config-ui/src/components/miller-columns/components/item/styled.ts
new file mode 100644
index 00000000..7c2dab4b
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/styled.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import styled from '@emotion/styled'
+
+export const Wrapper = styled.div<{ selected: boolean }>`
+  display: flex;
+  align-items: center;
+  padding: 4px 12px;
+  cursor: pointer;
+
+  ${({ selected }) => (selected ? 'background-color: #f5f5f7;' : '')}
+
+  &:hover {
+    background-color: #f5f5f7;
+  }
+
+  & > span.name {
+    font-size: 14px;
+  }
+
+  & > span.count {
+    font-size: 14px;
+  }
+`
diff --git a/config-ui/src/components/miller-columns/hooks/index.ts 
b/config-ui/src/components/miller-columns/hooks/index.ts
new file mode 100644
index 00000000..d9c9dbfb
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './use-columns'
+export * from './use-item-map'
+export * from './use-miller-columns'
+export * from './use-test'
diff --git a/config-ui/src/components/miller-columns/hooks/use-columns.ts 
b/config-ui/src/components/miller-columns/hooks/use-columns.ts
new file mode 100644
index 00000000..9753e2f7
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-columns.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useMemo } from 'react'
+
+import { ItemType, ItemMapType, ColumnType } from '../types'
+
+interface Props {
+  items: ItemType[]
+  itemMap: ItemMapType
+  activeItemId?: ItemType['id']
+}
+
+export const useColumns = ({ items, itemMap, activeItemId }: Props) => {
+  return useMemo(() => {
+    const rootLeaf = { items, activeId: null, parentId: null }
+
+    if (!activeItemId) {
+      return [rootLeaf]
+    }
+
+    const activeItem = itemMap.getItem(activeItemId)
+
+    const columns: ColumnType[] = [
+      {
+        parentId: activeItem.id,
+        items: activeItem.items,
+        activeId: null
+      }
+    ]
+
+    const collect = (item: ItemType) => {
+      const parent = itemMap.getItemParent(item.id)
+
+      columns.unshift({
+        parentId: parent?.id ?? null,
+        items: parent?.items ?? items,
+        activeId: item.id ?? null
+      })
+
+      if (parent) {
+        collect(parent)
+      }
+    }
+
+    collect(activeItem)
+
+    return columns
+  }, [items, itemMap, activeItemId])
+}
diff --git a/config-ui/src/components/miller-columns/hooks/use-item-map.ts 
b/config-ui/src/components/miller-columns/hooks/use-item-map.ts
new file mode 100644
index 00000000..ad62205c
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-item-map.ts
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useMemo } from 'react'
+
+import type { ItemType, ItemInfoType } from '../types'
+
+interface Props {
+  items: ItemType[]
+  selectedItemIds?: Array<ItemType['id']>
+}
+
+export const useItemMap = ({ items, selectedItemIds = [] }: Props) => {
+  return useMemo(() => {
+    const itemMap = new Map<ItemType['id'], ItemInfoType>()
+
+    const collect = ({
+      item,
+      parent
+    }: {
+      item: ItemType
+      parent?: ItemType
+    }) => {
+      if (!itemMap.has(item.id)) {
+        itemMap.set(item.id, {
+          item,
+          parentId: parent?.id,
+          selectedChildCount: 0
+        })
+      }
+
+      if (item.items) {
+        item.items.forEach((it) => collect({ item: it, parent: item }))
+      }
+    }
+
+    items.forEach((it) => collect({ item: it }))
+    selectedItemIds.forEach((id) => {
+      const childTotal = itemMap.get(id)?.item.total ?? 0
+      const addedCount = childTotal + 1
+      const parentId = itemMap.get(id)?.parentId
+      const parent = parentId ? itemMap.get(parentId) : null
+      if (parent) {
+        parent.selectedChildCount += addedCount
+      }
+    })
+
+    return {
+      getItem(id: ItemType['id']) {
+        return (itemMap.get(id) as ItemInfoType).item
+      },
+      getItemSelectedChildCount(id: ItemType['id']) {
+        return (itemMap.get(id) as ItemInfoType).selectedChildCount
+      },
+      getItemParent(id: ItemType['id']) {
+        const parentId = itemMap.get(id)?.parentId
+        return parentId ? (itemMap.get(parentId) as ItemInfoType).item : null
+      },
+      getItemMapSize() {
+        return itemMap.size
+      }
+    }
+  }, [items, selectedItemIds])
+}
diff --git 
a/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts 
b/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts
new file mode 100644
index 00000000..837f437c
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useState, useMemo, useEffect } from 'react'
+
+import type { ItemType, ColumnType } from '../types'
+import { RowStatus } from '../types'
+import { CheckStatus } from '../components'
+
+import { useItemMap } from './use-item-map'
+import { useColumns } from './use-columns'
+
+export interface UseMillerColumnsProps {
+  items: ItemType[]
+  activeItemId?: ItemType['id']
+  onActiveItemId?: (id: ItemType['id']) => void
+  selectedItemIds?: Array<ItemType['id']>
+  onSelectedItemIds?: (ids: Array<ItemType['id']>) => void
+}
+
+export const useMillerColumns = ({
+  items,
+  onActiveItemId,
+  onSelectedItemIds,
+  ...props
+}: UseMillerColumnsProps) => {
+  const [activeItemId, setActiveItemId] = useState<ItemType['id']>()
+  const [selectedItemIds, setSelectedItemIds] =
+    useState<Array<ItemType['id']>>()
+
+  const itemMap = useItemMap({ items, selectedItemIds })
+  const columns = useColumns({ items, itemMap, activeItemId })
+
+  useEffect(() => {
+    setActiveItemId(props.activeItemId)
+  }, [props.activeItemId])
+
+  useEffect(() => {
+    setSelectedItemIds(props.selectedItemIds)
+  }, [props.selectedItemIds])
+
+  return useMemo(
+    () => ({
+      columns,
+      itemMap,
+      activeItemId,
+      selectedItemIds,
+      getStatus(item: ItemType) {
+        if (item.id === activeItemId) {
+          return RowStatus.selected
+        }
+        return RowStatus.noselected
+      },
+      getChekecdStatus(item: ItemType) {
+        if (!selectedItemIds?.length) {
+          return CheckStatus.nochecked
+        }
+        if (selectedItemIds?.includes(item.id)) {
+          return CheckStatus.checked
+        }
+
+        const hasChildCheckedIds = selectedItemIds.filter((id) =>
+          item.items?.map((it) => it.id).includes(id)
+        )
+
+        if (!hasChildCheckedIds.length) {
+          return CheckStatus.nochecked
+        }
+
+        if (hasChildCheckedIds.length === item.items?.length) {
+          return CheckStatus.checked
+        }
+
+        return CheckStatus.indeterminate
+      },
+      getCheckedAllStatus(column: ColumnType) {
+        const itemIds = column.items?.map((it) => it.id) ?? []
+        const colSelectedIds = itemIds.filter((id) =>
+          selectedItemIds?.includes(id)
+        )
+        switch (true) {
+          case colSelectedIds.length === itemIds.length:
+            return CheckStatus.checked
+          case !!colSelectedIds.length:
+            return CheckStatus.indeterminate
+          default:
+            return CheckStatus.nochecked
+        }
+      },
+      getCheckedCount(item: ItemType) {
+        return itemMap.getItemSelectedChildCount(item.id)
+      },
+      onExpandItem(item: ItemType) {
+        if (!item.items?.length) {
+          return
+        }
+        onActiveItemId ? onActiveItemId(item.id) : setActiveItemId(item.id)
+      },
+      onSelectItem(item: ItemType) {
+        let newIds: Array<ItemType['id']>
+        let targetIds: Array<ItemType['id']> = [item.id]
+        const itemIds = item.items?.map((it) => it.id) ?? []
+
+        const collect = (id: ItemType['id']) => {
+          targetIds.push(id)
+          const item = itemMap.getItem(id)
+          if (item.items) {
+            item.items.forEach((it) => collect(it.id))
+          }
+        }
+
+        itemIds.forEach((id) => collect(id))
+
+        const isRemoveExistedItem = !!selectedItemIds?.includes(item.id)
+
+        if (isRemoveExistedItem) {
+          const parentItem = itemMap.getItemParent(item.id)
+          const deleteIds = [parentItem?.id, ...targetIds].filter(Boolean)
+          newIds =
+            selectedItemIds?.filter((id) => !deleteIds.includes(id)) ?? []
+        } else {
+          const parentItem = itemMap.getItemParent(item.id)
+          const addIds = targetIds.filter(
+            (id) => !selectedItemIds?.includes(id)
+          )
+
+          if (parentItem) {
+            const parentChildIds = parentItem.items?.map((it) => it.id) ?? []
+            const parentSelectedIds = parentChildIds.filter((id) =>
+              [...(selectedItemIds ?? []), item.id].includes(id)
+            )
+
+            const isAllChildSelected =
+              parentSelectedIds.length === parentItem?.items?.length
+
+            if (isAllChildSelected) {
+              addIds.push(parentItem.id)
+            }
+          }
+
+          newIds = [...(selectedItemIds ?? []), ...addIds]
+        }
+
+        onSelectedItemIds
+          ? onSelectedItemIds(newIds)
+          : setSelectedItemIds(newIds)
+      },
+      onSelectAllItem(column: ColumnType) {
+        let newIds: Array<ItemType['id']>
+        let targetIds: Array<ItemType['id']> = []
+        const itemIds = column.items?.map((it) => it.id) ?? []
+
+        const collect = (id: ItemType['id']) => {
+          targetIds.push(id)
+          const item = itemMap.getItem(id)
+          if (item.items) {
+            item.items.forEach((it) => collect(it.id))
+          }
+        }
+
+        itemIds.forEach((id) => collect(id))
+
+        const isRemoveExistedItems =
+          itemIds.filter((id) => selectedItemIds?.includes(id)).length ===
+          itemIds.length
+
+        if (isRemoveExistedItems) {
+          const deleteIds = [...targetIds, column.parentId].filter(Boolean)
+          newIds =
+            selectedItemIds?.filter((id) => !deleteIds.includes(id)) ?? []
+        } else {
+          const addIds = targetIds.filter(
+            (id) => !selectedItemIds?.includes(id)
+          )
+
+          if (column.parentId) {
+            addIds.push(column.parentId)
+          }
+
+          newIds = [...(selectedItemIds ?? []), ...addIds]
+        }
+
+        onSelectedItemIds
+          ? onSelectedItemIds(newIds)
+          : setSelectedItemIds(newIds)
+      }
+    }),
+    [columns, itemMap, activeItemId, selectedItemIds]
+  )
+}
diff --git a/config-ui/src/components/miller-columns/hooks/use-test.ts 
b/config-ui/src/components/miller-columns/hooks/use-test.ts
new file mode 100644
index 00000000..c7149f1a
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-test.ts
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useMemo, useState } from 'react'
+
+const items = [
+  {
+    id: 1,
+    title: 'merico-dev',
+    total: 13,
+    items: [
+      {
+        id: 11,
+        title: 'devlake'
+      },
+      {
+        id: 12,
+        title: 'devstream'
+      },
+      {
+        id: 13,
+        title: 'another-repo'
+      },
+      {
+        id: 14,
+        title: 'repo2'
+      },
+      {
+        id: 15,
+        title: 'repo2'
+      },
+      {
+        id: 16,
+        title: 'repo2'
+      },
+      {
+        id: 17,
+        title: 'repo2'
+      },
+      {
+        id: 18,
+        title: 'repo2'
+      },
+      {
+        id: 19,
+        title: 'ae-repo',
+        total: 4,
+        items: [
+          {
+            id: 191,
+            title: 'ae-repo-1'
+          },
+          {
+            id: 192,
+            title: 'ae-repo-2'
+          },
+          {
+            id: 193,
+            title: 'ae-repo-child',
+            total: 1,
+            items: [
+              {
+                id: 1931,
+                title: 'ae-repo-child-1'
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    id: 2,
+    title: 'mintsweet',
+    total: 2,
+    items: [
+      {
+        id: 21,
+        title: 'reate'
+      },
+      {
+        id: 22,
+        title: 'mst-advanced'
+      }
+    ]
+  },
+  {
+    id: 3,
+    title: 'test'
+  }
+]
+
+export const useTest = () => {
+  const [ids, setIds] = useState([])
+
+  console.log(ids)
+
+  return useMemo(() => {
+    return { items, ids, setIds }
+  }, [items, ids, setIds])
+}
diff --git a/config-ui/src/components/miller-columns/index.ts 
b/config-ui/src/components/miller-columns/index.ts
new file mode 100644
index 00000000..07cc2c3f
--- /dev/null
+++ b/config-ui/src/components/miller-columns/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './miller-columns'
+export * from './hooks/use-test'
diff --git a/config-ui/src/components/miller-columns/miller-columns.tsx 
b/config-ui/src/components/miller-columns/miller-columns.tsx
new file mode 100644
index 00000000..ce1948eb
--- /dev/null
+++ b/config-ui/src/components/miller-columns/miller-columns.tsx
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import React from 'react'
+
+import { useMillerColumns, UseMillerColumnsProps } from './hooks'
+import { Item, ItemAll } from './components'
+
+import * as S from './styled'
+
+interface Props extends UseMillerColumnsProps {
+  height?: number
+  firstColumnTitle?: React.ReactNode
+}
+
+export const MillerColumns = ({
+  items,
+  activeItemId,
+  onActiveItemId,
+  selectedItemIds,
+  onSelectedItemIds,
+  firstColumnTitle,
+  ...props
+}: Props) => {
+  const {
+    columns,
+    getStatus,
+    getChekecdStatus,
+    getCheckedCount,
+    onExpandItem,
+    onSelectItem,
+    getCheckedAllStatus,
+    onSelectAllItem
+  } = useMillerColumns({
+    items,
+    activeItemId,
+    onActiveItemId,
+    selectedItemIds,
+    onSelectedItemIds
+  })
+
+  return (
+    <S.Container {...props}>
+      {columns.map((col, i) => (
+        <div key={col.parentId} className='items'>
+          {i === 0 && firstColumnTitle && (
+            <div className='title'>{firstColumnTitle}</div>
+          )}
+          <ItemAll
+            column={col}
+            checkStatus={getCheckedAllStatus(col)}
+            onSelectAllItem={onSelectAllItem}
+          />
+          {col.items?.map((it) => (
+            <Item
+              key={it.id}
+              item={it}
+              status={getStatus(it)}
+              checkStatus={getChekecdStatus(it)}
+              checkedCount={getCheckedCount(it)}
+              onExpandItem={onExpandItem}
+              onSelectItem={onSelectItem}
+            />
+          ))}
+        </div>
+      ))}
+    </S.Container>
+  )
+}
diff --git a/config-ui/src/components/miller-columns/styled.ts 
b/config-ui/src/components/miller-columns/styled.ts
new file mode 100644
index 00000000..f22fb81d
--- /dev/null
+++ b/config-ui/src/components/miller-columns/styled.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import styled from '@emotion/styled'
+
+export const Container = styled.div<{ height?: number }>`
+  display: flex;
+  width: 100%;
+  ${({ height }) => `height: ${height}px;`}
+  overflow-x: auto;
+
+  .items {
+    flex: 1;
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    border: 1px solid #dbe4fd;
+    border-right: none;
+    border-radius: 4px;
+
+    &:last-child {
+      border-right: 1px solid #dbe4fd;
+    }
+
+    & > .title {
+      padding: 4px 12px;
+      font-weight: 700;
+      color: #292b3f;
+    }
+  }
+`
diff --git a/config-ui/src/components/miller-columns/types.ts 
b/config-ui/src/components/miller-columns/types.ts
new file mode 100644
index 00000000..2624f888
--- /dev/null
+++ b/config-ui/src/components/miller-columns/types.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export type ItemType = {
+  id: string | number
+  title: string
+  total?: number
+  items?: ItemType[]
+}
+
+export type ItemInfoType = {
+  item: ItemType
+  parentId?: ItemType['id']
+  selectedChildCount: number
+}
+
+export type ItemMapType = {
+  getItem: (id: ItemType['id']) => ItemType
+  getItemSelectedChildCount: (id: ItemType['id']) => number
+  getItemParent: (id: ItemType['id']) => ItemType | null
+}
+
+export type ColumnType = {
+  parentId: ItemType['id'] | null
+  items?: ItemType[]
+  activeId: ItemType['id'] | null
+}
+
+export enum RowStatus {
+  selected = 'selected',
+  noselected = 'noselected'
+}
+
+export enum CheckedStatus {
+  selected = 'selected',
+  noselected = 'noselected',
+  indeterminate = 'indeterminate'
+}


Reply via email to