first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
import { memo } from 'react'
import classNames from 'classnames'
import HistoryFileTreeItem from './history-file-tree-item'
import iconTypeFromName from '../../../file-tree/util/icon-type-from-name'
import type { FileDiff } from '../../services/types/file'
import MaterialIcon from '@/shared/components/material-icon'
type HistoryFileTreeDocProps = {
file: FileDiff
name: string
selected: boolean
onClick: (file: FileDiff, event: React.MouseEvent<HTMLLIElement>) => void
onKeyDown: (file: FileDiff, event: React.KeyboardEvent<HTMLLIElement>) => void
}
function HistoryFileTreeDoc({
file,
name,
selected,
onClick,
onKeyDown,
}: HistoryFileTreeDocProps) {
return (
<li
role="treeitem"
className={classNames({ selected })}
onClick={e => onClick(file, e)}
onKeyDown={e => onKeyDown(file, e)}
aria-selected={selected}
aria-label={name}
tabIndex={0}
>
<HistoryFileTreeItem
name={name}
operation={'operation' in file ? file.operation : undefined}
icons={
<MaterialIcon
type={iconTypeFromName(name)}
className="file-tree-icon"
/>
}
/>
</li>
)
}
export default memo(HistoryFileTreeDoc)

View File

@@ -0,0 +1,87 @@
import classNames from 'classnames'
import HistoryFileTreeDoc from './history-file-tree-doc'
import HistoryFileTreeFolder from './history-file-tree-folder'
import { ReactNode, useCallback } from 'react'
import type { HistoryFileTree, HistoryDoc } from '../../utils/file-tree'
import { useHistoryContext } from '../../context/history-context'
import { FileDiff } from '../../services/types/file'
import { fileFinalPathname } from '../../utils/file-diff'
type HistoryFileTreeFolderListProps = {
folders: HistoryFileTree[]
docs: HistoryDoc[]
rootClassName?: string
children?: ReactNode
}
function HistoryFileTreeFolderList({
folders,
docs,
rootClassName,
children,
}: HistoryFileTreeFolderListProps) {
const { selection, setSelection } = useHistoryContext()
const handleEvent = useCallback(
(file: FileDiff) => {
setSelection(prevSelection => {
if (file.pathname !== prevSelection.selectedFile?.pathname) {
return {
...prevSelection,
selectedFile: file,
previouslySelectedPathname: file.pathname,
}
}
return prevSelection
})
},
[setSelection]
)
const handleClick = useCallback(
(file: FileDiff) => {
handleEvent(file)
},
[handleEvent]
)
const handleKeyDown = useCallback(
(file: FileDiff, event: React.KeyboardEvent<HTMLLIElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
handleEvent(file)
}
},
[handleEvent]
)
return (
<ul className={classNames('list-unstyled', rootClassName)} role="tree">
{folders.map(folder => (
<HistoryFileTreeFolder
key={folder.name}
name={folder.name}
folders={folder.folders}
docs={folder.docs ?? []}
/>
))}
{docs.map(doc => (
<HistoryFileTreeDoc
key={doc.pathname}
name={doc.name}
file={doc}
selected={
!!selection.selectedFile &&
fileFinalPathname(selection.selectedFile) === doc.pathname
}
onClick={handleClick}
onKeyDown={handleKeyDown}
/>
))}
{children}
</ul>
)
}
export default HistoryFileTreeFolderList

View File

@@ -0,0 +1,88 @@
import { useState, memo } from 'react'
import { useTranslation } from 'react-i18next'
import HistoryFileTreeItem from './history-file-tree-item'
import HistoryFileTreeFolderList from './history-file-tree-folder-list'
import type { HistoryDoc, HistoryFileTree } from '../../utils/file-tree'
import MaterialIcon from '@/shared/components/material-icon'
type HistoryFileTreeFolderProps = {
name: string
folders: HistoryFileTree[]
docs: HistoryDoc[]
}
function hasChanges(fileTree: HistoryFileTree): boolean {
const hasSameLevelChanges = fileTree.docs?.some(
(doc: HistoryDoc) => (doc as any).operation !== undefined
)
if (hasSameLevelChanges) {
return true
}
const hasNestedChanges = fileTree.folders?.some(folder => {
return hasChanges(folder)
})
if (hasNestedChanges) {
return true
}
return false
}
function HistoryFileTreeFolder({
name,
folders,
docs,
}: HistoryFileTreeFolderProps) {
const { t } = useTranslation()
const [expanded, setExpanded] = useState(() => {
return hasChanges({ name, folders, docs })
})
const icons = (
<>
<button
onClick={() => setExpanded(!expanded)}
aria-label={expanded ? t('collapse') : t('expand')}
className="history-file-tree-folder-button"
>
<MaterialIcon
type={expanded ? 'expand_more' : 'chevron_right'}
className="file-tree-expand-icon"
/>
</button>
<MaterialIcon
type={expanded ? 'folder_open' : 'folder'}
className="file-tree-folder-icon"
/>
</>
)
return (
<>
<li
// FIXME
// eslint-disable-next-line jsx-a11y/role-has-required-aria-props
role="treeitem"
aria-expanded={expanded}
aria-label={name}
tabIndex={0}
onClick={() => setExpanded(!expanded)}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
setExpanded(!expanded)
}
}}
>
<HistoryFileTreeItem name={name} icons={icons} />
</li>
{expanded ? (
<HistoryFileTreeFolderList folders={folders} docs={docs} />
) : null}
</>
)
}
export default memo(HistoryFileTreeFolder)

View File

@@ -0,0 +1,34 @@
import classNames from 'classnames'
import type { ReactNode } from 'react'
import type { FileOperation } from '../../services/types/file-operation'
import OLTag from '@/features/ui/components/ol/ol-tag'
type FileTreeItemProps = {
name: string
operation?: FileOperation
icons: ReactNode
}
export default function HistoryFileTreeItem({
name,
operation,
icons,
}: FileTreeItemProps) {
return (
<div className="history-file-tree-item" role="presentation">
{icons}
<div className="history-file-tree-item-name-wrapper">
<div
className={classNames('history-file-tree-item-name', {
strikethrough: operation === 'removed',
})}
>
{name}
</div>
{operation && (
<OLTag className="history-file-tree-item-badge">{operation}</OLTag>
)}
</div>
</div>
)
}