chore: rename workspaces to organizations, bump ciphera-ui to 0.0.49
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,7 +17,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode
|
|||||||
const isOnline = useOnlineStatus()
|
const isOnline = useOnlineStatus()
|
||||||
const [orgs, setOrgs] = useState<any[]>([])
|
const [orgs, setOrgs] = useState<any[]>([])
|
||||||
|
|
||||||
// * Fetch organizations for the header workspace switcher
|
// * Fetch organizations for the header organization switcher
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (auth.user) {
|
if (auth.user) {
|
||||||
getUserOrganizations()
|
getUserOrganizations()
|
||||||
@@ -26,14 +26,14 @@ export default function LayoutContent({ children }: { children: React.ReactNode
|
|||||||
}
|
}
|
||||||
}, [auth.user])
|
}, [auth.user])
|
||||||
|
|
||||||
const handleSwitchWorkspace = async (orgId: string | null) => {
|
const handleSwitchOrganization = async (orgId: string | null) => {
|
||||||
if (!orgId) return // Pulse doesn't support personal workspace
|
if (!orgId) return // Pulse doesn't support personal organization context
|
||||||
try {
|
try {
|
||||||
const { access_token } = await switchContext(orgId)
|
const { access_token } = await switchContext(orgId)
|
||||||
await setSessionAction(access_token)
|
await setSessionAction(access_token)
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to switch workspace', err)
|
console.error('Failed to switch organization', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,9 +56,9 @@ export default function LayoutContent({ children }: { children: React.ReactNode
|
|||||||
appName="Pulse"
|
appName="Pulse"
|
||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
activeOrgId={auth.user?.org_id}
|
activeOrgId={auth.user?.org_id}
|
||||||
onSwitchWorkspace={handleSwitchWorkspace}
|
onSwitchOrganization={handleSwitchOrganization}
|
||||||
onCreateOrganization={handleCreateOrganization}
|
onCreateOrganization={handleCreateOrganization}
|
||||||
allowPersonalWorkspace={false}
|
allowPersonalOrganization={false}
|
||||||
showFaq={false}
|
showFaq={false}
|
||||||
showSecurity={false}
|
showSecurity={false}
|
||||||
showPricing={true}
|
showPricing={true}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Guided onboarding wizard for new Pulse users.
|
* Guided onboarding wizard for new Pulse users.
|
||||||
* Steps: Welcome → Workspace (create org) → Plan / trial → First site (optional) → Done.
|
* Steps: Welcome → Organization (create org) → Plan / trial → First site (optional) → Done.
|
||||||
* Supports ?step= in URL for back/refresh. Handles pulse_pending_checkout from pricing.
|
* Supports ?step= in URL for back/refresh. Handles pulse_pending_checkout from pricing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -47,12 +47,12 @@ import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock'
|
|||||||
import VerificationModal from '@/components/sites/VerificationModal'
|
import VerificationModal from '@/components/sites/VerificationModal'
|
||||||
|
|
||||||
const TOTAL_STEPS = 5
|
const TOTAL_STEPS = 5
|
||||||
const DEFAULT_ORG_NAME = 'My workspace'
|
const DEFAULT_ORG_NAME = 'My organization'
|
||||||
const SITE_DRAFT_KEY = 'pulse_welcome_site_draft'
|
const SITE_DRAFT_KEY = 'pulse_welcome_site_draft'
|
||||||
const WELCOME_COMPLETED_KEY = 'pulse_welcome_completed'
|
const WELCOME_COMPLETED_KEY = 'pulse_welcome_completed'
|
||||||
|
|
||||||
function slugFromName(name: string): string {
|
function slugFromName(name: string): string {
|
||||||
return name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'my-workspace'
|
return name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'my-organization'
|
||||||
}
|
}
|
||||||
|
|
||||||
function suggestSlugVariant(slug: string): string {
|
function suggestSlugVariant(slug: string): string {
|
||||||
@@ -143,7 +143,7 @@ function WelcomeContent() {
|
|||||||
}
|
}
|
||||||
}, [user, step])
|
}, [user, step])
|
||||||
|
|
||||||
const handleSelectWorkspace = async (org: OrganizationMember) => {
|
const handleSelectOrganization = async (org: OrganizationMember) => {
|
||||||
setSwitchingOrgId(org.organization_id)
|
setSwitchingOrgId(org.organization_id)
|
||||||
try {
|
try {
|
||||||
const { access_token } = await switchContext(org.organization_id)
|
const { access_token } = await switchContext(org.organization_id)
|
||||||
@@ -155,13 +155,13 @@ function WelcomeContent() {
|
|||||||
setStep(3)
|
setStep(3)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(getAuthErrorMessage(err) || 'Failed to switch workspace')
|
toast.error(getAuthErrorMessage(err) || 'Failed to switch organization')
|
||||||
} finally {
|
} finally {
|
||||||
setSwitchingOrgId(null)
|
setSwitchingOrgId(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateNewWorkspace = () => setStep(2)
|
const handleCreateNewOrganization = () => setStep(2)
|
||||||
|
|
||||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const val = e.target.value
|
const val = e.target.value
|
||||||
@@ -171,7 +171,7 @@ function WelcomeContent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWorkspaceSubmit = async (e: React.FormEvent) => {
|
const handleOrganizationSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setOrgLoading(true)
|
setOrgLoading(true)
|
||||||
setOrgError('')
|
setOrgError('')
|
||||||
@@ -186,7 +186,7 @@ function WelcomeContent() {
|
|||||||
trackWelcomeWorkspaceCreated(!!(typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')))
|
trackWelcomeWorkspaceCreated(!!(typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')))
|
||||||
setStep(3)
|
setStep(3)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const { message, suggestSlug } = getOrgErrorMessage(err, orgSlug, 'Failed to create workspace')
|
const { message, suggestSlug } = getOrgErrorMessage(err, orgSlug, 'Failed to create organization')
|
||||||
setOrgError(message)
|
setOrgError(message)
|
||||||
if (suggestSlug) setOrgSlug(suggestSlug)
|
if (suggestSlug) setOrgSlug(suggestSlug)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -367,10 +367,10 @@ function WelcomeContent() {
|
|||||||
<BarChartIcon className="h-7 w-7" />
|
<BarChartIcon className="h-7 w-7" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-xl font-bold text-neutral-900 dark:text-white">
|
||||||
Choose your workspace
|
Choose your organization
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Continue with an existing workspace or create a new one.
|
Continue with an existing organization or create a new one.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 mb-6">
|
<div className="space-y-2 mb-6">
|
||||||
@@ -378,12 +378,12 @@ function WelcomeContent() {
|
|||||||
<button
|
<button
|
||||||
key={org.organization_id}
|
key={org.organization_id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelectWorkspace(org)}
|
onClick={() => handleSelectOrganization(org)}
|
||||||
disabled={!!switchingOrgId}
|
disabled={!!switchingOrgId}
|
||||||
className="w-full flex items-center justify-between gap-3 rounded-xl border border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:border-brand-orange/50 px-4 py-3 text-left transition-colors disabled:opacity-60 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2"
|
className="w-full flex items-center justify-between gap-3 rounded-xl border border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:border-brand-orange/50 px-4 py-3 text-left transition-colors disabled:opacity-60 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2"
|
||||||
>
|
>
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<span className="font-medium text-neutral-900 dark:text-white">
|
||||||
{org.organization_name || 'Workspace'}
|
{org.organization_name || 'Organization'}
|
||||||
</span>
|
</span>
|
||||||
{user?.org_id === org.organization_id && (
|
{user?.org_id === org.organization_id && (
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Current</span>
|
<span className="text-xs text-neutral-500 dark:text-neutral-400">Current</span>
|
||||||
@@ -396,10 +396,10 @@ function WelcomeContent() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleCreateNewWorkspace}
|
onClick={handleCreateNewOrganization}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4 mr-2" />
|
<PlusIcon className="h-4 w-4 mr-2" />
|
||||||
Create a new workspace
|
Create a new organization
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -450,16 +450,16 @@ function WelcomeContent() {
|
|||||||
<BarChartIcon className="h-7 w-7" />
|
<BarChartIcon className="h-7 w-7" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-xl font-bold text-neutral-900 dark:text-white">
|
||||||
Name your workspace
|
Name your organization
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
You can change this later in settings.
|
You can change this later in settings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleWorkspaceSubmit} className="space-y-4">
|
<form onSubmit={handleOrganizationSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="welcome-org-name" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
<label htmlFor="welcome-org-name" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||||
Workspace name
|
Organization name
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="welcome-org-name"
|
id="welcome-org-name"
|
||||||
@@ -485,7 +485,7 @@ function WelcomeContent() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Used in your workspace URL.
|
Used in your organization URL.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{orgError && (
|
{orgError && (
|
||||||
@@ -511,7 +511,7 @@ function WelcomeContent() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep(2)}
|
onClick={() => setStep(2)}
|
||||||
className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus:outline-none focus:ring-2 focus:ring-brand-orange rounded"
|
className="flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus:outline-none focus:ring-2 focus:ring-brand-orange rounded"
|
||||||
aria-label="Back to workspace"
|
aria-label="Back to organization"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
Back
|
Back
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import { switchContext, OrganizationMember } from '@/lib/api/organization'
|
|||||||
import { setSessionAction } from '@/app/actions/auth'
|
import { setSessionAction } from '@/app/actions/auth'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) {
|
export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [switching, setSwitching] = useState<string | null>(null)
|
const [switching, setSwitching] = useState<string | null>(null)
|
||||||
|
|
||||||
const handleSwitch = async (orgId: string | null) => {
|
const handleSwitch = async (orgId: string | null) => {
|
||||||
console.log('Switching to workspace:', orgId)
|
console.log('Switching to organization:', orgId)
|
||||||
setSwitching(orgId || 'personal')
|
setSwitching(orgId || 'personal')
|
||||||
try {
|
try {
|
||||||
// * If orgId is null, we can't switch context via API in the same way if strict mode is on
|
// * If orgId is null, we can't switch context via API in the same way if strict mode is on
|
||||||
// * BUT, Pulse doesn't support personal workspace.
|
// * Pulse doesn't support personal organization context.
|
||||||
// * So we should probably NOT show the "Personal" option in Pulse if strict mode is enforced.
|
// * So we should probably NOT show the "Personal" option in Pulse if strict mode is enforced.
|
||||||
// * However, to match Drop exactly, we might want to show it but have it fail or redirect?
|
// * However, to match Drop exactly, we might want to show it but have it fail or redirect?
|
||||||
// * Let's assume for now we want to match Drop's UI structure.
|
// * Let's assume for now we want to match Drop's UI structure.
|
||||||
@@ -38,7 +38,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to switch workspace', err)
|
console.error('Failed to switch organization', err)
|
||||||
setSwitching(null)
|
setSwitching(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,10 +46,10 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
|
|||||||
return (
|
return (
|
||||||
<div className="border-b border-neutral-100 dark:border-neutral-800 pb-2 mb-2">
|
<div className="border-b border-neutral-100 dark:border-neutral-800 pb-2 mb-2">
|
||||||
<div className="px-3 py-2 text-xs font-medium text-neutral-500 uppercase tracking-wider">
|
<div className="px-3 py-2 text-xs font-medium text-neutral-500 uppercase tracking-wider">
|
||||||
Workspaces
|
Organizations
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Personal Workspace - HIDDEN IN PULSE (Strict Mode) */}
|
{/* Personal organization - HIDDEN IN PULSE (Strict Mode) */}
|
||||||
{/*
|
{/*
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSwitch(null)}
|
onClick={() => handleSwitch(null)}
|
||||||
@@ -70,7 +70,7 @@ export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: Organiz
|
|||||||
</button>
|
</button>
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
{/* Organization Workspaces */}
|
{/* Organization list */}
|
||||||
{orgs.map((org) => (
|
{orgs.map((org) => (
|
||||||
<button
|
<button
|
||||||
key={org.organization_id}
|
key={org.organization_id}
|
||||||
|
|||||||
@@ -237,11 +237,11 @@ export default function OrganizationSettings() {
|
|||||||
}
|
}
|
||||||
}, [activeTab, currentOrgId, loadAudit, auditFetchTrigger])
|
}, [activeTab, currentOrgId, loadAudit, auditFetchTrigger])
|
||||||
|
|
||||||
// If no org ID, we are in personal workspace, so don't show org settings
|
// If no org ID, we are in personal organization context, so don't show org settings
|
||||||
if (!currentOrgId) {
|
if (!currentOrgId) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 text-center text-neutral-500">
|
<div className="p-6 text-center text-neutral-500">
|
||||||
<p>You are in your Personal Workspace. Switch to an Organization to manage its settings.</p>
|
<p>You are in your personal context. Switch to an Organization to manage its settings.</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.48",
|
"@ciphera-net/ui": "^0.0.49",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"country-flag-icons": "^1.6.4",
|
"country-flag-icons": "^1.6.4",
|
||||||
@@ -1467,9 +1467,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ciphera-net/ui": {
|
"node_modules/@ciphera-net/ui": {
|
||||||
"version": "0.0.48",
|
"version": "0.0.49",
|
||||||
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.48/20eedc6319567fff80530d865ebbed6e6f784dab",
|
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.49/ef6f7f06a134bc3d3b4cb1086f689ddb34f1652a",
|
||||||
"integrity": "sha512-kDrGV7tzAjeiz7MtK5G81iY08Zg4NxHTlDNkH5/Pkl2aSpyraf04/J/pGshChjdXMdioXrJA7JU8YH+GlI1LfA==",
|
"integrity": "sha512-ga2n0kO7JeOFzVVRX+FU5iQxodv2yE/hUnlEUHEomorKzWCADM9wAOLGcxi8mcVz49jy/4IQlHRdpF9LH64uQg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.48",
|
"@ciphera-net/ui": "^0.0.49",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"country-flag-icons": "^1.6.4",
|
"country-flag-icons": "^1.6.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user