improvement: modals

This commit is contained in:
Emir Karabeg
2026-03-10 22:30:29 -07:00
parent a4ac7155f2
commit 3c0da7671a
63 changed files with 568 additions and 122 deletions

View File

@@ -14,6 +14,10 @@ import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const patchWorkspaceSchema = z.object({
name: z.string().trim().min(1).optional(),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/)
.optional(),
billedAccountUserId: z.string().optional(),
allowPersonalApiKeys: z.boolean().optional(),
})
@@ -113,10 +117,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
try {
const body = patchWorkspaceSchema.parse(await request.json())
const { name, billedAccountUserId, allowPersonalApiKeys } = body
const { name, color, billedAccountUserId, allowPersonalApiKeys } = body
if (
name === undefined &&
color === undefined &&
billedAccountUserId === undefined &&
allowPersonalApiKeys === undefined
) {
@@ -139,6 +144,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
updateData.name = name
}
if (color !== undefined) {
updateData.color = color
}
if (allowPersonalApiKeys !== undefined) {
updateData.allowPersonalApiKeys = Boolean(allowPersonalApiKeys)
}

View File

@@ -9,11 +9,16 @@ import { getSession } from '@/lib/auth'
import { PlatformEvents } from '@/lib/core/telemetry'
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { getRandomWorkspaceColor } from '@/lib/workspaces/colors'
const logger = createLogger('Workspaces')
const createWorkspaceSchema = z.object({
name: z.string().trim().min(1, 'Name is required'),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/)
.optional(),
skipDefaultWorkflow: z.boolean().optional().default(false),
})
@@ -65,9 +70,9 @@ export async function POST(req: Request) {
}
try {
const { name, skipDefaultWorkflow } = createWorkspaceSchema.parse(await req.json())
const { name, color, skipDefaultWorkflow } = createWorkspaceSchema.parse(await req.json())
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow)
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow, color)
recordAudit({
workspaceId: newWorkspace.id,
@@ -96,16 +101,23 @@ async function createDefaultWorkspace(userId: string, userName?: string | null)
return createWorkspace(userId, workspaceName)
}
async function createWorkspace(userId: string, name: string, skipDefaultWorkflow = false) {
async function createWorkspace(
userId: string,
name: string,
skipDefaultWorkflow = false,
explicitColor?: string
) {
const workspaceId = crypto.randomUUID()
const workflowId = crypto.randomUUID()
const now = new Date()
const color = explicitColor || getRandomWorkspaceColor()
try {
await db.transaction(async (tx) => {
await tx.insert(workspace).values({
id: workspaceId,
name,
color,
ownerId: userId,
billedAccountUserId: userId,
allowPersonalApiKeys: true,
@@ -174,6 +186,7 @@ async function createWorkspace(userId: string, name: string, skipDefaultWorkflow
return {
id: workspaceId,
name,
color,
ownerId: userId,
billedAccountUserId: userId,
allowPersonalApiKeys: true,

View File

@@ -596,7 +596,7 @@ export function Files() {
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
You have unsaved changes. Are you sure you want to discard them?
</p>
</ModalBody>
@@ -731,7 +731,7 @@ function DeleteConfirmModal({
<ModalContent size='sm'>
<ModalHeader>Delete File</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{fileName}</span>?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>

View File

@@ -34,7 +34,7 @@ export function DeleteChunkModal({
<ModalContent size='sm'>
<ModalHeader>Delete Chunk</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete this chunk?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>

View File

@@ -72,7 +72,7 @@ function UnsavedChangesModal({
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
You have unsaved changes. Are you sure you want to discard them?
</p>
</ModalBody>
@@ -1168,7 +1168,7 @@ export function Document({
<ModalContent size='sm'>
<ModalHeader>Delete Document</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{effectiveDocumentName}

View File

@@ -1121,7 +1121,7 @@ export function KnowledgeBase({
<ModalContent size='sm'>
<ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
This will permanently delete the knowledge base and all {pagination.total} document
@@ -1151,7 +1151,7 @@ export function KnowledgeBase({
{(() => {
const docToDelete = documents.find((doc) => doc.id === documentToDelete)
return (
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{docToDelete?.filename ?? 'this document'}
@@ -1190,7 +1190,7 @@ export function KnowledgeBase({
<ModalContent size='sm'>
<ModalHeader>Delete Documents</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete {selectedDocuments.size} document
{selectedDocuments.size === 1 ? '' : 's'}?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>

View File

@@ -419,7 +419,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
<ModalHeader>Delete Tag</ModalHeader>
<ModalBody>
<div className='space-y-[8px]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will
remove this tag from {selectedTagUsage?.documentCount || 0} document
{selectedTagUsage?.documentCount !== 1 ? 's' : ''}.{' '}
@@ -462,7 +462,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
<ModalHeader>Documents using "{selectedTag?.displayName}"</ModalHeader>
<ModalBody>
<div className='space-y-[8px]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
{selectedTagUsage?.documentCount || 0} document
{selectedTagUsage?.documentCount !== 1 ? 's are' : ' is'} currently using this tag
definition.
@@ -470,7 +470,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
{selectedTagUsage?.documentCount === 0 ? (
<div className='rounded-[6px] border p-[16px] text-center'>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
This tag definition is not being used by any documents. You can safely delete it
to free up the tag slot.
</p>

View File

@@ -41,7 +41,7 @@ export function DeleteKnowledgeBaseModal({
<ModalContent size='sm'>
<ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
{knowledgeBaseName ? (
<>
Are you sure you want to delete{' '}

View File

@@ -226,7 +226,7 @@ export function Schedules() {
/>
<Modal open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Schedule</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -378,7 +378,7 @@ export function ApiKeys() {
<ModalContent size='sm'>
<ModalHeader>Delete Sim key</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Deleting{' '}
<span className='font-medium text-[var(--text-primary)]'>{deleteKey?.name}</span> will
immediately revoke access for any integrations using it.{' '}

View File

@@ -115,7 +115,7 @@ export function CreateApiKeyModal({
<ModalContent size='md'>
<ModalHeader>Create new Sim key</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
{keyType === 'workspace'
? "This key will have access to all workflows in this workspace. Make sure to copy it after creation as you won't be able to see it again."
: "This key will have access to your personal workflows. Make sure to copy it after creation as you won't be able to see it again."}
@@ -218,7 +218,7 @@ export function CreateApiKeyModal({
<ModalContent size='sm'>
<ModalHeader>Your Sim key has been created</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
This is the only time you will see your Sim key.{' '}
<span className='font-semibold text-[var(--text-primary)]'>
Copy it now and store it securely.

View File

@@ -319,7 +319,7 @@ export function BYOK() {
)}
</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
This key will be used for all {PROVIDERS.find((p) => p.id === editingProvider)?.name}{' '}
requests in this workspace. Your key is encrypted and stored securely.
</p>
@@ -405,7 +405,7 @@ export function BYOK() {
<ModalContent size='sm'>
<ModalHeader>Delete API Key</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete the{' '}
<span className='font-medium text-[var(--text-primary)]'>
{PROVIDERS.find((p) => p.id === deleteConfirmProvider)?.name}

View File

@@ -264,7 +264,7 @@ export function Copilot() {
<ModalContent size='sm'>
<ModalHeader>Create new API key</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
This key will allow access to Copilot features. Make sure to copy it after creation as
you won't be able to see it again.
</p>
@@ -326,7 +326,7 @@ export function Copilot() {
<ModalContent size='sm'>
<ModalHeader>Your API key has been created</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
This is the only time you will see your API key.{' '}
<span className='font-semibold text-[var(--text-primary)]'>
Copy it now and store it securely.
@@ -363,7 +363,7 @@ export function Copilot() {
<ModalContent size='sm'>
<ModalHeader>Delete API key</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Deleting{' '}
<span className='font-medium text-[var(--text-primary)]'>
{deleteKey?.name || 'Unnamed Key'}

View File

@@ -857,7 +857,7 @@ export function CredentialSets() {
<ModalContent size='sm'>
<ModalHeader>Leave Polling Group</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to leave{' '}
<span className='font-medium text-[var(--text-primary)]'>
{leavingMembership?.name}
@@ -884,7 +884,7 @@ export function CredentialSets() {
<ModalContent size='sm'>
<ModalHeader>Delete Polling Group</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{deletingSet?.name}</span>?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>

View File

@@ -1029,7 +1029,7 @@ export function CredentialsManager() {
if (!open) resetCreateForm()
}}
>
<ModalContent size='lg'>
<ModalContent size='md'>
<ModalHeader>Create Secret</ModalHeader>
<ModalBody>
{(createError ||
@@ -1402,7 +1402,7 @@ export function CredentialsManager() {
{credentialToDelete?.type === 'oauth' ? 'Disconnect Secret' : 'Delete Secret'}
</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to{' '}
{credentialToDelete?.type === 'oauth' ? 'disconnect' : 'delete'}{' '}
<span className='font-medium text-[var(--text-primary)]'>
@@ -1444,7 +1444,7 @@ export function CredentialsManager() {
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
You have unsaved changes. Are you sure you want to discard them?
</p>
</ModalBody>

View File

@@ -189,7 +189,7 @@ export function CustomTools() {
<ModalContent size='sm'>
<ModalHeader>Delete Custom Tool</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{toolToDelete?.name}</span>?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>

View File

@@ -496,7 +496,7 @@ export function General() {
<ModalContent size='sm'>
<ModalHeader>Reset Password</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
A password reset link will be sent to{' '}
<span className='font-medium text-[var(--text-primary)]'>{profile?.email}</span>.
Click the link in the email to create a new password.

View File

@@ -715,7 +715,7 @@ export function MCP({ initialServerId }: MCPProps) {
<ModalContent size='sm'>
<ModalHeader>Delete MCP Server</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{serverToDelete?.name}</span>
? <span className='text-[var(--text-error)]'>This action cannot be undone.</span>

View File

@@ -126,7 +126,7 @@ export function SkillModal({
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent size='xl'>
<ModalContent size='lg'>
<ModalHeader>{initialValues ? 'Edit Skill' : 'Create Skill'}</ModalHeader>
<ModalBody>
<div className='flex flex-col gap-[18px]'>

View File

@@ -170,7 +170,7 @@ export function Skills() {
<ModalContent size='sm'>
<ModalHeader>Delete Skill</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{skillToDelete?.name}</span>?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>

View File

@@ -144,7 +144,7 @@ export function CreditBalance({
</div>
<div className='rounded-[6px] bg-[var(--surface-4)] p-[12px]'>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Credits are non-refundable and don't expire. They'll be applied automatically
to your {entityType === 'organization' ? 'team' : ''} usage.
</p>

View File

@@ -1043,7 +1043,7 @@ function TeamPlanModal({ open, onOpenChange, isAnnual, onConfirm }: TeamPlanModa
<ModalContent size='sm'>
<ModalHeader>Get For Team</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Choose a plan and number of seats for your team. Credits are pooled across all members.
</p>
@@ -1257,7 +1257,7 @@ function ManagePlanModal({
Manage {currentTier.name} Plan{isTeamPlan ? ' (Team)' : ''}
</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
You're on the{' '}
<span className='font-medium text-[var(--text-primary)]'>{currentTier.name}</span> plan
{isTeamPlan ? ' for your team' : ''}, billed{' '}

View File

@@ -36,7 +36,7 @@ export function RemoveMemberDialog({
<ModalContent size='sm'>
<ModalHeader>{isSelfRemoval ? 'Leave Organization' : 'Remove Team Member'}</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
{isSelfRemoval ? (
'Are you sure you want to leave this organization? You will lose access to all team resources.'
) : (

View File

@@ -68,7 +68,7 @@ export function TeamSeats({
<ModalContent size='sm'>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>{description}</p>
<p className='text-[var(--text-secondary)]'>{description}</p>
<div className='mt-[16px] flex flex-col gap-[4px]'>
<Label htmlFor='seats' className='text-[13px]'>

View File

@@ -629,7 +629,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
<ModalContent size='sm'>
<ModalHeader>Remove Workflow</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to remove{' '}
<span className='font-medium text-[var(--text-primary)]'>
{toolToDelete?.toolName}
@@ -662,7 +662,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
}
}}
>
<ModalContent className='w-[480px]'>
<ModalContent size='md'>
<ModalHeader>{toolToView?.toolName}</ModalHeader>
<ModalBody>
<div className='flex flex-col gap-[18px]'>
@@ -812,10 +812,10 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
}
}}
>
<ModalContent className='w-[420px]'>
<ModalContent size='sm'>
<ModalHeader>Add Workflow</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Select a deployed workflow to add to this MCP server. The workflow will be available
as a tool.
</p>
@@ -1215,7 +1215,7 @@ export function WorkflowMcpServers() {
<ModalContent size='sm'>
<ModalHeader>Delete MCP Server</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{serverToDelete?.name}</span>
? <span className='text-[var(--text-error)]'>This action cannot be undone.</span>

View File

@@ -1,17 +1,20 @@
import {
BookOpen,
Bug,
Card,
HexSimple,
Key,
KeySquare,
LogIn,
Mail,
Server,
Settings,
ShieldCheck,
TerminalWindow,
User,
Users,
Wrench,
} from 'lucide-react'
import { Card, HexSimple, Key, TerminalWindow } from '@/components/emcn'
} from '@/components/emcn'
import { AgentSkillsIcon, McpIcon } from '@/components/icons'
import { getEnv, isTruthy } from '@/lib/core/config/env'

View File

@@ -162,7 +162,7 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
{error}
</div>
)}
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
{isSingleRow ? 'this row' : `these ${deleteCount} rows`}? This will permanently remove
all data in {isSingleRow ? 'this row' : 'these rows'}.{' '}
@@ -186,7 +186,7 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
return (
<Modal open={isOpen} onOpenChange={handleClose}>
<ModalContent className='w-[600px]'>
<ModalContent size='lg'>
<ModalHeader>
<div className='flex flex-col gap-[4px]'>
<h2 className='font-semibold text-[16px]'>{isAddMode ? 'Add New Row' : 'Edit Row'}</h2>

View File

@@ -1447,7 +1447,7 @@ export function Table({
<ModalContent size='sm'>
<ModalHeader>Delete Table</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{tableData?.name}</span>?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -1482,7 +1482,7 @@ export function Table({
<ModalContent size='sm'>
<ModalHeader>Delete Column</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{deletingColumn}</span>? This
will remove all data in this column.{' '}

View File

@@ -205,10 +205,10 @@ export function Tables() {
/>
<Modal open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Table</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{activeTable?.name}</span>?
This will permanently delete all {activeTable?.rowCount} rows.{' '}

View File

@@ -418,7 +418,7 @@ export function ChatDeploy({
<ModalContent size='sm'>
<ModalHeader>Delete Chat</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{existingChat?.title || 'this chat'}

View File

@@ -210,7 +210,7 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
return (
<>
<Modal open={open} onOpenChange={(openState) => !openState && handleCloseAttempt()}>
<ModalContent className='max-w-[480px]'>
<ModalContent size='md'>
<ModalHeader>
<span>Edit API Info</span>
</ModalHeader>
@@ -301,7 +301,7 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
</Modal>
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
<ModalContent className='max-w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>
<span>Unsaved Changes</span>
</ModalHeader>

View File

@@ -89,13 +89,13 @@ export function VersionDescriptionModal({
return (
<>
<Modal open={open} onOpenChange={(openState) => !openState && handleCloseAttempt()}>
<ModalContent className='max-w-[480px]'>
<ModalContent size='md'>
<ModalHeader>
<span>Version Description</span>
</ModalHeader>
<ModalBody className='space-y-[10px]'>
<div className='flex items-center justify-between'>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
{currentDescription ? 'Edit the' : 'Add a'} description for{' '}
<span className='font-medium text-[var(--text-primary)]'>{versionName}</span>
</p>
@@ -146,7 +146,7 @@ export function VersionDescriptionModal({
</Modal>
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
<ModalContent className='max-w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>
<span>Unsaved Changes</span>
</ModalHeader>

View File

@@ -235,7 +235,7 @@ export function GeneralDeploy({
<ModalContent size='sm'>
<ModalHeader>Load Deployment</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to load{' '}
<span className='font-medium text-[var(--text-primary)]'>
{versionToLoadInfo?.name || `v${versionToLoad}`}
@@ -261,7 +261,7 @@ export function GeneralDeploy({
<ModalContent size='sm'>
<ModalHeader>Promote to live</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to promote{' '}
<span className='font-medium text-[var(--text-primary)]'>
{versionToPromoteInfo?.name || `v${versionToPromote}`}

View File

@@ -365,7 +365,7 @@ export function TemplateDeploy({
<ModalContent size='sm'>
<ModalHeader>Delete Template</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{existingTemplate?.name || formData.name || 'this template'}

View File

@@ -861,7 +861,7 @@ export function DeployModal({
<ModalContent size='sm'>
<ModalHeader>Undeploy API</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to undeploy this workflow?{' '}
<span className='text-[var(--text-error)]'>
This will remove the API endpoint and make it unavailable to external users.
@@ -887,7 +887,7 @@ export function DeployModal({
<ModalContent size='sm'>
<ModalHeader>Delete A2A Agent</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{existingA2aAgent?.name || 'this agent'}

View File

@@ -1177,7 +1177,7 @@ try {
<ModalContent size='sm'>
<ModalHeader>Delete Custom Tool</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
This will permanently delete the tool and remove it from any workflows that are using
it. <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
@@ -1205,7 +1205,7 @@ try {
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
You have unsaved changes to this tool. Are you sure you want to discard your changes
and close the editor?
</p>

View File

@@ -599,7 +599,7 @@ export const Panel = memo(function Panel() {
<ModalContent size='sm'>
<ModalHeader>Delete Workflow</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{currentWorkflow?.name ?? 'this workflow'}

View File

@@ -168,7 +168,7 @@ export function SettingsSidebar({
}
return (
<div className='flex flex-1 flex-col overflow-hidden'>
<>
{/* Back button */}
<div className='mt-[10px] flex flex-shrink-0 flex-col gap-[2px] px-[8px]'>
<Tooltip.Root key={`back-${isCollapsed}`}>
@@ -195,7 +195,12 @@ export function SettingsSidebar({
</div>
{/* Settings sections */}
<div className='mt-[14px] flex flex-1 flex-col gap-[14px] overflow-y-auto overflow-x-hidden'>
<div
className={cn(
'mt-[14px] flex flex-1 flex-col gap-[14px]',
!isCollapsed && 'overflow-y-auto overflow-x-hidden'
)}
>
{sessionLoading || orgsLoading
? Array.from({ length: 3 }, (_, i) => (
<div key={i} className='flex flex-shrink-0 flex-col'>
@@ -273,6 +278,6 @@ export function SettingsSidebar({
)
})}
</div>
</div>
</>
)
}

View File

@@ -172,7 +172,7 @@ export function DeleteModal({
<ModalContent size='sm'>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
{renderDescription()}{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>

View File

@@ -0,0 +1,90 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
Button,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from '@/components/emcn'
interface CreateWorkspaceModalProps {
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: (name: string) => Promise<void>
isCreating: boolean
}
/**
* Modal for naming a new workspace before creation.
*/
export function CreateWorkspaceModal({
open,
onOpenChange,
onConfirm,
isCreating,
}: CreateWorkspaceModalProps) {
const [name, setName] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (open) {
setName('')
requestAnimationFrame(() => inputRef.current?.focus())
}
}, [open])
const handleSubmit = useCallback(async () => {
const trimmed = name.trim()
if (!trimmed || isCreating) return
await onConfirm(trimmed)
}, [name, isCreating, onConfirm])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
void handleSubmit()
}
},
[handleSubmit]
)
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent size='sm'>
<ModalHeader>Create Workspace</ModalHeader>
<ModalBody>
<Input
ref={inputRef}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Workspace name'
maxLength={100}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck={false}
disabled={isCreating}
/>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={() => onOpenChange(false)} disabled={isCreating}>
Cancel
</Button>
<Button
variant='tertiary'
onClick={() => void handleSubmit()}
disabled={!name.trim() || isCreating}
>
{isCreating ? 'Creating...' : 'Create'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}

View File

@@ -608,7 +608,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
<ModalContent size='sm'>
<ModalHeader>Remove Member</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to remove{' '}
<span className='font-medium text-[var(--text-primary)]'>
{memberToRemove?.email}
@@ -641,7 +641,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
<ModalContent size='sm'>
<ModalHeader>Cancel Invitation</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to cancel the invitation for{' '}
<span className='font-medium text-[var(--text-primary)]'>
{invitationToRemove?.email}

View File

@@ -21,6 +21,7 @@ import {
import { cn } from '@/lib/core/utils/cn'
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
import { CreateWorkspaceModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal'
import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal'
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -29,6 +30,7 @@ const logger = createLogger('WorkspaceHeader')
interface Workspace {
id: string
name: string
color?: string
ownerId: string
role?: string
permissions?: 'admin' | 'write' | 'read' | null
@@ -51,8 +53,8 @@ interface WorkspaceHeaderProps {
setIsWorkspaceMenuOpen: (isOpen: boolean) => void
/** Callback when workspace is switched */
onWorkspaceSwitch: (workspace: Workspace) => void
/** Callback when create workspace is clicked */
onCreateWorkspace: () => Promise<void>
/** Callback when create workspace is confirmed with a name */
onCreateWorkspace: (name: string) => Promise<void>
/** Callback to rename the workspace */
onRenameWorkspace: (workspaceId: string, newName: string) => Promise<void>
/** Callback to delete the workspace */
@@ -93,6 +95,7 @@ export function WorkspaceHeader({
onLeaveWorkspace,
sessionUserId,
}: WorkspaceHeaderProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
@@ -324,7 +327,12 @@ export function WorkspaceHeader({
}
}}
>
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-7)] font-medium text-[12px] text-[var(--text-secondary)] leading-none'>
<div
className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] font-medium text-[12px] text-white leading-none'
style={{
backgroundColor: activeWorkspaceFull?.color || 'var(--brand-tertiary-2)',
}}
>
{workspaceInitial}
</div>
<span className='min-w-0 flex-1 truncate text-left font-base text-[14px] text-[var(--text-primary)]'>
@@ -355,7 +363,12 @@ export function WorkspaceHeader({
) : (
<>
<div className='flex items-center gap-[8px] px-[2px] py-[2px]'>
<div className='flex h-[32px] w-[32px] flex-shrink-0 items-center justify-center rounded-[6px] bg-[var(--surface-7)] font-medium text-[12px] text-[var(--text-secondary)]'>
<div
className='flex h-[32px] w-[32px] flex-shrink-0 items-center justify-center rounded-[6px] font-medium text-[12px] text-white'
style={{
backgroundColor: activeWorkspaceFull?.color || 'var(--brand-tertiary-2)',
}}
>
{workspaceInitial}
</div>
<div className='flex min-w-0 flex-col'>
@@ -478,15 +491,15 @@ export function WorkspaceHeader({
<button
type='button'
className='flex w-full cursor-pointer select-none items-center gap-[8px] rounded-[5px] px-[8px] py-[5px] font-medium text-[12px] text-[var(--text-body)] outline-none transition-colors hover:bg-[var(--surface-active)] disabled:pointer-events-none disabled:opacity-50'
onClick={async (e) => {
onClick={(e) => {
e.stopPropagation()
await onCreateWorkspace()
setIsWorkspaceMenuOpen(false)
setIsCreateModalOpen(true)
}}
disabled={isCreatingWorkspace}
>
<Plus className='h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
{isCreatingWorkspace ? 'Creating workspace...' : 'Create new workspace'}
Create new workspace
</button>
</>
)}
@@ -500,7 +513,10 @@ export function WorkspaceHeader({
title={activeWorkspace?.name || 'Loading...'}
disabled
>
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-7)] font-medium text-[12px] text-[var(--text-secondary)] leading-none'>
<div
className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] font-medium text-[12px] text-white leading-none'
style={{ backgroundColor: activeWorkspaceFull?.color || 'var(--brand-tertiary-2)' }}
>
{workspaceInitial}
</div>
<span className='min-w-0 flex-1 truncate text-left font-base text-[14px] text-[var(--text-primary)]'>
@@ -542,6 +558,17 @@ export function WorkspaceHeader({
)
})()}
{/* Create Workspace Modal */}
<CreateWorkspaceModal
open={isCreateModalOpen}
onOpenChange={setIsCreateModalOpen}
onConfirm={async (name) => {
await onCreateWorkspace(name)
setIsCreateModalOpen(false)
}}
isCreating={isCreatingWorkspace}
/>
{/* Invite Modal */}
<InviteModal
open={isInviteModalOpen}
@@ -562,7 +589,7 @@ export function WorkspaceHeader({
<ModalContent size='sm'>
<ModalHeader>Leave Workspace</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to leave{' '}
<span className='font-base text-[var(--text-primary)]'>{leaveTarget?.name}</span>? You
will lose access to all workflows and data in this workspace.{' '}

View File

@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { usePathname, useRouter } from 'next/navigation'
import { generateWorkspaceName } from '@/lib/workspaces/naming'
import { useLeaveWorkspace } from '@/hooks/queries/invitations'
import {
useCreateWorkspace,
@@ -133,25 +132,26 @@ export function useWorkspaceManagement({
[switchToWorkspace]
)
const handleCreateWorkspace = useCallback(async () => {
if (createWorkspaceMutation.isPending) {
logger.info('Workspace creation already in progress, ignoring request')
return
}
const handleCreateWorkspace = useCallback(
async (name: string) => {
if (createWorkspaceMutation.isPending) {
logger.info('Workspace creation already in progress, ignoring request')
return
}
try {
logger.info('Creating new workspace')
const workspaceName = generateWorkspaceName()
logger.info(`Generated workspace name: ${workspaceName}`)
try {
logger.info(`Creating new workspace: ${name}`)
const newWorkspace = await createWorkspaceMutation.mutateAsync({ name: workspaceName })
logger.info('Created new workspace:', newWorkspace)
const newWorkspace = await createWorkspaceMutation.mutateAsync({ name })
logger.info('Created new workspace:', newWorkspace)
await switchWorkspace(newWorkspace)
} catch (error) {
logger.error('Error creating workspace:', error)
}
}, [createWorkspaceMutation, switchWorkspace])
await switchWorkspace(newWorkspace)
} catch (error) {
logger.error('Error creating workspace:', error)
}
},
[createWorkspaceMutation, switchWorkspace]
)
const confirmDeleteWorkspace = useCallback(
async (workspaceToDelete: Workspace, templateAction?: 'keep' | 'delete') => {

View File

@@ -68,10 +68,16 @@ export function useExportWorkspace({ onSuccess }: UseExportWorkspaceProps = {})
})
)
const workspaceResponse = await fetch(`/api/workspaces/${workspaceId}`)
const workspaceColor = workspaceResponse.ok
? ((await workspaceResponse.json()).workspace?.color as string | undefined)
: undefined
const zipBlob = await exportWorkspaceToZip(
workspaceName,
workflowsToExport,
foldersToExport
foldersToExport,
workspaceColor
)
const zipFilename = `${sanitizePathSegment(workspaceName)}-${Date.now()}.zip`

View File

@@ -60,7 +60,11 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {})
const createResponse = await fetch('/api/workspaces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: workspaceName, skipDefaultWorkflow: true }),
body: JSON.stringify({
name: workspaceName,
...(metadata?.workspaceColor && { color: metadata.workspaceColor }),
skipDefaultWorkflow: true,
}),
})
if (!createResponse.ok) {

View File

@@ -90,7 +90,7 @@ const ModalOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-[500] bg-[#E4E4E4]/50 backdrop-blur-[0.75px] dark:bg-[#0D0D0D]/50',
'fixed inset-0 z-[500] bg-black/10 backdrop-blur-[2px]',
ANIMATION_CLASSES,
className
)}
@@ -159,7 +159,7 @@ const ModalContent = React.forwardRef<
className={cn(
ANIMATION_CLASSES,
CONTENT_ANIMATION_CLASSES,
'fixed top-[50%] z-[500] flex max-h-[84vh] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-[8px] border bg-[var(--bg)] shadow-sm duration-200',
'fixed top-[50%] z-[500] flex max-h-[84vh] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-xl bg-[var(--bg)] text-[13px] ring-1 ring-foreground/10 duration-200',
MODAL_SIZES[size],
className
)}
@@ -197,13 +197,10 @@ const ModalHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex min-w-0 items-center justify-between gap-[8px] px-[16px] py-[10px]',
className
)}
className={cn('flex min-w-0 items-center justify-between gap-2 px-4 pt-4', className)}
{...props}
>
<DialogPrimitive.Title className='min-w-0 font-medium text-[16px] text-[var(--text-primary)] leading-snug'>
<DialogPrimitive.Title className='min-w-0 font-medium text-[var(--text-primary)] text-base leading-none'>
{children}
</DialogPrimitive.Title>
<DialogPrimitive.Close asChild>
@@ -299,7 +296,7 @@ const ModalTabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
'relative flex gap-[16px] px-4',
'relative flex gap-[16px] px-4 pt-3',
disabled && 'pointer-events-none opacity-50',
className
)}
@@ -359,14 +356,7 @@ ModalTabsContent.displayName = 'ModalTabsContent'
*/
const ModalBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex-1 overflow-y-auto border-t bg-[var(--surface-2)] px-[14px] py-[10px]',
className
)}
{...props}
/>
<div ref={ref} className={cn('flex-1 overflow-y-auto px-4 pt-3 pb-4', className)} {...props} />
)
)
@@ -380,7 +370,7 @@ const ModalFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
<div
ref={ref}
className={cn(
'flex justify-end gap-[8px] border-t bg-[var(--surface-2)] px-[16px] py-[10px]',
'flex justify-end gap-2 rounded-b-xl border-t bg-muted/50 px-4 py-3',
className
)}
{...props}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* BookOpen icon component - open book
* @param props - SVG properties including className, fill, etc.
*/
export function BookOpen(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M0.75 2.75C0.75 2.75 3.25 0.75 6.25 0.75C9.25 0.75 10.25 2.75 10.25 2.75V18.75C10.25 18.75 9.25 17.25 6.25 17.25C3.25 17.25 0.75 18.75 0.75 18.75V2.75Z' />
<path d='M10.25 2.75C10.25 2.75 11.25 0.75 14.25 0.75C17.25 0.75 19.75 2.75 19.75 2.75V18.75C19.75 18.75 17.25 17.25 14.25 17.25C11.25 17.25 10.25 18.75 10.25 18.75' />
</svg>
)
}

View File

@@ -0,0 +1,32 @@
import type { SVGProps } from 'react'
/**
* Bug icon component - debug beetle
* @param props - SVG properties including className, fill, etc.
*/
export function Bug(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M6.25 4.75L4.25 2.75' />
<path d='M14.25 4.75L16.25 2.75' />
<path d='M6.25 14.75L4.25 16.75' />
<path d='M14.25 14.75L16.25 16.75' />
<path d='M0.75 9.75H4.75' />
<path d='M15.75 9.75H19.75' />
<rect x='4.75' y='4.75' width='11' height='13' rx='5.5' />
<path d='M4.75 9.75H15.75' />
<line x1='10.25' y1='9.75' x2='10.25' y2='17.75' />
</svg>
)
}

View File

@@ -6,8 +6,10 @@ export { ArrowUpDown } from './arrow-up-down'
export { Asterisk } from './asterisk'
export { Bell } from './bell'
export { Blimp } from './blimp'
export { BookOpen } from './book-open'
export { BubbleChatClose } from './bubble-chat-close'
export { BubbleChatPreview } from './bubble-chat-preview'
export { Bug } from './bug'
export { Calendar } from './calendar'
export { Card } from './card'
export { ChevronDown } from './chevron-down'
@@ -30,10 +32,13 @@ export { HelpCircle } from './help-circle'
export { HexSimple } from './hex-simple'
export { Home } from './home'
export { Key } from './key'
export { KeySquare } from './key-square'
export { Layout } from './layout'
export { Library } from './library'
export { ListFilter } from './list-filter'
export { Loader } from './loader'
export { LogIn } from './log-in'
export { Mail } from './mail'
export { MoreHorizontal } from './more-horizontal'
export { NoWrap } from './no-wrap'
export { PanelLeft } from './panel-left'
@@ -44,7 +49,9 @@ export { Redo } from './redo'
export { Rocket } from './rocket'
export { Rows3 } from './rows3'
export { Search } from './search'
export { Server } from './server'
export { Settings } from './settings'
export { ShieldCheck } from './shield-check'
export { Table } from './table'
export { TerminalWindow } from './terminal-window'
export { Trash } from './trash'
@@ -55,7 +62,10 @@ export { TypeNumber } from './type-number'
export { TypeText } from './type-text'
export { Undo } from './undo'
export { Upload } from './upload'
export { User } from './user'
export { UserPlus } from './user-plus'
export { Users } from './users'
export { Wrap } from './wrap'
export { Wrench } from './wrench'
export { ZoomIn } from './zoom-in'
export { ZoomOut } from './zoom-out'

View File

@@ -0,0 +1,28 @@
import type { SVGProps } from 'react'
/**
* KeySquare icon component - key inside a rounded square
* @param props - SVG properties including className, fill, etc.
*/
export function KeySquare(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<rect x='1.25' y='0.75' width='18' height='18' rx='2.5' />
<circle cx='8.75' cy='9.25' r='2.5' />
<path d='M10.75 7.75L14.25 7.75' />
<path d='M14.25 7.75V10.25' />
<path d='M12.25 7.75V9.75' />
</svg>
)
}

View File

@@ -0,0 +1,26 @@
import type { SVGProps } from 'react'
/**
* LogIn icon component - arrow entering a door
* @param props - SVG properties including className, fill, etc.
*/
export function LogIn(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M11.75 0.75H16.25C17.3546 0.75 18.25 1.64543 18.25 2.75V16.75C18.25 17.8546 17.3546 18.75 16.25 18.75H11.75' />
<path d='M8.25 13.75L12.25 9.75L8.25 5.75' />
<path d='M12.25 9.75H1.25' />
</svg>
)
}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* Mail icon component - envelope
* @param props - SVG properties including className, fill, etc.
*/
export function Mail(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<rect x='0.75' y='2.75' width='19' height='14' rx='2' />
<path d='M0.75 5.75L10.25 11.75L19.75 5.75' />
</svg>
)
}

View File

@@ -0,0 +1,27 @@
import type { SVGProps } from 'react'
/**
* Server icon component - stacked server boxes
* @param props - SVG properties including className, fill, etc.
*/
export function Server(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<rect x='1.75' y='0.75' width='17' height='7.5' rx='1.5' />
<rect x='1.75' y='11.25' width='17' height='7.5' rx='1.5' />
<circle cx='5.75' cy='4.5' r='0.75' fill='currentColor' />
<circle cx='5.75' cy='15' r='0.75' fill='currentColor' />
</svg>
)
}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* ShieldCheck icon component - shield with checkmark
* @param props - SVG properties including className, fill, etc.
*/
export function ShieldCheck(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M10.25 1.25L2.25 5.25V10.25C2.25 15.25 5.65 19.85 10.25 20.75C14.85 19.85 18.25 15.25 18.25 10.25V5.25L10.25 1.25Z' />
<path d='M7.25 10.75L9.25 12.75L13.25 8.75' />
</svg>
)
}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* User icon component - single person silhouette
* @param props - SVG properties including className, fill, etc.
*/
export function User(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<circle cx='10.25' cy='6.25' r='4' />
<path d='M2.25 18.75C2.25 14.3317 5.83172 10.75 10.25 10.75C14.6683 10.75 18.25 14.3317 18.25 18.75' />
</svg>
)
}

View File

@@ -0,0 +1,27 @@
import type { SVGProps } from 'react'
/**
* Users icon component - two person silhouettes
* @param props - SVG properties including className, fill, etc.
*/
export function Users(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<circle cx='8.25' cy='5.75' r='3.5' />
<path d='M0.75 18.75C0.75 14.6079 4.10786 11.25 8.25 11.25C12.3921 11.25 15.75 14.6079 15.75 18.75' />
<path d='M14.25 0.93C15.11 0.63 16.04 0.73 16.83 1.19C17.62 1.65 18.17 2.42 18.33 3.32C18.49 4.21 18.25 5.13 17.68 5.83C17.11 6.54 16.27 6.95 15.37 6.97' />
<path d='M16.75 11.43C18.08 11.67 19.29 12.35 20.17 13.37C21.05 14.39 21.53 15.69 21.53 17.03V18.75' />
</svg>
)
}

View File

@@ -0,0 +1,24 @@
import type { SVGProps } from 'react'
/**
* Wrench icon component - adjustable wrench tool
* @param props - SVG properties including className, fill, etc.
*/
export function Wrench(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M15.09 1.41C13.78 0.74 12.25 0.63 10.85 1.11C9.45 1.59 8.3 2.62 7.68 3.96C7.06 5.3 7.02 6.84 7.57 8.2L1.25 14.52C0.86 14.91 0.86 15.54 1.25 15.93L3.57 18.25C3.96 18.64 4.59 18.64 4.98 18.25L11.3 11.93C12.66 12.48 14.2 12.44 15.54 11.82C16.88 11.2 17.91 10.05 18.39 8.65C18.87 7.25 18.76 5.72 18.09 4.41L14.87 7.63L12.25 7.25L11.87 4.63L15.09 1.41Z' />
</svg>
)
}

View File

@@ -119,7 +119,7 @@ function AddMembersModal({
onOpenChange(o)
}}
>
<ModalContent className='w-[420px]'>
<ModalContent size='sm'>
<ModalHeader>Add Members</ModalHeader>
<ModalBody className='!pb-[16px]'>
{availableMembers.length === 0 ? (
@@ -1094,7 +1094,7 @@ export function AccessControl() {
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
You have unsaved changes. Do you want to save them before closing?
</p>
</ModalBody>
@@ -1262,7 +1262,7 @@ export function AccessControl() {
<ModalContent size='sm'>
<ModalHeader>Delete Permission Group</ModalHeader>
<ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'>
<p className='text-[var(--text-secondary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{deletingGroup?.name}</span>?
All members will be removed from this group.{' '}

View File

@@ -21,6 +21,7 @@ export const workspaceKeys = {
export interface Workspace {
id: string
name: string
color?: string
ownerId: string
role?: string
membershipId?: string

View File

@@ -32,6 +32,7 @@ export interface FolderExportData {
export interface WorkspaceExportStructure {
workspace: {
name: string
color?: string
exportedAt: string
}
workflows: WorkflowExportData[]
@@ -178,7 +179,8 @@ function buildFolderPath(
export async function exportWorkspaceToZip(
workspaceName: string,
workflows: WorkflowExportData[],
folders: FolderExportData[]
folders: FolderExportData[],
workspaceColor?: string
): Promise<Blob> {
const zip = new JSZip()
const foldersMap = new Map(folders.map((f) => [f.id, f]))
@@ -186,6 +188,7 @@ export async function exportWorkspaceToZip(
const metadata = {
workspace: {
name: workspaceName,
...(workspaceColor && { color: workspaceColor }),
exportedAt: new Date().toISOString(),
},
folders: folders.map((f) => ({
@@ -292,6 +295,7 @@ export interface ImportedWorkflow {
export interface WorkspaceImportMetadata {
workspaceName: string
workspaceColor?: string
exportedAt?: string
folders?: Array<{
id: string
@@ -326,6 +330,7 @@ export async function extractWorkflowsFromZip(
const parsed = JSON.parse(content)
metadata = {
workspaceName: parsed.workspace?.name || 'Imported Workspace',
workspaceColor: parsed.workspace?.color,
exportedAt: parsed.workspace?.exportedAt,
folders: parsed.folders,
}

View File

@@ -1,3 +1,20 @@
/** Color palette for workspace accents, aligned with the workflow color family. */
export const WORKSPACE_COLORS = [
'#2ABBF8', // Blue
'#22c55e', // Green
'#FFCC02', // Yellow
'#a855f7', // Purple
'#4aea7f', // Mint
'#f97316', // Orange
'#14b8a6', // Teal
'#ff6b6b', // Coral
] as const
/** Picks a random workspace color from the hero palette. */
export function getRandomWorkspaceColor(): string {
return WORKSPACE_COLORS[Math.floor(Math.random() * WORKSPACE_COLORS.length)]
}
const APP_COLORS = [
{ from: '#4F46E5', to: '#7C3AED' }, // indigo to purple
{ from: '#7C3AED', to: '#C026D3' }, // purple to fuchsia

View File

@@ -59,6 +59,7 @@ export async function duplicateWorkspace(
await tx.insert(workspaceTable).values({
id: newWorkspaceId,
name,
color: sourceWorkspace.color,
ownerId: userId,
billedAccountUserId: userId,
allowPersonalApiKeys: sourceWorkspace.allowPersonalApiKeys,

View File

@@ -1030,6 +1030,7 @@ export const invitation = pgTable(
export const workspace = pgTable('workspace', {
id: text('id').primaryKey(),
name: text('name').notNull(),
color: text('color').notNull().default('#32bd7e'),
ownerId: text('owner_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),