Skip to content

Commit 4978a32

Browse files
committed
chore: ui refactor
1 parent 3e1987c commit 4978a32

File tree

5 files changed

+511
-175
lines changed

5 files changed

+511
-175
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { useState } from 'react'
2+
3+
import { Checkbox } from '@/components/ui/checkbox'
4+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
5+
6+
import { runCreateApp } from '@/lib/server-fns'
7+
import FileViewer from './file-viewer'
8+
import FileTree from './file-tree'
9+
10+
import type { Mode } from '@tanstack/cta-engine'
11+
12+
export default function AppliedAddOn({
13+
projectPath,
14+
output: originalOutput,
15+
addOnInfo,
16+
outputWithoutAddon: originalOutputWithoutAddon,
17+
originalOptions,
18+
addOns,
19+
}: {
20+
projectPath: string
21+
output: {
22+
files: Record<string, string>
23+
}
24+
outputWithoutAddon: {
25+
files: Record<string, string>
26+
}
27+
addOnInfo: {
28+
templates: Array<string>
29+
}
30+
originalOptions: {
31+
existingAddOns: Array<string>
32+
mode: Mode
33+
}
34+
addOns: Record<string, Array<any>>
35+
}) {
36+
const [selectedFile, setSelectedFile] = useState<string | null>(null)
37+
38+
const [options, setOptions] = useState(originalOptions)
39+
const [output, setOutput] = useState(originalOutput)
40+
const [outputWithoutAddon, setOutputWithoutAddon] = useState(
41+
originalOutputWithoutAddon,
42+
)
43+
const [selectedAddOns, setSelectedAddOns] = useState<Array<string>>([])
44+
45+
async function updateOptions(
46+
updatedOptions: Partial<typeof options>,
47+
updatedAddOns: Array<string> = [],
48+
) {
49+
const newMode = updatedOptions.mode || options.mode
50+
const existingAddOns = [
51+
...(originalOptions.existingAddOns || []),
52+
...(updatedAddOns || []),
53+
].filter((id) => addOns[newMode as Mode].some((addOn) => addOn.id === id))
54+
55+
const newOptions = {
56+
...options,
57+
...updatedOptions,
58+
existingAddOns,
59+
}
60+
setOptions(newOptions)
61+
const [newOutput, newOutputWithoutAddon] = await Promise.all([
62+
runCreateApp({
63+
data: { withAddOn: true, options: newOptions },
64+
}),
65+
runCreateApp({
66+
data: { withAddOn: false, options: newOptions },
67+
}),
68+
])
69+
setOutput(newOutput)
70+
setOutputWithoutAddon(newOutputWithoutAddon)
71+
}
72+
73+
return (
74+
<div>
75+
<div className="flex flex-row items-center mb-5">
76+
<ToggleGroup
77+
type="single"
78+
value={options.mode}
79+
onValueChange={(v: string) => {
80+
if (v) {
81+
updateOptions(
82+
{
83+
mode: v as Mode,
84+
},
85+
selectedAddOns,
86+
)
87+
}
88+
}}
89+
>
90+
<ToggleGroupItem
91+
value="code-router"
92+
disabled={!addOnInfo.templates.includes('code-router')}
93+
>
94+
Code Router
95+
</ToggleGroupItem>
96+
<ToggleGroupItem
97+
value="file-router"
98+
disabled={!addOnInfo.templates.includes('file-router')}
99+
>
100+
File Router
101+
</ToggleGroupItem>
102+
</ToggleGroup>
103+
<div className="flex flex-row ml-5 flex-wrap">
104+
{addOns[options.mode as Mode].map((addOn) => (
105+
<div key={addOn.name} className="mr-2 flex items-center">
106+
<Checkbox
107+
id={addOn.id}
108+
checked={
109+
originalOptions.existingAddOns.includes(addOn.id) ||
110+
selectedAddOns.includes(addOn.id)
111+
}
112+
disabled={originalOptions.existingAddOns.includes(addOn.id)}
113+
onClick={() => {
114+
let updatedAddOns = selectedAddOns.includes(addOn.id)
115+
? selectedAddOns.filter((id) => id !== addOn.id)
116+
: [...selectedAddOns, addOn.id]
117+
setSelectedAddOns(updatedAddOns)
118+
updateOptions({}, updatedAddOns)
119+
}}
120+
/>
121+
<label htmlFor={addOn.id} className="ml-2">
122+
{addOn.name}
123+
</label>
124+
</div>
125+
))}
126+
</div>
127+
</div>
128+
<div className="flex flex-row">
129+
<FileTree
130+
prefix={projectPath}
131+
tree={output.files}
132+
originalTree={outputWithoutAddon.files}
133+
onFileSelected={(file) => {
134+
setSelectedFile(file)
135+
}}
136+
/>
137+
<div className="max-w-3/4 w-3/4 pl-2">
138+
{selectedFile ? (
139+
<FileViewer
140+
filePath={selectedFile}
141+
originalFile={outputWithoutAddon.files[selectedFile]}
142+
modifiedFile={output.files[selectedFile]}
143+
/>
144+
) : null}
145+
</div>
146+
</div>
147+
</div>
148+
)
149+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useMemo } from 'react'
2+
import { FileText, Folder } from 'lucide-react'
3+
4+
import { TreeView } from '@/components/ui/tree-view'
5+
6+
import type { TreeDataItem } from '@/components/ui/tree-view'
7+
8+
export default function FileTree({
9+
prefix,
10+
tree,
11+
originalTree,
12+
onFileSelected,
13+
extraTreeItems = [],
14+
}: {
15+
prefix: string
16+
tree: Record<string, string>
17+
originalTree: Record<string, string>
18+
onFileSelected: (file: string) => void
19+
extraTreeItems?: Array<TreeDataItem>
20+
}) {
21+
const computedTree = useMemo(() => {
22+
const treeData: Array<TreeDataItem> = []
23+
24+
function changed(file: string) {
25+
if (!originalTree[file]) {
26+
return true
27+
}
28+
return tree[file] !== originalTree[file]
29+
}
30+
31+
Object.keys(tree)
32+
.sort()
33+
.forEach((file) => {
34+
const parts = file.replace(`${prefix}/`, '').split('/')
35+
36+
let currentLevel = treeData
37+
parts.forEach((part, index) => {
38+
const existingNode = currentLevel.find((node) => node.name === part)
39+
if (existingNode) {
40+
currentLevel = existingNode.children || []
41+
} else {
42+
const newNode: TreeDataItem = {
43+
id: index === parts.length - 1 ? file : `${file}-${index}`,
44+
name: part,
45+
children: index < parts.length - 1 ? [] : undefined,
46+
icon:
47+
index < parts.length - 1
48+
? () => <Folder className="w-4 h-4 mr-2" />
49+
: () => <FileText className="w-4 h-4 mr-2" />,
50+
onClick:
51+
index === parts.length - 1
52+
? () => {
53+
onFileSelected(file)
54+
}
55+
: undefined,
56+
className:
57+
index === parts.length - 1 && changed(file)
58+
? 'text-green-300'
59+
: '',
60+
}
61+
currentLevel.push(newNode)
62+
currentLevel = newNode.children!
63+
}
64+
})
65+
})
66+
return [...extraTreeItems, ...treeData]
67+
}, [prefix, tree, originalTree, extraTreeItems])
68+
69+
return (
70+
<TreeView
71+
data={computedTree}
72+
defaultNodeIcon={() => <Folder className="w-4 h-4 mr-2" />}
73+
defaultLeafIcon={() => <FileText className="w-4 h-4 mr-2" />}
74+
className="max-w-1/4 w-1/4 pr-2"
75+
/>
76+
)
77+
}

