@orsetra/shared-ui 1.0.32 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ui/index.ts +1 -0
- package/components/ui/project-selector-modal.tsx +185 -0
- package/lib/utils.ts +22 -0
- package/package.json +1 -1
package/components/ui/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export { Collapsible, CollapsibleTrigger, CollapsibleContent } from './collapsib
|
|
|
33
33
|
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } from './command'
|
|
34
34
|
export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup } from './context-menu'
|
|
35
35
|
export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from './dialog'
|
|
36
|
+
export { ProjectSelectorModal, type Project, type ProjectSelectorModalProps } from './project-selector-modal'
|
|
36
37
|
export { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription } from './drawer'
|
|
37
38
|
export { Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField } from './form'
|
|
38
39
|
export { HoverCard, HoverCardTrigger, HoverCardContent } from './hover-card'
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "./dialog"
|
|
11
|
+
import { Button } from "./button"
|
|
12
|
+
import { Label } from "./label"
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from "./select"
|
|
20
|
+
import { Loader2, FolderKanban } from "lucide-react"
|
|
21
|
+
|
|
22
|
+
export interface Project {
|
|
23
|
+
name: string
|
|
24
|
+
alias?: string
|
|
25
|
+
description?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProjectSelectorModalProps {
|
|
29
|
+
open: boolean
|
|
30
|
+
onOpenChange?: (open: boolean) => void
|
|
31
|
+
getProjects: () => Promise<Project[]>
|
|
32
|
+
storage: {
|
|
33
|
+
getItem: (key: string) => string | null
|
|
34
|
+
setItem: (key: string, value: string) => void
|
|
35
|
+
}
|
|
36
|
+
storageKey?: string
|
|
37
|
+
doInit: () => void | Promise<void>
|
|
38
|
+
title?: string
|
|
39
|
+
description?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ProjectSelectorModal({
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
getProjects,
|
|
46
|
+
storage,
|
|
47
|
+
storageKey = "current-project",
|
|
48
|
+
doInit,
|
|
49
|
+
title = "Select Project",
|
|
50
|
+
description = "Choose a project to continue. This will be used for all operations in this application.",
|
|
51
|
+
}: ProjectSelectorModalProps) {
|
|
52
|
+
const [projects, setProjects] = React.useState<Project[]>([])
|
|
53
|
+
const [loading, setLoading] = React.useState(true)
|
|
54
|
+
const [selectedProject, setSelectedProject] = React.useState<string>("")
|
|
55
|
+
const [submitting, setSubmitting] = React.useState(false)
|
|
56
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
57
|
+
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
if (open) {
|
|
60
|
+
loadProjects()
|
|
61
|
+
// Try to load current project from storage
|
|
62
|
+
const currentProject = storage.getItem(storageKey)
|
|
63
|
+
if (currentProject) {
|
|
64
|
+
setSelectedProject(currentProject)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, [open, storage, storageKey])
|
|
68
|
+
|
|
69
|
+
const loadProjects = async () => {
|
|
70
|
+
setLoading(true)
|
|
71
|
+
setError(null)
|
|
72
|
+
try {
|
|
73
|
+
const projectsList = await getProjects()
|
|
74
|
+
setProjects(projectsList)
|
|
75
|
+
|
|
76
|
+
// If no project selected yet and we have projects, select the first one
|
|
77
|
+
if (!selectedProject && projectsList.length > 0) {
|
|
78
|
+
setSelectedProject(projectsList[0].name)
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error("Failed to load projects:", err)
|
|
82
|
+
setError(err instanceof Error ? err.message : "Failed to load projects")
|
|
83
|
+
} finally {
|
|
84
|
+
setLoading(false)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
|
|
91
|
+
if (!selectedProject) {
|
|
92
|
+
setError("Please select a project")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setSubmitting(true)
|
|
97
|
+
setError(null)
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Save to storage
|
|
101
|
+
storage.setItem(storageKey, selectedProject)
|
|
102
|
+
|
|
103
|
+
// Call doInit
|
|
104
|
+
await Promise.resolve(doInit())
|
|
105
|
+
|
|
106
|
+
// Close modal
|
|
107
|
+
onOpenChange?.(false)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error("Failed to initialize:", err)
|
|
110
|
+
setError(err instanceof Error ? err.message : "Failed to initialize")
|
|
111
|
+
setSubmitting(false)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
117
|
+
<DialogContent className="rounded-none sm:max-w-md" onInteractOutside={(e) => e.preventDefault()}>
|
|
118
|
+
<DialogHeader>
|
|
119
|
+
<DialogTitle className="flex items-center gap-2">
|
|
120
|
+
<FolderKanban className="h-5 w-5 text-ibm-blue-60" />
|
|
121
|
+
{title}
|
|
122
|
+
</DialogTitle>
|
|
123
|
+
<DialogDescription>{description}</DialogDescription>
|
|
124
|
+
</DialogHeader>
|
|
125
|
+
|
|
126
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
127
|
+
{loading ? (
|
|
128
|
+
<div className="flex items-center justify-center py-8">
|
|
129
|
+
<Loader2 className="h-6 w-6 animate-spin text-ibm-blue-60" />
|
|
130
|
+
<span className="ml-2 text-sm text-ibm-gray-70">Loading projects...</span>
|
|
131
|
+
</div>
|
|
132
|
+
) : error ? (
|
|
133
|
+
<div className="rounded-none border border-red-200 bg-red-50 p-3 text-sm text-red-800">
|
|
134
|
+
{error}
|
|
135
|
+
</div>
|
|
136
|
+
) : projects.length === 0 ? (
|
|
137
|
+
<div className="text-center py-8 text-sm text-ibm-gray-60">
|
|
138
|
+
No projects available
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
<Label htmlFor="project">
|
|
143
|
+
Project <span className="text-red-600">*</span>
|
|
144
|
+
</Label>
|
|
145
|
+
<Select value={selectedProject} onValueChange={setSelectedProject}>
|
|
146
|
+
<SelectTrigger id="project" className="rounded-none">
|
|
147
|
+
<SelectValue placeholder="Select a project" />
|
|
148
|
+
</SelectTrigger>
|
|
149
|
+
<SelectContent className="rounded-none">
|
|
150
|
+
{projects.map((project) => (
|
|
151
|
+
<SelectItem key={project.name} value={project.name} className="rounded-none">
|
|
152
|
+
<div className="flex flex-col">
|
|
153
|
+
<span className="font-medium">{project.alias || project.name}</span>
|
|
154
|
+
{project.description && (
|
|
155
|
+
<span className="text-xs text-ibm-gray-60">{project.description}</span>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</SelectItem>
|
|
159
|
+
))}
|
|
160
|
+
</SelectContent>
|
|
161
|
+
</Select>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<div className="flex items-center justify-end gap-3 pt-2">
|
|
166
|
+
<Button
|
|
167
|
+
type="submit"
|
|
168
|
+
className="rounded-none"
|
|
169
|
+
disabled={submitting || loading || !selectedProject || projects.length === 0}
|
|
170
|
+
>
|
|
171
|
+
{submitting ? (
|
|
172
|
+
<>
|
|
173
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
174
|
+
Initializing...
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
"Continue"
|
|
178
|
+
)}
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
</form>
|
|
182
|
+
</DialogContent>
|
|
183
|
+
</Dialog>
|
|
184
|
+
)
|
|
185
|
+
}
|
package/lib/utils.ts
CHANGED
|
@@ -4,3 +4,25 @@ import { twMerge } from "tailwind-merge"
|
|
|
4
4
|
export function cn(...inputs: ClassValue[]) {
|
|
5
5
|
return twMerge(clsx(inputs))
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
export function getTypeColor(type: string) {
|
|
9
|
+
switch (type) {
|
|
10
|
+
case "production":
|
|
11
|
+
return "bg-red-10 text-red-70 border-red-30"
|
|
12
|
+
case "staging":
|
|
13
|
+
return "bg-yellow-10 text-yellow-70 border-yellow-30"
|
|
14
|
+
case "development":
|
|
15
|
+
case "dev":
|
|
16
|
+
return "bg-blue-10 text-blue-70 border-blue-30"
|
|
17
|
+
case "test":
|
|
18
|
+
return "bg-purple-10 text-purple-70 border-purple-30"
|
|
19
|
+
case "sandbox":
|
|
20
|
+
return "bg-green-10 text-green-70 border-green-30"
|
|
21
|
+
case "ACTIVE":
|
|
22
|
+
return "bg-green-10 text-green-70 border-green-30"
|
|
23
|
+
case "INACTIVE":
|
|
24
|
+
return "bg-red-10 text-red-70 border-red-30"
|
|
25
|
+
default:
|
|
26
|
+
return "bg-gray-10 text-gray-70 border-gray-30"
|
|
27
|
+
}
|
|
28
|
+
}
|