mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement: modals
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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{' '}
|
||||
|
||||
@@ -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)]'>
|
||||
|
||||
@@ -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.{' '}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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{' '}
|
||||
|
||||
@@ -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.'
|
||||
) : (
|
||||
|
||||
@@ -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]'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.{' '}
|
||||
|
||||
@@ -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.{' '}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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.{' '}
|
||||
|
||||
@@ -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') => {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
25
apps/sim/components/emcn/icons/book-open.tsx
Normal file
25
apps/sim/components/emcn/icons/book-open.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
apps/sim/components/emcn/icons/bug.tsx
Normal file
32
apps/sim/components/emcn/icons/bug.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
28
apps/sim/components/emcn/icons/key-square.tsx
Normal file
28
apps/sim/components/emcn/icons/key-square.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
26
apps/sim/components/emcn/icons/log-in.tsx
Normal file
26
apps/sim/components/emcn/icons/log-in.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
apps/sim/components/emcn/icons/mail.tsx
Normal file
25
apps/sim/components/emcn/icons/mail.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
27
apps/sim/components/emcn/icons/server.tsx
Normal file
27
apps/sim/components/emcn/icons/server.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
apps/sim/components/emcn/icons/shield-check.tsx
Normal file
25
apps/sim/components/emcn/icons/shield-check.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
apps/sim/components/emcn/icons/user.tsx
Normal file
25
apps/sim/components/emcn/icons/user.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
27
apps/sim/components/emcn/icons/users.tsx
Normal file
27
apps/sim/components/emcn/icons/users.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
24
apps/sim/components/emcn/icons/wrench.tsx
Normal file
24
apps/sim/components/emcn/icons/wrench.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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.{' '}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const workspaceKeys = {
|
||||
export interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
color?: string
|
||||
ownerId: string
|
||||
role?: string
|
||||
membershipId?: string
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
Reference in New Issue
Block a user