pi sills
This commit is contained in:
@@ -40,6 +40,7 @@ export function App() {
|
||||
<Route path="/settings/system" element={user?.role !== 'Member' ? <Dashboard.SystemSettings /> : <Navigate to="/" replace />} />
|
||||
<Route path="/settings/resources" element={user?.role !== 'Member' ? <Dashboard.ResourceSettings /> : <Navigate to="/" replace />} />
|
||||
<Route path="/settings/users" element={user?.role === 'Super Admin' ? <Dashboard.UserSettings /> : <Navigate to="/" replace />} />
|
||||
<Route path="/settings/integrations" element={<Dashboard.IntegrationsSettings />} />
|
||||
<Route path="/automation" element={<Dashboard.Automation />} />
|
||||
<Route path="/chat" element={<Dashboard.SessionListPage />} />
|
||||
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useGlobal } from 'hooks/useGlobal';
|
||||
|
||||
const initialState: LoginFormState = {
|
||||
// email: 'pastilhas@pastilhas.dev',
|
||||
// password: '1234567890',
|
||||
// password: '',
|
||||
};
|
||||
export function Login() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@@ -70,6 +70,7 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
locked
|
||||
mobilePanelId={mobilePanelId}
|
||||
onMobilePanelChange={(id) => {
|
||||
if (!id) navigate('/chat', { replace: true });
|
||||
|
||||
@@ -9,7 +9,7 @@ export const FilesScreen = () => {
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView workspace={workspace} ephemeral={ephemeral} />
|
||||
<WorkspaceView workspace={workspace} locked ephemeral={ephemeral} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import type { LayoutNode, PanelComponents, DefaultFileSort } from 'officerdev';
|
||||
import { WorkspaceView, useFileViewerPanels } from 'officerdev';
|
||||
import { useWorkspacesState } from 'state/useWorkspacesState';
|
||||
import { useSettings } from 'state/useSettings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
const HomeHeader = () => {
|
||||
const { settings, saveSettings } = useSettings();
|
||||
|
||||
const completeOnboarding = () => {
|
||||
saveSettings({ ...settings, onboarding: { complete: true } });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6 text-center">
|
||||
<div>
|
||||
@@ -11,6 +19,9 @@ const HomeHeader = () => {
|
||||
<p className="mt-2 text-sm opacity-70">
|
||||
Please follow the video instructions below in order to get familiar with all that is possible.
|
||||
</p>
|
||||
<Button className="mt-6" onClick={completeOnboarding}>
|
||||
I'm ready
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -25,10 +36,26 @@ const defaultSort: DefaultFileSort = { field: 'type', direction: 'desc' };
|
||||
export const HomeScreen = () => {
|
||||
const workspace = useWorkspacesState<LayoutNode>('screens/home', defaultLayout);
|
||||
const ephemeral = useFileViewerPanels();
|
||||
const { settings } = useSettings();
|
||||
|
||||
if (settings.onboarding.complete) {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView workspace={workspace} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView workspace={workspace} initialFilePath="/Onboarding" defaultFileSort={defaultSort} components={components} ephemeral={ephemeral} />
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
locked
|
||||
initialFilePath="/Onboarding"
|
||||
defaultFileSort={defaultSort}
|
||||
components={components}
|
||||
ephemeral={ephemeral}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import * as Dropdown from '@/components/ui/dropdown-menu';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { User, Users, LogOut, Settings, Package, Sun, Moon } from 'lucide-react';
|
||||
import { User, Users, LogOut, Settings, Package, Puzzle, Sun, Moon } from 'lucide-react';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
import { useColorMode } from '@/components/ui/ThemeProvider';
|
||||
@@ -59,6 +59,12 @@ export function UserMenu() {
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/integrations">
|
||||
<Puzzle className="mr-2 h-4 w-4" />
|
||||
Integrations
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{user?.role === 'Super Admin' && (
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/users">
|
||||
|
||||
@@ -30,6 +30,7 @@ export const ProjectListScreen = () => {
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
locked
|
||||
mobilePanelId={mobilePanelId}
|
||||
onMobilePanelChange={(id) => {
|
||||
if (!id) setSelected(null);
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
type GoogleStatus = {
|
||||
connected: boolean;
|
||||
email: string | null;
|
||||
configured: boolean;
|
||||
};
|
||||
|
||||
export const GoogleAccount = () => {
|
||||
const client = useClient();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [status, setStatus] = useState<GoogleStatus>({ connected: false, email: null, configured: false });
|
||||
|
||||
const fetchStatus = () => {
|
||||
client
|
||||
.get<GoogleStatus>('/integrations/google/status')
|
||||
.then(setStatus)
|
||||
.catch(() => {})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const result = params.get('google');
|
||||
if (result === 'success') {
|
||||
toast.success('Google account connected');
|
||||
} else if (result === 'error') {
|
||||
toast.error('Failed to connect Google account');
|
||||
}
|
||||
if (result) {
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleConnect = () => {
|
||||
const params = new URLSearchParams({
|
||||
token: client.token ?? '',
|
||||
origin: window.location.origin,
|
||||
});
|
||||
window.location.href = `/api/integrations/google/authorize?${params.toString()}`;
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
try {
|
||||
await client.delete('/integrations/google/connection');
|
||||
setStatus({ ...status, connected: false, email: null });
|
||||
toast.success('Google account disconnected');
|
||||
} catch {
|
||||
toast.error('Failed to disconnect Google account');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
if (!status.configured) {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<p className="text-sm text-duck-dark/60 dark:text-foreground/60">
|
||||
Google integration has not been configured yet. Ask your administrator to set up Google OAuth credentials in
|
||||
the Enterprise settings.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status.connected) {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-green-500 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Connected</p>
|
||||
<p className="text-xs text-duck-dark/50 dark:text-foreground/50 truncate">{status.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40">
|
||||
Officer has access to your Google Calendar, Gmail, and other enabled services.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleDisconnect}
|
||||
className="w-full h-11 cursor-pointer"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<p className="text-sm text-duck-dark/60 dark:text-foreground/60">
|
||||
Connect your Google account to give Officer access to your Calendar, Gmail, and other Google services.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleConnect}
|
||||
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer"
|
||||
>
|
||||
Connect Google Account
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
type GoogleOAuthSettings = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
const SCOPES = [
|
||||
{ scope: 'gmail.readonly', description: 'Read emails' },
|
||||
{ scope: 'calendar.readonly', description: 'Read calendar events' },
|
||||
];
|
||||
|
||||
const SetupGuide = ({ redirectUri }: { redirectUri: string }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-sm font-medium text-duck-teal cursor-pointer hover:underline w-full">
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${open ? 'rotate-180' : ''}`} />
|
||||
Step-by-step setup guide
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ol className="mt-3 grid gap-4 text-sm text-duck-dark/70 dark:text-foreground/70 list-decimal list-outside pl-5">
|
||||
<li>
|
||||
<strong className="text-duck-dark dark:text-foreground">Create a Google Cloud project</strong>
|
||||
<p className="mt-1">
|
||||
Go to the{' '}
|
||||
<a href="https://console.cloud.google.com/projectcreate" target="_blank" rel="noopener noreferrer" className="text-duck-teal underline">
|
||||
New Project
|
||||
</a>{' '}
|
||||
page. Give it a name (e.g. "Officer") and click <strong>Create</strong>.
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong className="text-duck-dark dark:text-foreground">Enable the APIs</strong>
|
||||
<p className="mt-1">
|
||||
Go to{' '}
|
||||
<a href="https://console.cloud.google.com/apis/library" target="_blank" rel="noopener noreferrer" className="text-duck-teal underline">
|
||||
API Library
|
||||
</a>
|
||||
. Search for and enable each of these:
|
||||
</p>
|
||||
<ul className="mt-1 list-disc list-outside pl-5 grid gap-0.5">
|
||||
<li><strong>Gmail API</strong></li>
|
||||
<li><strong>Google Calendar API</strong></li>
|
||||
</ul>
|
||||
<p className="mt-1">Click each one, then click <strong>Enable</strong>.</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong className="text-duck-dark dark:text-foreground">Configure the OAuth consent screen</strong>
|
||||
<p className="mt-1">
|
||||
Go to{' '}
|
||||
<a href="https://console.cloud.google.com/auth/branding" target="_blank" rel="noopener noreferrer" className="text-duck-teal underline">
|
||||
OAuth Branding
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<ul className="mt-1 list-disc list-outside pl-5 grid gap-0.5">
|
||||
<li>Set <strong>App name</strong> to your organization name or "Officer"</li>
|
||||
<li>Set <strong>User support email</strong> to your admin email</li>
|
||||
<li>Add your admin email under <strong>Developer contact information</strong></li>
|
||||
<li>Click <strong>Save</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong className="text-duck-dark dark:text-foreground">Set the audience</strong>
|
||||
<p className="mt-1">
|
||||
Go to{' '}
|
||||
<a href="https://console.cloud.google.com/auth/audience" target="_blank" rel="noopener noreferrer" className="text-duck-teal underline">
|
||||
OAuth Audience
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<ul className="mt-1 list-disc list-outside pl-5 grid gap-0.5">
|
||||
<li>
|
||||
If your team uses Google Workspace, select <strong>Internal</strong> — no verification needed
|
||||
</li>
|
||||
<li>
|
||||
Otherwise, select <strong>External</strong> and add your team's emails under <strong>Test users</strong> (required while the app is unverified; limit of 100 test users)
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong className="text-duck-dark dark:text-foreground">Add scopes</strong>
|
||||
<p className="mt-1">
|
||||
In the left sidebar, click{' '}
|
||||
<a href="https://console.cloud.google.com/auth/scopes" target="_blank" rel="noopener noreferrer" className="text-duck-teal underline">
|
||||
Data Access
|
||||
</a>
|
||||
, then click <strong>Add or remove scopes</strong>. Search for and add:
|
||||
</p>
|
||||
<ul className="mt-1 list-disc list-outside pl-5 grid gap-0.5">
|
||||
{SCOPES.map((s) => (
|
||||
<li key={s.scope}>
|
||||
<code className="text-xs bg-duck-dark/5 dark:bg-foreground/5 px-1.5 py-0.5 rounded">
|
||||
{s.scope}
|
||||
</code>{' '}
|
||||
— {s.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-1">Click <strong>Update</strong>, then <strong>Save</strong>.</p>
|
||||
<p className="mt-2 text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||
Note: <code className="bg-duck-dark/5 dark:bg-foreground/5 px-1 py-0.5 rounded">calendar.readonly</code> is classified as <strong>sensitive</strong> and{' '}
|
||||
<code className="bg-duck-dark/5 dark:bg-foreground/5 px-1 py-0.5 rounded">gmail.readonly</code> as <strong>restricted</strong> by Google.
|
||||
This is fine for Internal apps (Google Workspace) and External apps in testing mode. Publishing to production with restricted scopes requires Google verification.
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong className="text-duck-dark dark:text-foreground">Create OAuth credentials</strong>
|
||||
<p className="mt-1">
|
||||
In the left sidebar, click{' '}
|
||||
<a href="https://console.cloud.google.com/auth/clients" target="_blank" rel="noopener noreferrer" className="text-duck-teal underline">
|
||||
Clients
|
||||
</a>
|
||||
, then click <strong>Create OAuth client</strong>.
|
||||
</p>
|
||||
<ul className="mt-1 list-disc list-outside pl-5 grid gap-0.5">
|
||||
<li>Application type: <strong>Web application</strong></li>
|
||||
<li>Name: anything (e.g. "Officer")</li>
|
||||
<li>
|
||||
Authorized redirect URIs: add{' '}
|
||||
<code className="text-xs bg-duck-dark/5 dark:bg-foreground/5 px-1.5 py-0.5 rounded break-all">
|
||||
{redirectUri}
|
||||
</code>
|
||||
</li>
|
||||
<li>Click <strong>Create</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong className="text-duck-dark dark:text-foreground">Copy the credentials</strong>
|
||||
<p className="mt-1">
|
||||
A dialog will show your <strong>Client ID</strong> and <strong>Client Secret</strong>. Copy both and paste them into the fields below.
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
type VerifyStatus = { valid: boolean; error: string | null } | null;
|
||||
|
||||
const CredentialStatus = ({ status, isVerifying }: { status: VerifyStatus; isVerifying: boolean }) => {
|
||||
if (isVerifying) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-3">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-duck-dark/20 dark:bg-foreground/20 animate-pulse shrink-0" />
|
||||
<span className="text-sm text-duck-dark/50 dark:text-foreground/50">Verifying credentials...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!status) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-3">
|
||||
<div className={`h-2.5 w-2.5 rounded-full shrink-0 ${status.valid ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<span className={`text-sm ${status.valid ? 'text-duck-dark dark:text-foreground' : 'text-red-500'}`}>
|
||||
{status.valid ? 'Credentials valid' : status.error ?? 'Invalid credentials'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GoogleOAuthConfig = () => {
|
||||
const client = useClient();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [clientId, setClientId] = useState('');
|
||||
const [clientSecret, setClientSecret] = useState('');
|
||||
const [verifyStatus, setVerifyStatus] = useState<VerifyStatus>(null);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
const verify = () => {
|
||||
setIsVerifying(true);
|
||||
client
|
||||
.get<{ valid: boolean; error: string | null }>('/integrations/google/verify')
|
||||
.then(setVerifyStatus)
|
||||
.catch(() => setVerifyStatus({ valid: false, error: 'Verification request failed' }))
|
||||
.finally(() => setIsVerifying(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
client
|
||||
.get<GoogleOAuthSettings | null>('/integrations/google/config')
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
setClientId(data.clientId);
|
||||
setClientSecret(data.clientSecret);
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Verify on load if credentials exist
|
||||
useEffect(() => {
|
||||
if (!isLoading && clientId && clientSecret) verify();
|
||||
}, [isLoading]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isSaving) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await client.put('/integrations/google/config', { clientId: clientId.trim(), clientSecret: clientSecret.trim() });
|
||||
toast.success('Google OAuth configuration saved');
|
||||
verify();
|
||||
} catch {
|
||||
toast.error('Failed to save Google OAuth configuration');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
const redirectUri = `${window.location.origin}/api/integrations/google/callback`;
|
||||
|
||||
return (
|
||||
<div className="grid gap-5">
|
||||
<CredentialStatus status={verifyStatus} isVerifying={isVerifying} />
|
||||
|
||||
<SetupGuide redirectUri={redirectUri} />
|
||||
|
||||
<div className="border-t border-duck-dark/10 dark:border-foreground/10 pt-5 grid gap-5">
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70 dark:text-foreground/70">Client ID</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="text"
|
||||
value={clientId}
|
||||
onChange={(ev) => setClientId(ev.target.value)}
|
||||
placeholder="123456789.apps.googleusercontent.com"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70 dark:text-foreground/70">Client Secret</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
value={clientSecret}
|
||||
onChange={(ev) => setClientSecret(ev.target.value)}
|
||||
placeholder="GOCSPX-..."
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70 dark:text-foreground/70">Redirect URI</span>
|
||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40">
|
||||
Add this URI to your Google OAuth client's authorized redirect URIs
|
||||
</p>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark/60"
|
||||
type="text"
|
||||
value={redirectUri}
|
||||
readOnly
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !clientId.trim() || !clientSecret.trim()}
|
||||
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Puzzle, KeyRound, UserCircle } from 'lucide-react';
|
||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
||||
import { WorkspaceLayout } from 'officerdev';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
import { SettingsSidebar, SettingsContent, type SettingsSection } from '../SettingsPanel';
|
||||
import { GoogleOAuthConfig } from './GoogleOAuthConfig';
|
||||
import { GoogleAccount } from './GoogleAccount';
|
||||
|
||||
const GLOBAL_KEY = 'INTEGRATIONS_SETTINGS_SELECTED';
|
||||
const TAB_KEY = 'INTEGRATIONS_SETTINGS_TAB';
|
||||
|
||||
const enterpriseSections: SettingsSection[] = [
|
||||
{ key: 'google-oauth', icon: KeyRound, title: 'Google OAuth', description: 'Client ID and secret for Google APIs', content: <GoogleOAuthConfig /> },
|
||||
];
|
||||
|
||||
const personalSections: SettingsSection[] = [
|
||||
{ key: 'google-account', icon: UserCircle, title: 'Google Account', description: 'Connect your Google account', content: <GoogleAccount /> },
|
||||
];
|
||||
|
||||
const IntegrationsSidebar = () => {
|
||||
const { user } = useAuth();
|
||||
const isSuperAdmin = user?.role === 'Super Admin';
|
||||
const [tab, setTab] = useGlobal<string>(TAB_KEY, isSuperAdmin ? 'enterprise' : 'personal');
|
||||
const sections = tab === 'enterprise' ? enterpriseSections : personalSections;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-3 pb-2">
|
||||
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal">
|
||||
<Puzzle className="h-4 w-4" />
|
||||
Integrations
|
||||
</div>
|
||||
</div>
|
||||
{isSuperAdmin && (
|
||||
<div className="px-3 pb-2">
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="enterprise" className="flex-1 cursor-pointer">
|
||||
Enterprise
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="personal" className="flex-1 cursor-pointer">
|
||||
Personal
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
<SettingsSidebar globalKey={GLOBAL_KEY} icon={Puzzle} label="Integrations" sections={sections} hideHeader />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const IntegrationsContent = () => {
|
||||
const { user } = useAuth();
|
||||
const isSuperAdmin = user?.role === 'Super Admin';
|
||||
const [tab] = useGlobal<string>(TAB_KEY, isSuperAdmin ? 'enterprise' : 'personal');
|
||||
const sections = tab === 'enterprise' ? enterpriseSections : personalSections;
|
||||
|
||||
return <SettingsContent globalKey={GLOBAL_KEY} sections={sections} />;
|
||||
};
|
||||
|
||||
const layout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'integrations-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'integrations-left', appType: null }, size: 20 },
|
||||
{ node: { type: 'panel', id: 'integrations-right', appType: null }, size: 80 },
|
||||
],
|
||||
};
|
||||
|
||||
export const IntegrationsSettings = () => {
|
||||
const panelComponents: PanelComponents = useMemo(
|
||||
() => ({
|
||||
'integrations-left': IntegrationsSidebar,
|
||||
'integrations-right': IntegrationsContent,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -24,6 +24,7 @@ type SettingsSidebarProps = {
|
||||
label: string;
|
||||
sections: SettingsSection[];
|
||||
groups?: SettingsSectionGroup[];
|
||||
hideHeader?: boolean;
|
||||
};
|
||||
|
||||
const SectionButton = ({
|
||||
@@ -50,7 +51,7 @@ const SectionButton = ({
|
||||
</button>
|
||||
);
|
||||
|
||||
export const SettingsSidebar = ({ globalKey, icon: Icon, label, sections, groups }: SettingsSidebarProps) => {
|
||||
export const SettingsSidebar = ({ globalKey, icon: Icon, label, sections, groups, hideHeader }: SettingsSidebarProps) => {
|
||||
const allSections = groups ? groups.flatMap((g) => g.sections) : sections;
|
||||
const [selectedKey, setSelectedKey] = useGlobal<string | null>(globalKey, allSections[0]?.key ?? null);
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -61,12 +62,14 @@ export const SettingsSidebar = ({ globalKey, icon: Icon, label, sections, groups
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
<div className="p-3 pb-2">
|
||||
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal">
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
{!hideHeader && (
|
||||
<div className="p-3 pb-2">
|
||||
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal">
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-3 pb-2">
|
||||
<Input placeholder="Search..." value={search} onChange={(ev) => setSearch(ev.target.value)} className="h-8 text-xs" />
|
||||
</div>
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './ProfileSettings';
|
||||
export * from './SystemSettings';
|
||||
export * from './ResourceSettings';
|
||||
export * from './UserSettings';
|
||||
export * from './IntegrationsSettings';
|
||||
|
||||
@@ -8,7 +8,7 @@ export const TerminalScreen = () => {
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView workspace={workspace} />
|
||||
<WorkspaceView workspace={workspace} locked />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export const WorkspacesScreen = () => {
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
locked
|
||||
mobilePanelId={mobilePanelId}
|
||||
onMobilePanelChange={(id) => {
|
||||
if (!id) setSelected(null);
|
||||
|
||||
Reference in New Issue
Block a user