mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-10 21:10:59 +00:00
refactor(web): streamline workspace controls
This commit is contained in:
@@ -90,6 +90,9 @@ export default function WorkspaceSettingsPanel({
|
|||||||
const cloudMembersURL = workspaceInfo
|
const cloudMembersURL = workspaceInfo
|
||||||
? `${systemInfo.cloud_service_url.replace(/\/$/, '')}/cloud?workspace=${encodeURIComponent(workspaceInfo.workspace.uuid)}#workspace-members`
|
? `${systemInfo.cloud_service_url.replace(/\/$/, '')}/cloud?workspace=${encodeURIComponent(workspaceInfo.workspace.uuid)}#workspace-members`
|
||||||
: '';
|
: '';
|
||||||
|
const cloudPortalURL = workspaceInfo
|
||||||
|
? `${systemInfo.cloud_service_url.replace(/\/$/, '')}/cloud?workspace=${encodeURIComponent(workspaceInfo.workspace.uuid)}&step=plan`
|
||||||
|
: '';
|
||||||
|
|
||||||
const loadWorkspace = useCallback(async () => {
|
const loadWorkspace = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -221,6 +224,14 @@ export default function WorkspaceSettingsPanel({
|
|||||||
{t(`workspace.roles.${workspaceInfo.membership.role}`)}
|
{t(`workspace.roles.${workspaceInfo.membership.role}`)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{isCloudProjection && workspaceInfo && (
|
||||||
|
<Button asChild size="sm" variant="outline">
|
||||||
|
<a href={cloudPortalURL} target="_blank" rel="noopener noreferrer">
|
||||||
|
{t('workspace.upgradePlan')}
|
||||||
|
<ExternalLink className="size-3.5" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{canManageCloudMembers && (
|
{canManageCloudMembers && (
|
||||||
<Button asChild size="sm" variant="outline">
|
<Button asChild size="sm" variant="outline">
|
||||||
<a href={cloudMembersURL} target="_blank" rel="noopener noreferrer">
|
<a href={cloudMembersURL} target="_blank" rel="noopener noreferrer">
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import {
|
import { Building2, Check, Settings } from 'lucide-react';
|
||||||
Building2,
|
|
||||||
Check,
|
|
||||||
ExternalLink,
|
|
||||||
Settings,
|
|
||||||
Sparkles,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
switchWorkspaceAndReload,
|
switchWorkspaceAndReload,
|
||||||
systemInfo,
|
|
||||||
useCurrentWorkspace,
|
useCurrentWorkspace,
|
||||||
useWorkspaceBootstrap,
|
useWorkspaceBootstrap,
|
||||||
} from '@/app/infra/http';
|
} from '@/app/infra/http';
|
||||||
@@ -19,7 +12,6 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -38,77 +30,72 @@ export default function WorkspaceSwitcher({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentWorkspace = useCurrentWorkspace();
|
const currentWorkspace = useCurrentWorkspace();
|
||||||
const workspaces = useWorkspaceBootstrap();
|
const workspaces = useWorkspaceBootstrap();
|
||||||
const isCloud = currentWorkspace?.workspace.source === 'cloud_projection';
|
|
||||||
|
|
||||||
if (!currentWorkspace) return null;
|
if (!currentWorkspace) return null;
|
||||||
|
|
||||||
const cloudPortalUrl = `${systemInfo.cloud_service_url.replace(/\/$/, '')}/cloud?workspace=${encodeURIComponent(currentWorkspace.workspace.uuid)}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn('min-w-44 max-w-64 justify-start', className)}
|
className={cn(
|
||||||
|
'h-11 min-w-52 max-w-72 justify-start px-3 text-[15px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
aria-label={t('workspace.switchWorkspace')}
|
aria-label={t('workspace.switchWorkspace')}
|
||||||
>
|
>
|
||||||
<Building2 className="size-4 shrink-0" />
|
<Building2 className="size-4 shrink-0" />
|
||||||
<span className="truncate">{currentWorkspace.workspace.name}</span>
|
<span className="truncate">{currentWorkspace.workspace.name}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="min-w-64">
|
<DropdownMenuContent align="end" className="min-w-80 p-2">
|
||||||
<DropdownMenuLabel>{t('workspace.switchWorkspace')}</DropdownMenuLabel>
|
<DropdownMenuLabel className="px-3 py-2 text-sm">
|
||||||
|
{t('workspace.switchWorkspace')}
|
||||||
|
</DropdownMenuLabel>
|
||||||
{workspaces.map((entry) => {
|
{workspaces.map((entry) => {
|
||||||
const selected =
|
const selected =
|
||||||
entry.workspace.uuid === currentWorkspace.workspace.uuid;
|
entry.workspace.uuid === currentWorkspace.workspace.uuid;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={entry.workspace.uuid}
|
key={entry.workspace.uuid}
|
||||||
|
className="min-h-14 gap-3 px-3 py-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!selected)
|
if (!selected)
|
||||||
void switchWorkspaceAndReload(entry.workspace.uuid);
|
void switchWorkspaceAndReload(entry.workspace.uuid);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Building2 className="size-4" />
|
<Building2 className="size-5" />
|
||||||
<span className="min-w-0 flex-1 truncate">
|
<span className="min-w-0 flex-1 truncate font-medium">
|
||||||
{entry.workspace.name}
|
{entry.workspace.name}
|
||||||
</span>
|
</span>
|
||||||
|
{entry.workspace.source === 'cloud_projection' && (
|
||||||
|
<span className="rounded-md border bg-muted px-2 py-0.5 text-[11px] font-medium uppercase text-muted-foreground">
|
||||||
|
{entry.plan_name || t('workspace.planUnavailable')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{t(`workspace.roles.${entry.membership.role}`)}
|
{t(`workspace.roles.${entry.membership.role}`)}
|
||||||
</span>
|
</span>
|
||||||
{selected && <Check className="size-4" />}
|
{selected && <Check className="size-4" />}
|
||||||
|
{selected && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
aria-label={t('workspace.settings')}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
requestWorkspaceSettings();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{isCloud && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuLabel className="flex items-center justify-between gap-3 font-normal">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{t('workspace.currentPlan')}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-foreground">
|
|
||||||
{currentWorkspace.plan_name || t('workspace.planUnavailable')}
|
|
||||||
</span>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a
|
|
||||||
href={cloudPortalUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Sparkles className="size-4" />
|
|
||||||
{t('workspace.upgradePlan')}
|
|
||||||
<ExternalLink className="ml-auto size-3.5" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem onClick={requestWorkspaceSettings}>
|
|
||||||
<Settings className="size-4" />
|
|
||||||
{t('workspace.settings')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,3 +67,22 @@ test('links Cloud workspace managers to the Space member invitation surface', ()
|
|||||||
assert.match(workspaceSettingsPanelSource, /#workspace-members/);
|
assert.match(workspaceSettingsPanelSource, /#workspace-members/);
|
||||||
assert.match(workspaceSettingsPanelSource, /workspace\.inviteMember/);
|
assert.match(workspaceSettingsPanelSource, /workspace\.inviteMember/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keeps workspace plan and settings controls on the workspace row', () => {
|
||||||
|
assert.match(workspaceSwitcherSource, /entry\.plan_name/);
|
||||||
|
assert.match(workspaceSwitcherSource, /aria-label=\{t\('workspace\.settings'\)\}/);
|
||||||
|
assert.match(workspaceSwitcherSource, /className="size-8"/);
|
||||||
|
assert.doesNotMatch(workspaceSwitcherSource, /workspace\.currentPlan/);
|
||||||
|
assert.doesNotMatch(workspaceSwitcherSource, /workspace\.upgradePlan/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('moves Cloud plan upgrades into Workspace Settings', () => {
|
||||||
|
assert.match(workspaceSettingsPanelSource, /workspace\.upgradePlan/);
|
||||||
|
assert.match(workspaceSettingsPanelSource, /cloudPortalURL/);
|
||||||
|
assert.match(workspaceSettingsPanelSource, /step=plan/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses a larger workspace trigger and menu surface', () => {
|
||||||
|
assert.match(workspaceSwitcherSource, /h-11/);
|
||||||
|
assert.match(workspaceSwitcherSource, /min-w-80/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user