cli/add-on-developer/src/components/file-viewer.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function FileViewer({
1313
modifiedFile,
1414
filePath,
1515
}: {
16-
originalFile: string
16+
originalFile?: string
1717
modifiedFile: string
1818
filePath: string
1919
}) {
@@ -37,7 +37,7 @@ export default function FileViewer({
3737
}
3838
const language = getLanguage(filePath)
3939

40-
if (!originalFile) {
40+
if (!originalFile || originalFile === modifiedFile) {
4141
return (
4242
<CodeMirror
4343
value={modifiedFile}
@@ -51,14 +51,7 @@ export default function FileViewer({
5151
)
5252
}
5353
return (
54-
<CodeMirrorMerge
55-
orientation="b-a"
56-
theme={okaidia}
57-
height="100vh"
58-
width="100%"
59-
readOnly
60-
className="text-lg"
61-
>
54+
<CodeMirrorMerge orientation="a-b" theme={okaidia} className="text-lg">
6255
<CodeMirrorMerge.Original value={originalFile} extensions={[language]} />
6356
<CodeMirrorMerge.Modified value={modifiedFile} extensions={[language]} />
6457
</CodeMirrorMerge>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
function Table({ className, ...props }: React.ComponentProps<"table">) {
6+
return (
7+
<div
8+
data-slot="table-container"
9+
className="relative w-full overflow-x-auto"
10+
>
11+
<table
12+
data-slot="table"
13+
className={cn("w-full caption-bottom text-sm", className)}
14+
{...props}
15+
/>
16+
</div>
17+
)
18+
}
19+
20+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
21+
return (
22+
<thead
23+
data-slot="table-header"
24+
className={cn("[&_tr]:border-b", className)}
25+
{...props}
26+
/>
27+
)
28+
}
29+
30+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
31+
return (
32+
<tbody
33+
data-slot="table-body"
34+
className={cn("[&_tr:last-child]:border-0", className)}
35+
{...props}
36+
/>
37+
)
38+
}
39+
40+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
41+
return (
42+
<tfoot
43+
data-slot="table-footer"
44+
className={cn(
45+
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
46+
className
47+
)}
48+
{...props}
49+
/>
50+
)
51+
}
52+
53+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
54+
return (
55+
<tr
56+
data-slot="table-row"
57+
className={cn(
58+
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
59+
className
60+
)}
61+
{...props}
62+
/>
63+
)
64+
}
65+
66+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
67+
return (
68+
<th
69+
data-slot="table-head"
70+
className={cn(
71+
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
72+
className
73+
)}
74+
{...props}
75+
/>
76+
)
77+
}
78+
79+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
80+
return (
81+
<td
82+
data-slot="table-cell"
83+
className={cn(
84+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
85+
className
86+
)}
87+
{...props}
88+
/>
89+
)
90+
}
91+
92+
function TableCaption({
93+
className,
94+
...props
95+
}: React.ComponentProps<"caption">) {
96+
return (
97+
<caption
98+
data-slot="table-caption"
99+
className={cn("text-muted-foreground mt-4 text-sm", className)}
100+
{...props}
101+
/>
102+
)
103+
}
104+
105+
export {
106+
Table,
107+
TableHeader,
108+
TableBody,
109+
TableFooter,
110+
TableHead,
111+
TableRow,
112+
TableCell,
113+
TableCaption,
114+
}

0 commit comments

Comments
 (0)