Skip to content

Commit fdf5cbe

Browse files
authored
feat!: add default view functionality to package updates and create tests for validation (#1161)
1 parent c055dbb commit fdf5cbe

10 files changed

Lines changed: 206 additions & 12 deletions

File tree

bun-tests/fake-snippets-api/routes/packages/update.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,110 @@ test("update package privacy settings", async () => {
5959
expect(updatedPackage?.is_unlisted).toBe(true)
6060
})
6161

62+
test("update package default view to valid values", async () => {
63+
const { axios, db } = await getTestServer()
64+
65+
const packageResponse = await axios.post("/api/packages/create", {
66+
name: "testuser/view-package",
67+
description: "View Package",
68+
})
69+
const packageId = packageResponse.data.package.package_id
70+
71+
const validViews = ["files", "3d", "pcb", "schematic"]
72+
73+
for (const view of validViews) {
74+
const response = await axios.post("/api/packages/update", {
75+
package_id: packageId,
76+
default_view: view,
77+
})
78+
79+
expect(response.status).toBe(200)
80+
expect(response.data.ok).toBe(true)
81+
expect(response.data.package.default_view).toBe(view)
82+
83+
const updatedPackage = db.packages.find((p) => p.package_id === packageId)
84+
expect(updatedPackage?.default_view).toBe(view as any)
85+
}
86+
})
87+
88+
test("update package default view with invalid value should fail", async () => {
89+
const { axios } = await getTestServer()
90+
91+
const packageResponse = await axios.post("/api/packages/create", {
92+
name: "testuser/invalid-view-package",
93+
description: "Invalid View Package",
94+
})
95+
const packageId = packageResponse.data.package.package_id
96+
97+
const invalidViews = ["invalid", "code", "preview", "dashboard", "", null]
98+
99+
for (const invalidView of invalidViews) {
100+
try {
101+
await axios.post("/api/packages/update", {
102+
package_id: packageId,
103+
default_view: invalidView,
104+
})
105+
throw new Error(
106+
`Expected request to fail for invalid view: ${invalidView}`,
107+
)
108+
} catch (error: any) {
109+
expect(error.status).toBe(400)
110+
}
111+
}
112+
})
113+
114+
test("package has default view of files when not specified", async () => {
115+
const { axios, db } = await getTestServer()
116+
117+
const packageResponse = await axios.post("/api/packages/create", {
118+
name: "testuser/default-view-package",
119+
description: "Default View Package",
120+
})
121+
const packageId = packageResponse.data.package.package_id
122+
123+
const createdPackage = db.packages.find((p) => p.package_id === packageId)
124+
expect(createdPackage?.default_view).toBe("files")
125+
126+
const response = await axios.post("/api/packages/update", {
127+
package_id: packageId,
128+
description: "Updated without changing view",
129+
})
130+
131+
expect(response.status).toBe(200)
132+
expect(response.data.package.default_view).toBe("files")
133+
})
134+
135+
test("update package with multiple fields including default view", async () => {
136+
const { axios, db } = await getTestServer()
137+
138+
const packageResponse = await axios.post("/api/packages/create", {
139+
name: "testuser/multi-update-package",
140+
description: "Multi Update Package",
141+
})
142+
const packageId = packageResponse.data.package.package_id
143+
144+
const response = await axios.post("/api/packages/update", {
145+
package_id: packageId,
146+
name: "multi-updated-package",
147+
description: "Updated Description",
148+
website: "https://example.com",
149+
is_private: true,
150+
default_view: "pcb",
151+
})
152+
153+
expect(response.status).toBe(200)
154+
expect(response.data.ok).toBe(true)
155+
expect(response.data.package.name).toBe("testuser/multi-updated-package")
156+
expect(response.data.package.description).toBe("Updated Description")
157+
expect(response.data.package.website).toBe("https://example.com")
158+
expect(response.data.package.is_private).toBe(true)
159+
expect(response.data.package.default_view).toBe("pcb")
160+
161+
const updatedPackage = db.packages.find((p) => p.package_id === packageId)
162+
expect(updatedPackage?.default_view).toBe("pcb")
163+
expect(updatedPackage?.website).toBe("https://example.com")
164+
})
165+
62166
test("update non-existent package", async () => {
63167
const { axios } = await getTestServer()
64168

fake-snippets-api/lib/db/schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export const packageSchema = z.object({
219219
ai_description: z.string().nullable(),
220220
latest_license: z.string().nullable().optional(),
221221
ai_usage_instructions: z.string().nullable(),
222+
default_view: z
223+
.enum(["files", "3d", "pcb", "schematic"])
224+
.default("files")
225+
.optional(),
222226
})
223227
export type Package = z.infer<typeof packageSchema>
224228

fake-snippets-api/routes/api/packages/create.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default withRouteSpec({
5555
is_public: is_private === true ? false : true,
5656
is_unlisted: is_private === true ? true : (is_unlisted ?? false),
5757
ai_usage_instructions: "placeholder ai usage instructions",
58+
default_view: "files",
5859
})
5960

6061
if (!newPackage) {

fake-snippets-api/routes/api/packages/update.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default withRouteSpec({
2020
website: z.string().optional(),
2121
is_private: z.boolean().optional(),
2222
is_unlisted: z.boolean().optional(),
23+
default_view: z.enum(["files", "3d", "pcb", "schematic"]).optional(),
2324
})
2425
.transform((data) => ({
2526
...data,
@@ -30,8 +31,15 @@ export default withRouteSpec({
3031
package: packageSchema,
3132
}),
3233
})(async (req, ctx) => {
33-
const { package_id, name, description, website, is_private, is_unlisted } =
34-
req.jsonBody
34+
const {
35+
package_id,
36+
name,
37+
description,
38+
website,
39+
is_private,
40+
is_unlisted,
41+
default_view,
42+
} = req.jsonBody
3543

3644
const packageIndex = ctx.db.packages.findIndex(
3745
(p) => p.package_id === package_id,
@@ -77,6 +85,7 @@ export default withRouteSpec({
7785
is_public:
7886
is_private !== undefined ? !is_private : existingPackage.is_public,
7987
is_unlisted: is_unlisted ?? existingPackage.is_unlisted,
88+
default_view: default_view ?? existingPackage.default_view,
8089
updated_at: new Date().toISOString(),
8190
})
8291

src/components/ViewPackagePage/components/mobile-sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ const MobileSidebar = ({
211211
packageAuthor={packageInfo.owner_github_username}
212212
onUpdate={handlePackageUpdate}
213213
packageName={packageInfo.name}
214+
currentDefaultView={packageInfo.default_view}
214215
/>
215216
)}
216217
</div>

src/components/ViewPackagePage/components/repo-page-content.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import PackageHeader from "./package-header"
1919
import { useGlobalStore } from "@/hooks/use-global-store"
2020
import { useLocation } from "wouter"
2121
import { Package } from "fake-snippets-api/lib/db/schema"
22+
import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
2223

2324
interface PackageFile {
2425
package_file_id: string
@@ -43,22 +44,37 @@ export default function RepoPageContent({
4344
onFileClicked,
4445
onEditClicked,
4546
}: RepoPageContentProps) {
46-
const [location, setLocation] = useLocation()
4747
const [activeView, setActiveView] = useState<string>("files")
4848
const session = useGlobalStore((s) => s.session)
49+
const { circuitJson, isLoading: isCircuitJsonLoading } =
50+
useCurrentPackageCircuitJson()
4951

50-
// Handle hash-based view selection
52+
// Handle initial view selection and hash-based view changes
5153
useEffect(() => {
52-
// Get the hash without the # character
54+
if (isCircuitJsonLoading) return
5355
const hash = window.location.hash.slice(1)
54-
// Valid views
5556
const validViews = ["files", "3d", "pcb", "schematic", "bom"]
57+
const circuitDependentViews = ["3d", "pcb", "schematic", "bom"]
5658

57-
// If hash is a valid view, set it as active
58-
if (validViews.includes(hash)) {
59+
const availableViews = circuitJson
60+
? validViews
61+
: validViews.filter((view) => !circuitDependentViews.includes(view))
62+
63+
if (hash && availableViews.includes(hash)) {
5964
setActiveView(hash)
65+
} else if (
66+
packageInfo?.default_view &&
67+
availableViews.includes(packageInfo.default_view)
68+
) {
69+
setActiveView(packageInfo.default_view)
70+
window.location.hash = packageInfo.default_view
71+
} else {
72+
setActiveView("files")
73+
if (!hash || !availableViews.includes(hash)) {
74+
window.location.hash = "files"
75+
}
6076
}
61-
}, [])
77+
}, [packageInfo?.default_view, circuitJson, isCircuitJsonLoading])
6278

6379
const importantFilePaths = packageFiles
6480
?.filter((pf) => isPackageFileImportant(pf.file_path))

src/components/ViewPackagePage/components/sidebar-about-section.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export default function SidebarAboutSection() {
172172
packageAuthor={packageInfo.owner_github_username}
173173
onUpdate={handlePackageUpdate}
174174
packageName={packageInfo.name}
175+
currentDefaultView={packageInfo.default_view}
175176
/>
176177
)}
177178
</>

src/components/dialogs/edit-package-details-dialog.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface EditPackageDetailsDialogProps {
3535
currentDescription: string
3636
currentWebsite: string
3737
currentLicense?: string | null
38+
currentDefaultView?: string
3839
isPrivate?: boolean
3940
packageName: string
4041
packageReleaseId: string | null
@@ -43,6 +44,7 @@ interface EditPackageDetailsDialogProps {
4344
newDescription: string,
4445
newWebsite: string,
4546
newLicense: string | null,
47+
newDefaultView: string,
4648
) => void
4749
}
4850

@@ -53,6 +55,7 @@ export const EditPackageDetailsDialog = ({
5355
currentDescription,
5456
currentWebsite,
5557
currentLicense,
58+
currentDefaultView = "files",
5659
isPrivate = false,
5760
packageName,
5861
packageReleaseId,
@@ -68,12 +71,14 @@ export const EditPackageDetailsDialog = ({
6871
setFormData,
6972
websiteError,
7073
hasLicenseChanged,
74+
hasDefaultViewChanged,
7175
hasChanges,
7276
isFormValid,
7377
} = usePackageDetailsForm({
7478
initialDescription: currentDescription,
7579
initialWebsite: currentWebsite,
7680
initialLicense: currentLicense || null,
81+
initialDefaultView: currentDefaultView,
7782
isDialogOpen: open,
7883
initialVisibility: isPrivate ? "private" : "public",
7984
})
@@ -108,6 +113,7 @@ export const EditPackageDetailsDialog = ({
108113
description: formData.description,
109114
website: formData.website,
110115
is_private: formData.visibility == "private",
116+
default_view: formData.defaultView,
111117
})
112118
const privacyUpdateResponse = await axios.post("/snippets/update", {
113119
snippet_id: packageId,
@@ -149,6 +155,7 @@ export const EditPackageDetailsDialog = ({
149155
website: formData.website,
150156
license: formData.license,
151157
visibility: formData.visibility,
158+
defaultView: formData.defaultView,
152159
}
153160
},
154161
onMutate: async () => {
@@ -160,11 +167,12 @@ export const EditPackageDetailsDialog = ({
160167
website: formData.website,
161168
license: formData.license,
162169
is_private: formData.visibility == "private",
170+
default_view: formData.defaultView,
163171
}))
164172
return { previous }
165173
},
166174
onSuccess: (data) => {
167-
onUpdate?.(data.description, data.website, data.license)
175+
onUpdate?.(data.description, data.website, data.license, data.defaultView)
168176
onOpenChange(false)
169177
qc.invalidateQueries([
170178
"packageFile",
@@ -218,7 +226,7 @@ export const EditPackageDetailsDialog = ({
218226
</DialogContent>
219227
</Dialog>
220228
<Dialog open={open !== showConfirmDelete} onOpenChange={onOpenChange}>
221-
<DialogContent className="sm:max-w-[500px] lg:h-[70vh] sm:h-[90vh] overflow-y-auto w-[95vw] h-[80vh] p-6 gap-6 rounded-2xl shadow-lg">
229+
<DialogContent className="sm:max-w-[500px] lg:h-[85vh] sm:h-[90vh] overflow-y-auto no-scrollbar w-[95vw] h-[80vh] p-6 gap-6 rounded-2xl shadow-lg">
222230
<div className="flex flex-col gap-10">
223231
<DialogHeader>
224232
<DialogTitle>Edit Package Details</DialogTitle>
@@ -310,6 +318,29 @@ export const EditPackageDetailsDialog = ({
310318
</SelectContent>
311319
</Select>
312320
</div>
321+
<div className="space-y-1">
322+
<Label htmlFor="defaultView">Default View</Label>
323+
<Select
324+
value={formData.defaultView}
325+
onValueChange={(value) =>
326+
setFormData((prev) => ({
327+
...prev,
328+
defaultView: value,
329+
}))
330+
}
331+
disabled={updatePackageDetailsMutation.isLoading}
332+
>
333+
<SelectTrigger className="w-full">
334+
<SelectValue placeholder="Select default view" />
335+
</SelectTrigger>
336+
<SelectContent className="!z-[999]">
337+
<SelectItem value="files">Files</SelectItem>
338+
<SelectItem value="3d">3D</SelectItem>
339+
<SelectItem value="pcb">PCB</SelectItem>
340+
<SelectItem value="schematic">Schematic</SelectItem>
341+
</SelectContent>
342+
</Select>
343+
</div>
313344
</div>
314345

315346
<details

0 commit comments

Comments
 (0)