Skip to content

Commit 3d8810e

Browse files
feat(common): add platform support for organization switcher (hoppscotch#5708)
Extends the organization platform definition to support switching between multiple organizations and displaying custom branding (logo and name) in the application header. Adds shared utilities for file uploads and avatar generation, including deterministic colour support. These changes enable the Cloud for Organizations tier to offer: - Multi-organization switching via sidebar UI. - Custom logo uploads for organization branding. - Seamless navigation between different organization instances.
1 parent 824dce7 commit 3d8810e

File tree

7 files changed

+479
-8
lines changed

7 files changed

+479
-8
lines changed

packages/hoppscotch-common/locales/en.json

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
"new_version_found": "New version found. Refresh to update.",
153153
"open_in_hoppscotch": "Open in Hoppscotch",
154154
"options": "Options",
155+
"powered_by": "Powered by Hoppscotch",
155156
"proxy_privacy_policy": "Proxy privacy policy",
156157
"reload": "Reload",
157158
"search": "Search and commands",
@@ -735,6 +736,22 @@
735736
"title": "Export",
736737
"success": "Successfully exported"
737738
},
739+
"file_upload": {
740+
"choose_file": "Choose file",
741+
"max_size_format": "Max 5MB. Supports JPEG, PNG, GIF, WebP",
742+
"profile_photo_updated": "Profile photo updated successfully",
743+
"profile_photo_removed": "Profile photo removed successfully",
744+
"org_logo_updated": "Organization logo updated successfully",
745+
"error_size_limit": "File size must be less than 5MB",
746+
"error_invalid_format": "File must be an image (JPEG, PNG, GIF, or WebP)",
747+
"error_invalid_upload_type": "Invalid upload type",
748+
"error_invalid_org_id": "Invalid organization ID format",
749+
"error_upload_failed": "Upload failed. Please try again",
750+
"error_network_failed": "Network error. Please check your connection",
751+
"error_timeout": "Upload timed out after 30 seconds. Please try again",
752+
"error_missing_backend_url": "Backend URL is not configured. Please set the VITE_BACKEND_API_URL environment variable in your environment settings",
753+
"error_invalid_backend_url": "Invalid backend URL configuration"
754+
},
738755
"filename": {
739756
"cookie_key_value_pairs": "Cookie",
740757
"codegen": "{request_name} - code",
@@ -1257,6 +1274,7 @@
12571274
"profile_description": "Update your profile details",
12581275
"profile_email": "Email address",
12591276
"profile_name": "Profile name",
1277+
"profile_photo": "Profile photo",
12601278
"proxy": "Proxy",
12611279
"proxy_url": "Proxy URL",
12621280
"proxy_use_toggle": "Use the proxy middleware to send requests",
@@ -2191,7 +2209,25 @@
21912209
"delete_account_description": "This will delete all data associated with your Hoppscotch account, including this and any other organizations you are part of.",
21922210
"delete_account": "Delete Hoppscotch Account",
21932211
"user_deletion_failed_sole_admin": "The user is the sole admin of one or more organization instances. Please demote the user before attempting deletion.",
2194-
"user_deletion_failed_sole_team_owner": "The user is the sole team owner on one or more organization instances. Please transfer the ownership or delete the relevant workspaces before attempting deletion."
2212+
"user_deletion_failed_sole_team_owner": "The user is the sole team owner on one or more organization instances. Please transfer the ownership or delete the relevant workspaces before attempting deletion.",
2213+
"no_organizations": "You are not a member of any organizations",
2214+
"admin": "Admin"
2215+
},
2216+
"organization_sidebar": {
2217+
"instances": "Instances",
2218+
"hoppscotch": "Hoppscotch",
2219+
"organizations": "Organizations",
2220+
"admin": "Admin",
2221+
"no_orgs": "No organizations yet",
2222+
"no_orgs_title": "No organizations yet",
2223+
"no_orgs_description": "Join or create an organization to collaborate with your team",
2224+
"error_loading": "Failed to load organizations",
2225+
"expand": "Expand sidebar",
2226+
"collapse": "Collapse sidebar",
2227+
"status_canceled": "Canceled",
2228+
"status_inactive": "Inactive",
2229+
"status_canceled_tooltip": "Subscription canceled - Contact support to reactivate this workspace",
2230+
"status_inactive_tooltip": "This organization is temporarily inactive - Contact support for assistance"
21952231
},
21962232
"billing": {
21972233
"confirm": {

packages/hoppscotch-common/src/components.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ declare module 'vue' {
235235
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
236236
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
237237
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
238+
IconLucideCheck: typeof import('~icons/lucide/check')['default']
238239
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
239240
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
240241
IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default']
@@ -251,11 +252,13 @@ declare module 'vue' {
251252
IconLucideLoader2: typeof import('~icons/lucide/loader2')['default']
252253
IconLucideLock: typeof import('~icons/lucide/lock')['default']
253254
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
255+
IconLucidePauseCircle: typeof import('~icons/lucide/pause-circle')['default']
254256
IconLucidePlusCircle: typeof import('~icons/lucide/plus-circle')['default']
255257
IconLucideRss: typeof import('~icons/lucide/rss')['default']
256258
IconLucideSearch: typeof import('~icons/lucide/search')['default']
257259
IconLucideTerminal: typeof import('~icons/lucide/terminal')['default']
258260
IconLucideTriangleAlert: typeof import('~icons/lucide/triangle-alert')['default']
261+
IconLucideUser: typeof import('~icons/lucide/user')['default']
259262
IconLucideUsers: typeof import('~icons/lucide/users')['default']
260263
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
261264
IconLucideX: typeof import('~icons/lucide/x')['default']

packages/hoppscotch-common/src/components/app/Header.vue

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
}"
1515
>
1616
<div class="flex">
17+
<!-- Instance Switcher (Desktop/On-prem) -->
1718
<tippy
1819
v-if="platform.instance?.instanceSwitchingEnabled"
1920
interactive
@@ -43,6 +44,42 @@
4344
</div>
4445
</template>
4546
</tippy>
47+
48+
<router-link
49+
v-else-if="showOrgLogo"
50+
to="/"
51+
class="flex items-center gap-2 px-2 py-1 rounded hover:bg-primaryDark focus-visible:bg-primaryDark focus-visible:outline-none transition-colors"
52+
>
53+
<div
54+
v-if="orgInfo?.logo"
55+
class="h-6 w-6 rounded overflow-hidden flex-shrink-0"
56+
>
57+
<img
58+
:src="sanitizeLogoUrl(orgInfo.logo)"
59+
:alt="orgInfo.name || t('app.name')"
60+
class="h-full w-full object-cover"
61+
/>
62+
</div>
63+
<div
64+
v-else-if="orgInfo?.name"
65+
class="h-6 w-6 rounded flex items-center justify-center text-xs font-semibold flex-shrink-0"
66+
:class="getOrgColor(orgInfo.name)"
67+
aria-hidden="true"
68+
>
69+
{{ getOrgInitials(orgInfo.name) }}
70+
</div>
71+
<div class="flex items-center gap-1.5 min-w-0">
72+
<span class="font-bold tracking-wide text-secondaryDark truncate">
73+
{{ orgInfo?.name || t("app.name") }}
74+
</span>
75+
<span
76+
class="text-secondary text-xs hidden sm:inline flex-shrink-0"
77+
>
78+
• {{ t("app.powered_by") }}
79+
</span>
80+
</div>
81+
</router-link>
82+
4683
<HoppButtonSecondary
4784
v-else
4885
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
@@ -354,6 +391,11 @@ import { getKernelMode } from "@hoppscotch/kernel"
354391
import { useI18n } from "@composables/i18n"
355392
import { useReadonlyStream } from "@composables/stream"
356393
import { defineActionHandler, invokeAction } from "@helpers/actions"
394+
import {
395+
getOrgInitials,
396+
getOrgColor,
397+
sanitizeLogoUrl,
398+
} from "@helpers/utils/organization"
357399
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
358400
import { useService } from "dioc/vue"
359401
import * as TE from "fp-ts/TaskEither"
@@ -390,6 +432,7 @@ const instanceSwitcherRef =
390432
kernelMode === "desktop" ? ref<any | null>(null) : ref(null)
391433
392434
const isUserAdmin = ref(false)
435+
const orgInfo = ref<{ name?: string; logo?: string | null } | null>(null)
393436
394437
/**
395438
* Feature flag to enable the workspace selector login conversion
@@ -398,18 +441,23 @@ const workspaceSelectorFlagEnabled = computed(
398441
() => !!platform.platformFeatureFlags.workspaceSwitcherLogin?.value
399442
)
400443
401-
/**
402-
* Show the dashboard link if the user is not on the default cloud instance and is an admin
403-
*/
444+
const showOrgLogo = computed(() => {
445+
return platform.organization?.isDefaultCloudInstance === false
446+
})
447+
404448
onMounted(async () => {
405449
const { organization } = platform
406450
407451
if (!organization || organization.isDefaultCloudInstance) return
408452
409-
const orgInfo = await organization.getOrgInfo()
453+
const fetchedOrgInfo = await organization.getOrgInfo()
410454
411-
if (orgInfo) {
412-
isUserAdmin.value = !!orgInfo.isAdmin
455+
if (fetchedOrgInfo) {
456+
isUserAdmin.value = !!fetchedOrgInfo.isAdmin
457+
orgInfo.value = {
458+
name: fetchedOrgInfo.name,
459+
logo: fetchedOrgInfo.logo || null,
460+
}
413461
}
414462
})
415463

0 commit comments

Comments
 (0)