Skip to content

Commit b448f77

Browse files
committed
invite, billing, home
1 parent 2df6d39 commit b448f77

41 files changed

Lines changed: 948 additions & 1302 deletions

File tree

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/api/organizations/[id]/members/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export const POST = withRouteHandler(
243243
if (!seatValidation.canInvite) {
244244
return NextResponse.json(
245245
{
246-
error: `Cannot invite member. Using ${seatValidation.currentSeats} of ${seatValidation.maxSeats} seats.`,
246+
error: `Cannot invite teammate. Using ${seatValidation.currentSeats} of ${seatValidation.maxSeats} seats.`,
247247
details: seatValidation,
248248
},
249249
{ status: 400 }

apps/sim/app/playground/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,6 @@ export default function PlaygroundPage() {
402402
onRemove={() => {}}
403403
placeholder='Add tags'
404404
placeholderWithTags='Add another'
405-
tagVariant='secondary'
406405
triggerKeys={['Enter', ',']}
407406
/>
408407
</div>

apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getDocumentIcon } from '@/components/icons/document-icons'
1111
import { cn } from '@/lib/core/utils/cn'
1212
import { workflowBorderColor } from '@/lib/workspaces/colors'
1313
import type { ChatContextKind, ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
14+
import { getBareIconStyle } from '@/blocks/icon-color'
1415
import { registry as blockRegistry } from '@/blocks/registry'
1516

1617
interface RenderIconArgs {
@@ -52,7 +53,7 @@ function renderIntegrationTile({ context, className }: RenderIconArgs): ReactNod
5253
const block = blockRegistry[context.blockType]
5354
if (!block) return null
5455
const Icon = block.icon
55-
return <Icon className={className} />
56+
return <Icon className={className} style={getBareIconStyle(Icon)} />
5657
}
5758

5859
/**

apps/sim/app/workspace/[workspaceId]/home/components/credits-chip/credits-chip.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Credit } from '@/components/emcn/icons'
88
import { ON_DEMAND_UNLIMITED } from '@/lib/billing/constants'
99
import { formatCredits } from '@/lib/billing/credits/conversion'
1010
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
11+
import { usePlanView } from '@/hooks/queries/plan-view'
1112
import { prefetchUpgradeBillingData, useSubscriptionData } from '@/hooks/queries/subscription'
1213
import { prefetchWorkspaceSettings } from '@/hooks/queries/workspace'
1314

@@ -18,28 +19,40 @@ export function CreditsChip() {
1819
}
1920

2021
function CreditsChipInner() {
21-
const { data, isLoading } = useSubscriptionData()
22+
const { planView, isLoading, hasData } = usePlanView()
23+
/**
24+
* `usePlanView` is built on top of `useSubscriptionData`, so the second call
25+
* dedups against the same React Query cache entry. We read the raw usage
26+
* fields here because `planView` intentionally only exposes plan-derived
27+
* decisions, not display math.
28+
*/
29+
const { data } = useSubscriptionData()
2230
const router = useRouter()
2331
const queryClient = useQueryClient()
2432
const { workspaceId } = useParams<{ workspaceId: string }>()
2533

2634
const upgradeHref = `/workspace/${workspaceId}/upgrade`
2735

28-
// Warm the route bundle and the exact queries the Upgrade page gates on, so
29-
// the click navigates into already-cached data instead of a blank, loading page.
36+
/**
37+
* Warm the route bundle and the exact queries the Upgrade page gates on, so
38+
* the click navigates into already-cached data instead of a blank, loading page.
39+
*/
3040
const prefetchUpgrade = useCallback(() => {
3141
router.prefetch(upgradeHref)
3242
prefetchUpgradeBillingData(queryClient)
3343
prefetchWorkspaceSettings(queryClient, workspaceId)
3444
}, [router, queryClient, upgradeHref, workspaceId])
3545

36-
if (isLoading || !data?.data) return null
46+
if (isLoading || !hasData || !data?.data) return null
47+
if (!planView.showCredits) return null
3748

3849
const { usageLimit, currentUsage, creditBalance } = data.data
3950

40-
// Credits remaining = unused plan allowance plus any purchased credit balance.
41-
// Uncapped plans (limit at/above the on-demand threshold) render as ∞ via
42-
// `formatCredits`, so short-circuit instead of subtracting usage from it.
51+
/**
52+
* Credits remaining = unused plan allowance plus any purchased credit balance.
53+
* Uncapped plans (limit at/above the on-demand threshold) render as ∞ via
54+
* `formatCredits`, so short-circuit instead of subtracting usage from it.
55+
*/
4356
const remainingCredits =
4457
usageLimit >= ON_DEMAND_UNLIMITED
4558
? ON_DEMAND_UNLIMITED

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
MothershipResource,
2222
MothershipResourceType,
2323
} from '@/app/workspace/[workspaceId]/home/types'
24+
import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
2425
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
2526
import { logKeys } from '@/hooks/queries/logs'
2627
import { tableKeys } from '@/hooks/queries/tables'
@@ -103,16 +104,20 @@ function IconDropdownItem({ item, icon: Icon }: DropdownItemRenderProps & { icon
103104
}
104105

105106
/**
106-
* Renders an integration mention candidate using the block's own brand icon
107-
* (the SVG carries its own brand colors and renders at the standard 14px
108-
* icon size used elsewhere in the dropdown).
107+
* Renders an integration mention candidate using the block's own brand icon at
108+
* the standard 14px dropdown size. Single-fill icons drawn with
109+
* `fill='currentColor'` (e.g. HubSpot) are tinted with the block's brand
110+
* {@link BlockConfig.iconColor}; multi-color brand icons keep their own SVG fills.
109111
*/
110112
function IntegrationDropdownItem({ item }: DropdownItemRenderProps) {
111-
const Icon = item.iconComponent as ElementType | undefined
113+
const Icon = item.iconComponent as StyleableIcon | undefined
112114
if (!Icon) return <span className='truncate'>{item.name}</span>
113115
return (
114116
<>
115-
<Icon className='size-[14px] flex-shrink-0' />
117+
<Icon
118+
className='size-[14px] flex-shrink-0 text-[var(--text-icon)]'
119+
style={getBareIconStyle(Icon)}
120+
/>
116121
<span className='truncate'>{item.name}</span>
117122
</>
118123
)

apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
'use client'
22

3-
import { type ComponentType, useEffect, useMemo, useState } from 'react'
3+
import {
4+
type ComponentType,
5+
type CSSProperties,
6+
useCallback,
7+
useEffect,
8+
useMemo,
9+
useState,
10+
} from 'react'
411
import { useParams } from 'next/navigation'
5-
import { ArrowRight, ChevronDown, Expandable, ExpandableContent } from '@/components/emcn'
6-
import { Table } from '@/components/emcn/icons'
12+
import {
13+
ArrowRight,
14+
ChevronDown,
15+
chipVariants,
16+
Expandable,
17+
ExpandableContent,
18+
} from '@/components/emcn'
19+
import { Shuffle, Table } from '@/components/emcn/icons'
720
import {
821
GithubIcon,
922
GmailIcon,
@@ -22,10 +35,11 @@ import {
2235
resolveOAuthServiceForSlug,
2336
} from '@/lib/integrations'
2437
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
38+
import { getBareIconStyle } from '@/blocks/icon-color'
2539
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
2640
import { useOAuthConnections } from '@/hooks/queries/oauth/oauth-connections'
2741

28-
type Icon = ComponentType<{ className?: string }>
42+
type Icon = ComponentType<{ className?: string; style?: CSSProperties }>
2943

3044
type Action =
3145
| { kind: 'prompt'; id: string; label: string; prompt: string; icon: Icon }
@@ -195,6 +209,41 @@ function toIntegrationAction(service: ServiceInfo, slug: string): Action {
195209
}
196210
}
197211

212+
/**
213+
* Builds a fresh randomized set of suggested actions. Because it samples via
214+
* {@link sample}, each call yields a new ordering — this powers both the initial
215+
* personalization effect and the shuffle control. Users with connected services
216+
* get integration suggestions for services they have not yet connected; everyone
217+
* else falls back to sampling the table and integration prompt pools so the set
218+
* still changes on shuffle.
219+
*/
220+
function computeActions(
221+
services: readonly ServiceInfo[],
222+
connectedProviders: ReadonlySet<string>
223+
): Action[] {
224+
const candidates = services.flatMap((s) => {
225+
if (connectedProviders.has(s.providerId)) return []
226+
const slug = SLUG_BY_LOWER_NAME.get(s.name.toLowerCase())
227+
return slug ? [{ service: s, slug }] : []
228+
})
229+
const integrations = sample(candidates, 2).map(({ service, slug }) =>
230+
toIntegrationAction(service, slug)
231+
)
232+
233+
const integrationPool = INTEGRATION_PROMPTS.filter(
234+
(p) => !p.providerId || !connectedProviders.has(p.providerId)
235+
)
236+
const promptCount = 4 - integrations.length
237+
const [tablePick] = sample(TABLE_PROMPTS, 1)
238+
const integrationPicks = sample(
239+
integrationPool.length > 0 ? integrationPool : INTEGRATION_PROMPTS,
240+
promptCount - 1
241+
)
242+
const prompts = sample([tablePick, ...integrationPicks].map(toPromptAction), promptCount)
243+
244+
return [...integrations, ...prompts]
245+
}
246+
198247
/**
199248
* Initial actions rendered on first paint, before OAuth/credentials queries resolve.
200249
* For users with no connections this is also the final result, so the section never
@@ -259,28 +308,13 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) {
259308

260309
useEffect(() => {
261310
if (services.length === 0 || connectedProviders.size === 0) return
262-
263-
const candidates = services.flatMap((s) => {
264-
if (connectedProviders.has(s.providerId)) return []
265-
const slug = SLUG_BY_LOWER_NAME.get(s.name.toLowerCase())
266-
return slug ? [{ service: s, slug }] : []
267-
})
268-
const integrations = sample(candidates, 2).map(({ service, slug }) =>
269-
toIntegrationAction(service, slug)
270-
)
271-
272-
const integrationPool = INTEGRATION_PROMPTS.filter(
273-
(p) => !p.providerId || !connectedProviders.has(p.providerId)
274-
)
275-
const [tablePick] = sample(TABLE_PROMPTS, 1)
276-
const [integrationPick] = sample(
277-
integrationPool.length > 0 ? integrationPool : INTEGRATION_PROMPTS,
278-
1
279-
)
280-
281-
setActions([...integrations, toPromptAction(tablePick), toPromptAction(integrationPick)])
311+
setActions(computeActions(services, connectedProviders))
282312
}, [connectedProviders, services])
283313

314+
const handleShuffle = useCallback(() => {
315+
setActions(computeActions(services, connectedProviders))
316+
}, [services, connectedProviders])
317+
284318
const handleSelect = (action: Action) => {
285319
if (action.kind === 'prompt') {
286320
onSelectPrompt(action.prompt)
@@ -292,23 +326,40 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) {
292326

293327
return (
294328
<div className='mx-auto mt-7 w-full max-w-[48rem]'>
295-
<button
296-
type='button'
297-
onClick={() => {
298-
setAnimationsEnabled(true)
299-
setExpanded((prev) => !prev)
300-
}}
301-
aria-expanded={expanded}
302-
className='flex items-center gap-2'
303-
>
304-
<span className='text-[var(--text-muted)] text-small'>Suggested actions</span>
305-
<ChevronDown
329+
<div className='flex items-center justify-between'>
330+
<button
331+
type='button'
332+
onClick={() => {
333+
setAnimationsEnabled(true)
334+
setExpanded((prev) => !prev)
335+
}}
336+
aria-expanded={expanded}
337+
className='flex items-center gap-2'
338+
>
339+
<span className='text-[var(--text-muted)] text-small'>Suggested actions</span>
340+
<ChevronDown
341+
className={cn(
342+
'h-[7px] w-[9px] text-[var(--text-icon)] transition-transform duration-150',
343+
!expanded && '-rotate-90'
344+
)}
345+
/>
346+
</button>
347+
<button
348+
type='button'
349+
onClick={handleShuffle}
350+
aria-label='Shuffle suggested actions'
351+
aria-hidden={!expanded}
352+
tabIndex={expanded ? undefined : -1}
306353
className={cn(
307-
'h-[7px] w-[9px] text-[var(--text-icon)] transition-transform duration-150',
308-
!expanded && '-rotate-90'
354+
chipVariants({ variant: 'ghost', flush: true }),
355+
'-mr-2 gap-1.5 transition-opacity duration-150 ease-out motion-reduce:transition-none',
356+
expanded ? 'opacity-100' : 'pointer-events-none opacity-0'
309357
)}
310-
/>
311-
</button>
358+
>
359+
<span className='-mt-px text-[var(--text-muted)] text-small'>Shuffle</span>
360+
<Shuffle className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
361+
</button>
362+
</div>
312363
<Expandable expanded={expanded}>
313364
<ExpandableContent className={cn('mt-2', !animationsEnabled && '!animate-none')}>
314365
<div className='flex flex-col'>
@@ -324,7 +375,10 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) {
324375
i > 0 && 'border-t'
325376
)}
326377
>
327-
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
378+
<Icon
379+
className='size-[16px] flex-shrink-0 text-[var(--text-icon)]'
380+
style={getBareIconStyle(Icon)}
381+
/>
328382
<span className='flex-1 truncate text-[var(--text-body)] text-sm'>
329383
{action.label}
330384
</span>

0 commit comments

Comments
 (0)