Skip to content

Commit dae906a

Browse files
committed
Merge the shell lane
2 parents b5890cf + 49d0e07 commit dae906a

44 files changed

Lines changed: 2208 additions & 464 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx‎

Lines changed: 17 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,28 @@ import {
1212
import { X } from '@sim/emcn/icons'
1313
import { createLogger } from '@sim/logger'
1414
import { useMutation } from '@tanstack/react-query'
15-
import imageCompression from 'browser-image-compression'
1615
import Image from 'next/image'
1716
import { Controller, useForm } from 'react-hook-form'
1817
import { z } from 'zod'
18+
import { HELP_REQUEST_TYPES } from '@/lib/api/contracts/common'
19+
import {
20+
ACCEPTED_HELP_IMAGE_TYPES,
21+
compressHelpImage,
22+
DEFAULT_HELP_REQUEST_TYPE,
23+
HELP_REQUEST_TYPE_OPTIONS,
24+
submitHelpRequest,
25+
validateHelpImage,
26+
} from '@/lib/help/request'
1927

2028
const logger = createLogger('HelpModal')
2129

22-
const MAX_FILE_SIZE = 20 * 1024 * 1024
23-
const TARGET_SIZE_MB = 2
24-
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']
25-
2630
const SCROLL_DELAY_MS = 100
2731
const SUCCESS_RESET_DELAY_MS = 2000
2832

29-
const DEFAULT_REQUEST_TYPE = 'bug'
30-
31-
const REQUEST_TYPE_OPTIONS = [
32-
{ label: 'Bug report', value: 'bug' },
33-
{ label: 'Feedback', value: 'feedback' },
34-
{ label: 'Feature request', value: 'feature_request' },
35-
{ label: 'Other', value: 'other' },
36-
]
37-
3833
const formSchema = z.object({
3934
subject: z.string().min(1, 'Subject is required'),
4035
message: z.string().min(1, 'Message is required'),
41-
type: z.enum(['bug', 'feedback', 'feature_request', 'other'], {
36+
type: z.enum(HELP_REQUEST_TYPES, {
4237
error: 'Please select a request type',
4338
}),
4439
})
@@ -63,58 +58,6 @@ interface SubmitHelpVariables {
6358
workspaceId?: string
6459
}
6560

66-
async function compressImage(file: File): Promise<File> {
67-
if (file.size < TARGET_SIZE_MB * 1024 * 1024 || file.type === 'image/gif') {
68-
return file
69-
}
70-
71-
try {
72-
const compressedFile = await imageCompression(file, {
73-
maxSizeMB: TARGET_SIZE_MB,
74-
maxWidthOrHeight: 1920,
75-
useWebWorker: true,
76-
fileType: file.type,
77-
initialQuality: 0.8,
78-
alwaysKeepResolution: true,
79-
})
80-
81-
return new File([compressedFile], file.name, {
82-
type: file.type,
83-
lastModified: Date.now(),
84-
})
85-
} catch (error) {
86-
logger.warn('Image compression failed, using original file:', { error })
87-
return file
88-
}
89-
}
90-
91-
async function submitHelpRequest({ data, images, workflowId, workspaceId }: SubmitHelpVariables) {
92-
const formData = new FormData()
93-
formData.append('subject', data.subject)
94-
formData.append('message', data.message)
95-
formData.append('type', data.type)
96-
if (workspaceId) formData.append('workspaceId', workspaceId)
97-
formData.append('userAgent', navigator.userAgent)
98-
if (workflowId) {
99-
formData.append('workflowId', workflowId)
100-
}
101-
102-
images.forEach((image, index) => {
103-
formData.append(`image_${index}`, image)
104-
})
105-
106-
// boundary-raw-fetch: multipart/form-data submission with image attachments, requestJson only supports JSON bodies
107-
const response = await fetch('/api/help', {
108-
method: 'POST',
109-
body: formData,
110-
})
111-
112-
if (!response.ok) {
113-
const errorData = await response.json().catch(() => null)
114-
throw new Error(errorData?.error || 'Failed to submit help request')
115-
}
116-
}
117-
11861
export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpModalProps) {
11962
const scrollContainerRef = useRef<HTMLDivElement>(null)
12063
const imagesRef = useRef<ImageWithPreview[]>([])
@@ -128,13 +71,13 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
12871
defaultValues: {
12972
subject: '',
13073
message: '',
131-
type: DEFAULT_REQUEST_TYPE,
74+
type: DEFAULT_HELP_REQUEST_TYPE,
13275
},
13376
mode: 'onSubmit',
13477
})
13578

13679
const helpMutation = useMutation({
137-
mutationFn: submitHelpRequest,
80+
mutationFn: (variables: SubmitHelpVariables) => submitHelpRequest(variables),
13881
onSuccess: (_data, variables) => {
13982
setSubmitStatus('success')
14083
reset()
@@ -159,7 +102,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
159102
reset({
160103
subject: '',
161104
message: '',
162-
type: DEFAULT_REQUEST_TYPE,
105+
type: DEFAULT_HELP_REQUEST_TYPE,
163106
})
164107
} else {
165108
const previewsToRevoke = imagesRef.current
@@ -208,17 +151,12 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
208151
let hasError = false
209152

210153
for (const file of Array.from(files)) {
211-
if (file.size > MAX_FILE_SIZE) {
212-
hasError = true
213-
continue
214-
}
215-
216-
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
154+
if (validateHelpImage(file)) {
217155
hasError = true
218156
continue
219157
}
220158

221-
const compressedFile = await compressImage(file)
159+
const compressedFile = await compressHelpImage(file)
222160
const imageWithPreview = Object.assign(compressedFile, {
223161
preview: URL.createObjectURL(compressedFile),
224162
}) as ImageWithPreview
@@ -265,7 +203,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
265203
title='Request'
266204
value={field.value}
267205
onChange={field.onChange}
268-
options={REQUEST_TYPE_OPTIONS}
206+
options={HELP_REQUEST_TYPE_OPTIONS}
269207
placeholder='Select a request type'
270208
error={fieldState.error?.message}
271209
/>
@@ -305,7 +243,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
305243
title='Attach images (optional)'
306244
label='Drop images here or click to browse'
307245
description='PNG, JPEG, WebP, GIF (max 20MB each)'
308-
accept={ACCEPTED_IMAGE_TYPES.join(',')}
246+
accept={ACCEPTED_HELP_IMAGE_TYPES.join(',')}
309247
multiple
310248
onChange={processFiles}
311249
/>

‎apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx‎

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import type { ComponentType } from 'react'
44
import { ANONYMOUS_USER_ID } from '@sim/auth/principal'
5-
import type { DesktopUpdateState } from '@sim/desktop-bridge'
65
import {
76
Chip,
87
chipContentLabelClass,
@@ -32,22 +31,13 @@ import {
3231
SIDEBAR_RAIL_CHIP_CLASS,
3332
} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
3433
import { useUserProfile } from '@/hooks/queries/user-profile'
35-
import { useDesktopUpdateState } from '@/hooks/use-desktop-update-state'
34+
import {
35+
desktopUpdateActionLabel,
36+
hasAvailableDesktopUpdate,
37+
useDesktopUpdateState,
38+
} from '@/hooks/use-desktop-update-state'
3639
import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
3740

38-
function hasAvailableDesktopUpdate(state: DesktopUpdateState): boolean {
39-
return state.status === 'available' || state.status === 'downloading' || state.status === 'ready'
40-
}
41-
42-
function desktopUpdateActionLabel(state: DesktopUpdateState): string {
43-
if (state.status === 'downloading') {
44-
return state.percent === undefined
45-
? 'Downloading update…'
46-
: `Downloading update ${state.percent}%`
47-
}
48-
return state.status === 'ready' ? 'Restart to update' : 'Update'
49-
}
50-
5141
/** Compact primary update circle using the same footprint as the surrounding sidebar icons. */
5242
function DesktopUpdateIcon({ className }: { className?: string }) {
5343
return (

‎apps/sim/hooks/use-desktop-update-state.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ import { useEffect, useState } from 'react'
44
import type { DesktopUpdateState } from '@sim/desktop-bridge'
55
import { getDesktopUpdates } from '@/lib/desktop'
66

7+
export function hasAvailableDesktopUpdate(state: DesktopUpdateState): boolean {
8+
return state.status === 'available' || state.status === 'downloading' || state.status === 'ready'
9+
}
10+
11+
export function desktopUpdateActionLabel(state: DesktopUpdateState): string {
12+
if (state.status === 'downloading') {
13+
return state.percent === undefined
14+
? 'Downloading update…'
15+
: `Downloading update ${state.percent}%`
16+
}
17+
return state.status === 'ready' ? 'Restart to update' : 'Update'
18+
}
19+
720
const INITIAL_UPDATE_STATE: DesktopUpdateState = { status: 'idle' }
821

922
export function useDesktopUpdateState(): DesktopUpdateState {

‎apps/sim/lib/api/contracts/common.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { defineRouteContract } from '@/lib/api/contracts/types'
44

55
const NO_EMAIL_HEADER_CONTROL_CHARS_REGEX = /^[^\r\n\u0000-\u001F\u007F]+$/
66

7+
export const HELP_REQUEST_TYPES = ['bug', 'feedback', 'feature_request', 'other'] as const
8+
79
export const helpFormBodySchema = z.object({
810
subject: z
911
.string()
1012
.trim()
1113
.min(1, 'Subject is required')
1214
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
1315
message: z.string().min(1, 'Message is required'),
14-
type: z.enum(['bug', 'feedback', 'feature_request', 'other']),
16+
type: z.enum(HELP_REQUEST_TYPES),
1517
})
1618
export type HelpFormBody = z.input<typeof helpFormBodySchema>
1719

‎apps/sim/lib/help/request.ts‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createLogger } from '@sim/logger'
2+
import type { HelpFormBody } from '@/lib/api/contracts/common'
3+
4+
const logger = createLogger('HelpRequest')
5+
6+
const MAX_HELP_IMAGE_BYTES = 20 * 1024 * 1024
7+
const TARGET_SIZE_MB = 2
8+
export const ACCEPTED_HELP_IMAGE_TYPES = [
9+
'image/jpeg',
10+
'image/jpg',
11+
'image/png',
12+
'image/webp',
13+
'image/gif',
14+
]
15+
16+
export const HELP_REQUEST_TYPE_OPTIONS: { label: string; value: HelpFormBody['type'] }[] = [
17+
{ label: 'Bug report', value: 'bug' },
18+
{ label: 'Feedback', value: 'feedback' },
19+
{ label: 'Feature request', value: 'feature_request' },
20+
{ label: 'Other', value: 'other' },
21+
]
22+
23+
export const DEFAULT_HELP_REQUEST_TYPE = 'bug'
24+
25+
/** Why `file` cannot be attached to a help request, or null when it can. */
26+
export function validateHelpImage(file: Pick<File, 'size' | 'type'>): string | null {
27+
if (file.size > MAX_HELP_IMAGE_BYTES) return 'Images must be 20MB or smaller.'
28+
if (!ACCEPTED_HELP_IMAGE_TYPES.includes(file.type))
29+
return 'Attach a JPEG, PNG, WebP or GIF image.'
30+
return null
31+
}
32+
33+
export async function compressHelpImage(file: File): Promise<File> {
34+
if (file.size < TARGET_SIZE_MB * 1024 * 1024 || file.type === 'image/gif') {
35+
return file
36+
}
37+
38+
try {
39+
const { default: imageCompression } = await import('browser-image-compression')
40+
const compressedFile = await imageCompression(file, {
41+
maxSizeMB: TARGET_SIZE_MB,
42+
maxWidthOrHeight: 1920,
43+
useWebWorker: true,
44+
fileType: file.type,
45+
initialQuality: 0.8,
46+
alwaysKeepResolution: true,
47+
})
48+
49+
return new File([compressedFile], file.name, {
50+
type: file.type,
51+
lastModified: Date.now(),
52+
})
53+
} catch (error) {
54+
logger.warn('Image compression failed, using original file:', { error })
55+
return file
56+
}
57+
}
58+
59+
interface SubmitHelpRequestInput {
60+
data: HelpFormBody
61+
images: readonly File[]
62+
workflowId?: string
63+
workspaceId?: string
64+
}
65+
66+
export async function submitHelpRequest({
67+
data,
68+
images,
69+
workflowId,
70+
workspaceId,
71+
}: SubmitHelpRequestInput): Promise<void> {
72+
const formData = new FormData()
73+
formData.append('subject', data.subject)
74+
formData.append('message', data.message)
75+
formData.append('type', data.type)
76+
if (workspaceId) formData.append('workspaceId', workspaceId)
77+
formData.append('userAgent', navigator.userAgent)
78+
if (workflowId) {
79+
formData.append('workflowId', workflowId)
80+
}
81+
82+
images.forEach((image, index) => {
83+
formData.append(`image_${index}`, image)
84+
})
85+
86+
// boundary-raw-fetch: multipart/form-data submission with image attachments, requestJson only supports JSON bodies
87+
const response = await fetch('/api/help', {
88+
method: 'POST',
89+
body: formData,
90+
})
91+
92+
if (!response.ok) {
93+
const errorData = await response.json().catch(() => null)
94+
throw new Error(errorData?.error || 'Failed to submit help request')
95+
}
96+
}

‎apps/sim/platform/app/(shell)/_components/command-menu/command-menu.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { PLATFORM_DESTINATIONS } from '@/platform/app/(shell)/_components/destinations'
22
import type {
3+
ShellChat,
34
ShellData,
45
ShellDestination,
5-
ShellItem,
66
} from '@/platform/app/(shell)/_components/model'
77
import { CommandPalette } from '@/platform/design-system/components/command-palette/command-palette'
88
import { OverlayDialog } from '@/platform/design-system/components/modal/overlay-dialog'
@@ -11,7 +11,7 @@ interface CommandMenuProps {
1111
open: boolean
1212
onClose: () => void
1313
navigate: (href: string) => void
14-
chats: readonly ShellItem[]
14+
chats: readonly ShellChat[]
1515
workspaces: ShellData['workspaces']
1616
destinations?: readonly ShellDestination[]
1717
}
@@ -36,15 +36,15 @@ export function CommandMenu({
3636
items: workspaces.map((item) => ({
3737
id: item.id,
3838
label: item.name,
39-
value: '/home',
39+
value: item.href ?? '/home',
4040
})),
4141
},
4242
{
4343
label: 'Chats',
4444
items: chats.map((item) => ({
4545
id: item.id,
4646
label: item.title,
47-
value: `/chat/${item.id}`,
47+
value: item.href ?? `/chat/${item.id}`,
4848
})),
4949
},
5050
]}

0 commit comments

Comments
 (0)