| name | primary-sidebar |
| description | Complete guide for adding, updating, and removing tabs in the Primary Sidebar of OrcaQ. Covers the full flow — ActivityBarItemType enum → useActivityBarStore → PrimarySideBar component → Management panel component. Load this skill for any task involving the left sidebar, activity bar tabs, or management panels (Explorer, Schemas, ERD, Roles, Export, Agent). |
Primary Sidebar Flow — OrcaQ
Architecture Overview
The Primary Sidebar is driven by a single active tab value in a Pinia store. The flow is:
ActivityBarItemType (enum) ← Tab identity
useActivityBarStore.activityActive ← Which tab is currently active (persisted)
PrimarySideBar.vue ← Watches activityActive, renders the matching component
Management***.vue ← The actual panel content (KeepAlive'd)
The Activity Bar (the narrow icon strip on the far left) calls setActivityActive(type). The Primary Sidebar reacts to the change and swaps the rendered panel — all panels are wrapped in <KeepAlive> so their state is preserved when the user switches tabs.
File Locations
| Purpose | File |
|---|
| Tab type enum + store | core/stores/useActivityBarStore.ts |
| Sidebar shell (switcher) | components/modules/app-shell/primary-side-bar/components/PrimarySideBar.vue |
| Sidebar public API | components/modules/app-shell/primary-side-bar/index.ts |
| All management panels | components/modules/management/ |
| Management public API | components/modules/management/index.ts |
| Shared header component | components/modules/management/shared/components/ManagementSidebarHeader.vue |
Management panel locations
| Tab | Panel component file |
|---|
| Explorer | components/modules/management/explorer/ManagementExplorer.vue |
| Schemas | components/modules/management/schemas/ManagementSchemas.vue |
| ERD Diagram | components/modules/management/erd-diagram/ManagementErdDiagram.vue |
| Users & Roles | components/modules/management/role-permission/ManagementUsersAndPermission.vue |
| Export | components/modules/management/export/ManagementExport.vue |
| Agent | components/modules/management/agent/ManagementAgent.vue |
How the Switcher Works (PrimarySideBar.vue)
<script setup lang="ts">
const activityStore = useActivityBarStore();
const current = shallowRef();
watch(
() => activityStore.activityActive,
() => {
if (activityStore.activityActive === ActivityBarItemType.Explorer)
current.value = ManagementExplorer;
if (activityStore.activityActive === ActivityBarItemType.Schemas)
current.value = ManagementSchemas;
// ... one branch per tab
},
{ immediate: true }
);
</script>
<template>
<div class="w-full h-full flex flex-col" v-if="appConfigStore.layoutSize[0]">
<KeepAlive>
<component :is="current" />
</KeepAlive>
</div>
</template>
Key points:
- Uses
shallowRef (not ref) for the component — avoids deep reactivity on component objects.
immediate: true so the correct panel is rendered on first mount.
<KeepAlive> preserves scroll position and internal state when switching tabs.
- The panel is only mounted when the sidebar is open (
layoutSize[0] > 0).
How to Add a New Sidebar Tab
Step 1 — Add enum value
In core/stores/useActivityBarStore.ts:
export enum ActivityBarItemType {
Explorer = 'Explorer',
Schemas = 'Schemas',
ErdDiagram = 'ERDiagram',
UsersRoles = 'UsersRoles',
DatabaseExport = 'DatabaseExport',
Agent = 'Agent',
MyNewTab = 'MyNewTab',
}
Step 2 — Create the management panel module
Create the folder components/modules/management/my-new-tab/ with this structure:
my-new-tab/
├── index.ts ← exports ManagementMyNewTab
├── ManagementMyNewTab.vue ← entry component
├── components/ ← sub-components (optional)
├── hooks/ ← business logic composables (optional)
└── services/ ← API calls (optional)
ManagementMyNewTab.vue minimum template:
<script setup lang="ts">
import { ManagementSidebarHeader } from '../shared';
</script>
<template>
<div class="flex flex-col h-full w-full overflow-y-auto">
<ManagementSidebarHeader title="My New Tab" />
<!-- panel content here -->
</div>
</template>
index.ts:
export { default as ManagementMyNewTab } from './ManagementMyNewTab.vue';
Step 3 — Export from the management module
In components/modules/management/index.ts:
export * from './my-new-tab';
Step 4 — Register in PrimarySideBar
In components/modules/app-shell/primary-side-bar/components/PrimarySideBar.vue:
import { ManagementMyNewTab } from '#components';
watch(
() => activityStore.activityActive,
() => {
if (activityStore.activityActive === ActivityBarItemType.MyNewTab)
current.value = ManagementMyNewTab;
},
{ immediate: true }
);
Step 5 — Add Activity Bar button
The Activity Bar icon strip that calls setActivityActive is separate from the management module. Find the component that renders the icon list and add a button:
activityStore.setActivityActive(ActivityBarItemType.MyNewTab);
How to Update an Existing Panel
- Find the panel in
components/modules/management/<tab-name>/Management<Tab>.vue.
- Business logic (API calls, state) belongs in a hook under
<tab-name>/hooks/.
- Static structure (sub-components) belongs in
<tab-name>/components/.
- Persisted UI state (expanded nodes, scroll position) goes into
useActivityBarStore — see schemasExpandedState, schemaCurrentScrollTop as examples.
ManagementSidebarHeader — Shared Header Component
All panels use the shared header. Props:
| Prop | Type | Default | Purpose |
|---|
title | string | required | Panel title text |
showConnection | boolean | false | Show ConnectionSelector dropdown |
showSchema | boolean | false | Show SchemaSelector dropdown |
workspaceId | string | — | Required when showConnection is true |
showSearch | boolean | false | Show search input |
searchPlaceholder | string | 'Search...' | Input placeholder |
It also accepts a v-model:search for two-way search binding and an #actions slot for icon buttons in the title bar.
Standard usage:
<ManagementSidebarHeader
title="My Panel"
:show-connection="true"
:workspaceId="workspaceId"
:show-search="true"
v-model:search="searchInput"
>
<template #actions>
<Button size="iconSm" variant="ghost" @click="onRefresh">
<Icon name="hugeicons:refresh" class="size-4!" />
</Button>
</template>
</ManagementSidebarHeader>
Controlling the Sidebar Programmatically
import {
useActivityBarStore,
ActivityBarItemType,
} from '~/core/stores/useActivityBarStore';
const activityStore = useActivityBarStore();
activityStore.setActivityActive(ActivityBarItemType.Schemas);
const isSchemas = activityStore.activityActive === ActivityBarItemType.Schemas;
The sidebar visibility is controlled by appConfigStore.layoutSize[0]. Use onToggleActivityBarPanel() from useAppConfigStore to open/close it — do NOT manipulate layoutSize directly.
Hook Pattern for Panel Logic
Every non-trivial panel delegates logic to a hook. The hook receives UI callbacks via parameter (avoid importing component refs directly):
export function useMyPanelTree(callbacks: {
focusNode: (id: string) => void;
collapseAll: () => void;
expandAll: () => void;
isExpandedAll: ComputedRef<boolean>;
}) {
return {
treeData,
searchInput,
onClickNode,
};
}
Usage in the container:
<script setup lang="ts">
const treePanelRef = useTemplateRef<TreeInstance | null>('treePanelRef');
const { treeData, searchInput, onClickNode } = useMyPanelTree({
focusNode: id => treePanelRef.value?.focusItem(id),
collapseAll: () => treePanelRef.value?.collapseAll(),
expandAll: () => treePanelRef.value?.expandAll(),
isExpandedAll: computed(() => treePanelRef.value?.isExpandedAll ?? false),
});
</script>
Key Constraints
- Never add business logic directly in
PrimarySideBar.vue — it is a pure switcher. All logic lives in the management panel or its hooks.
- Always use
shallowRef for the current component — ref() causes deep reactivity overhead on component objects.
- Always wrap the
<component :is="current"> in <KeepAlive> — this preserves scroll position and tree expand state when switching tabs.
- Persisted tree state (expanded keys, scroll positions) goes into
useActivityBarStore, not into local component state.
- Imports in
PrimarySideBar.vue use #components (Nuxt auto-import barrel) — not direct relative paths.
- Each management sub-module must have its own
index.ts exporting its root component, and be re-exported from components/modules/management/index.ts.