diff --git a/docs/TASKS/workspaces/001-all-workspaces-selector.md b/docs/TASKS/workspaces/001-all-workspaces-selector.md
new file mode 100644
index 0000000..e1ac853
--- /dev/null
+++ b/docs/TASKS/workspaces/001-all-workspaces-selector.md
@@ -0,0 +1,33 @@
+# All Workspaces Selector
+
+## Feature
+
+Workspace navigation and cross-workspace content visibility.
+
+## Goal
+
+Allow users with access to multiple workspaces to select an "All Workspaces" scope from the workspace selector and view combined workspace data in list/calendar style views.
+
+## Scope
+
+- Add an explicit all-workspaces selection state to the frontend workspace store.
+- Add an "All Workspaces" entry as the first workspace selector item.
+- Add per-workspace visibility toggles for the all-workspaces aggregate scope.
+- Fetch list data without `workspaceId` when all workspaces are selected.
+- Filter all-workspaces list data to the currently visible workspace set.
+- Keep creation and workspace settings actions scoped to a concrete workspace.
+
+## Likely Files
+
+- `frontend/src/features/workspaces/stores/workspaceStore.js`
+- `frontend/src/layouts/main/WorkspaceSelector.vue`
+- `frontend/src/features/content/stores/contentItemsStore.js`
+- `frontend/src/features/campaigns/stores/campaignsStore.js`
+- `frontend/src/features/clients/stores/clientsStore.js`
+- `frontend/src/features/channels/stores/channelsStore.js`
+- `frontend/src/locales/en.json`
+- `frontend/src/locales/fr.json`
+
+## Validation
+
+- `cd frontend && npm run build`
diff --git a/frontend/src/features/campaigns/stores/campaignsStore.js b/frontend/src/features/campaigns/stores/campaignsStore.js
index c992bca..894e489 100644
--- a/frontend/src/features/campaigns/stores/campaignsStore.js
+++ b/frontend/src/features/campaigns/stores/campaignsStore.js
@@ -15,7 +15,7 @@ export const useCampaignsStore = defineStore('campaigns', () => {
const error = ref(null);
async function fetchCampaigns() {
- if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
+ if (!authStore.isAuthenticated) {
campaigns.value = [];
error.value = null;
return;
@@ -27,11 +27,13 @@ export const useCampaignsStore = defineStore('campaigns', () => {
try {
const response = await client.get('/api/campaigns', {
params: {
- workspaceId: workspaceStore.activeWorkspaceId,
+ workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
},
});
- campaigns.value = response.data ?? [];
+ campaigns.value = (response.data ?? []).filter(campaign =>
+ workspaceStore.isWorkspaceVisible(campaign.workspaceId)
+ );
} catch (fetchError) {
console.error('Failed to fetch campaigns:', fetchError);
campaigns.value = [];
@@ -75,9 +77,9 @@ export const useCampaignsStore = defineStore('campaigns', () => {
}
watch(
- () => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
- async ([isAuthenticated, workspaceId]) => {
- if (!isAuthenticated || !workspaceId) {
+ () => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
+ async ([isAuthenticated]) => {
+ if (!isAuthenticated) {
campaigns.value = [];
error.value = null;
return;
diff --git a/frontend/src/features/channels/stores/channelsStore.js b/frontend/src/features/channels/stores/channelsStore.js
index 205101a..e6b04fe 100644
--- a/frontend/src/features/channels/stores/channelsStore.js
+++ b/frontend/src/features/channels/stores/channelsStore.js
@@ -14,6 +14,7 @@ export const useChannelsStore = defineStore('channels', () => {
const isCreating = ref(false);
const error = ref(null);
const loadedWorkspaceId = ref(null);
+ const allWorkspacesKey = '__all__';
const availableNetworks = [
'Instagram',
@@ -28,15 +29,16 @@ export const useChannelsStore = defineStore('channels', () => {
async function fetchChannels({ force = false } = {}) {
const currentWorkspaceId = workspaceStore.activeWorkspaceId;
+ const currentScopeKey = currentWorkspaceId ?? workspaceStore.workspaceScopeKey ?? allWorkspacesKey;
- if (!authStore.isAuthenticated || !currentWorkspaceId) {
+ if (!authStore.isAuthenticated) {
channels.value = [];
error.value = null;
loadedWorkspaceId.value = null;
return;
}
- if (!force && loadedWorkspaceId.value === currentWorkspaceId) {
+ if (!force && loadedWorkspaceId.value === currentScopeKey) {
return;
}
@@ -46,12 +48,14 @@ export const useChannelsStore = defineStore('channels', () => {
try {
const response = await client.get('/api/channels', {
params: {
- workspaceId: currentWorkspaceId,
+ workspaceId: currentWorkspaceId ?? undefined,
},
});
- channels.value = response.data ?? [];
- loadedWorkspaceId.value = currentWorkspaceId;
+ channels.value = (response.data ?? []).filter(channel =>
+ workspaceStore.isWorkspaceVisible(channel.workspaceId)
+ );
+ loadedWorkspaceId.value = currentScopeKey;
} catch (fetchError) {
console.error('Failed to fetch channels:', fetchError);
channels.value = [];
@@ -101,9 +105,9 @@ export const useChannelsStore = defineStore('channels', () => {
}
watch(
- () => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
- async ([isAuthenticated, workspaceId]) => {
- if (!isAuthenticated || !workspaceId) {
+ () => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
+ async ([isAuthenticated]) => {
+ if (!isAuthenticated) {
channels.value = [];
error.value = null;
loadedWorkspaceId.value = null;
diff --git a/frontend/src/features/channels/views/ChannelsView.vue b/frontend/src/features/channels/views/ChannelsView.vue
index ebe8c07..499a408 100644
--- a/frontend/src/features/channels/views/ChannelsView.vue
+++ b/frontend/src/features/channels/views/ChannelsView.vue
@@ -47,10 +47,12 @@
.filter(channel => channel.network)
.map(channel => {
const metrics = buildMetrics(channel.name);
+ const workspace = workspaceStore.workspaces.find(candidate => candidate.id === channel.workspaceId);
return {
...channel,
...metrics,
+ workspaceName: workspace?.name ?? t('nav.noWorkspace'),
};
})
);
@@ -215,7 +217,7 @@
>
diff --git a/frontend/src/features/clients/stores/clientsStore.js b/frontend/src/features/clients/stores/clientsStore.js
index 7dd09f8..ce9e9a5 100644
--- a/frontend/src/features/clients/stores/clientsStore.js
+++ b/frontend/src/features/clients/stores/clientsStore.js
@@ -25,7 +25,7 @@ export const useClientsStore = defineStore('clients', () => {
});
async function fetchClients() {
- if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
+ if (!authStore.isAuthenticated) {
clients.value = [];
error.value = null;
return;
@@ -37,11 +37,13 @@ export const useClientsStore = defineStore('clients', () => {
try {
const response = await client.get('/api/clients', {
params: {
- workspaceId: workspaceStore.activeWorkspaceId,
+ workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
},
});
- clients.value = response.data ?? [];
+ clients.value = (response.data ?? []).filter(candidate =>
+ workspaceStore.isWorkspaceVisible(candidate.workspaceId)
+ );
} catch (fetchError) {
console.error('Failed to fetch clients:', fetchError);
clients.value = [];
@@ -153,9 +155,9 @@ export const useClientsStore = defineStore('clients', () => {
}
watch(
- () => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
- async ([isAuthenticated, workspaceId]) => {
- if (!isAuthenticated || !workspaceId) {
+ () => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
+ async ([isAuthenticated]) => {
+ if (!isAuthenticated) {
clients.value = [];
error.value = null;
return;
diff --git a/frontend/src/features/content/stores/contentItemDetailStore.js b/frontend/src/features/content/stores/contentItemDetailStore.js
index 7b985cc..f83ac94 100644
--- a/frontend/src/features/content/stores/contentItemDetailStore.js
+++ b/frontend/src/features/content/stores/contentItemDetailStore.js
@@ -24,6 +24,10 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
status: false,
});
+ function currentItemWorkspaceId() {
+ return item.value?.workspaceId ?? workspaceStore.activeWorkspaceId;
+ }
+
function reset() {
item.value = null;
revisions.value = [];
@@ -54,7 +58,7 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
client.get('/api/approvals', { params: { contentItemId } }),
client.get('/api/notifications', {
params: {
- workspaceId: workspaceStore.activeWorkspaceId,
+ workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
contentItemId,
},
}),
@@ -97,7 +101,7 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
const response = await client.post('/api/assets/google-drive', {
...payload,
contentItemId,
- workspaceId: workspaceStore.activeWorkspaceId,
+ workspaceId: currentItemWorkspaceId(),
});
if (response.data) {
assets.value = [...assets.value, response.data];
@@ -131,7 +135,7 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
const response = await client.post('/api/comments', {
...payload,
contentItemId,
- workspaceId: workspaceStore.activeWorkspaceId,
+ workspaceId: currentItemWorkspaceId(),
});
if (response.data) {
comments.value = [...comments.value, response.data];
@@ -202,7 +206,7 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
async function fetchNotifications(contentItemId) {
const response = await client.get('/api/notifications', {
params: {
- workspaceId: workspaceStore.activeWorkspaceId,
+ workspaceId: currentItemWorkspaceId() ?? undefined,
contentItemId,
},
});
diff --git a/frontend/src/features/content/stores/contentItemsStore.js b/frontend/src/features/content/stores/contentItemsStore.js
index d94c6a5..4274292 100644
--- a/frontend/src/features/content/stores/contentItemsStore.js
+++ b/frontend/src/features/content/stores/contentItemsStore.js
@@ -20,7 +20,7 @@ export const useContentItemsStore = defineStore('content-items', () => {
);
async function fetchContentItems(filters = {}) {
- if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
+ if (!authStore.isAuthenticated) {
items.value = [];
error.value = null;
return;
@@ -32,13 +32,15 @@ export const useContentItemsStore = defineStore('content-items', () => {
try {
const response = await client.get('/api/content-items', {
params: {
- workspaceId: workspaceStore.activeWorkspaceId,
+ workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
clientId: filters.clientId,
campaignId: filters.campaignId,
},
});
- items.value = response.data ?? [];
+ items.value = (response.data ?? []).filter(item =>
+ workspaceStore.isWorkspaceVisible(item.workspaceId)
+ );
} catch (fetchError) {
console.error('Failed to fetch content items:', fetchError);
items.value = [];
@@ -86,9 +88,9 @@ export const useContentItemsStore = defineStore('content-items', () => {
}
watch(
- () => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
- async ([isAuthenticated, workspaceId]) => {
- if (!isAuthenticated || !workspaceId) {
+ () => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
+ async ([isAuthenticated]) => {
+ if (!isAuthenticated) {
items.value = [];
error.value = null;
return;
diff --git a/frontend/src/features/workspaces/stores/workspaceStore.js b/frontend/src/features/workspaces/stores/workspaceStore.js
index 654ad29..f52d506 100644
--- a/frontend/src/features/workspaces/stores/workspaceStore.js
+++ b/frontend/src/features/workspaces/stores/workspaceStore.js
@@ -11,6 +11,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
const workspaces = ref([]);
const activeWorkspaceId = ref(null);
+ const visibleWorkspaceIds = ref([]);
const isLoading = ref(false);
const isCreating = ref(false);
const isUpdating = ref(false);
@@ -25,11 +26,43 @@ export const useWorkspaceStore = defineStore('workspace', () => {
const activeWorkspace = computed(() =>
workspaces.value.find(workspace => workspace.id === activeWorkspaceId.value) ?? null
);
+ const isAllWorkspacesSelected = computed(() =>
+ activeWorkspaceId.value === null && workspaces.value.length > 1
+ );
+ const visibleWorkspaceCount = computed(() =>
+ activeWorkspaceId.value ? 1 : visibleWorkspaceIds.value.length
+ );
+ const areAllWorkspacesVisible = computed(() =>
+ activeWorkspaceId.value === null &&
+ workspaces.value.length > 1 &&
+ visibleWorkspaceIds.value.length === workspaces.value.length
+ );
+ const visibleWorkspaceIdSet = computed(() => new Set(visibleWorkspaceIds.value));
+ const workspaceScopeKey = computed(() =>
+ activeWorkspaceId.value ?? visibleWorkspaceIds.value.slice().sort().join(',')
+ );
+
+ function allWorkspaceIds() {
+ return workspaces.value.map(workspace => workspace.id);
+ }
+
+ function normalizeVisibleWorkspaces() {
+ const workspaceIds = allWorkspaceIds();
+ const knownWorkspaceIds = new Set(workspaceIds);
+ const nextVisibleWorkspaceIds = visibleWorkspaceIds.value.filter(workspaceId =>
+ knownWorkspaceIds.has(workspaceId)
+ );
+
+ visibleWorkspaceIds.value = nextVisibleWorkspaceIds.length > 0
+ ? nextVisibleWorkspaceIds
+ : workspaceIds;
+ }
async function fetchWorkspaces() {
if (!authStore.isAuthenticated) {
workspaces.value = [];
activeWorkspaceId.value = null;
+ visibleWorkspaceIds.value = [];
error.value = null;
return;
}
@@ -40,9 +73,10 @@ export const useWorkspaceStore = defineStore('workspace', () => {
try {
const response = await client.get('/api/workspaces');
workspaces.value = response.data ?? [];
+ normalizeVisibleWorkspaces();
if (!workspaces.value.some(workspace => workspace.id === activeWorkspaceId.value)) {
- activeWorkspaceId.value = workspaces.value[0]?.id ?? null;
+ activeWorkspaceId.value = workspaces.value.length > 1 ? null : workspaces.value[0]?.id ?? null;
}
organizationStore.setSelectedOrganizationFromWorkspace(activeWorkspace.value);
@@ -75,6 +109,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
workspaces.value = [...workspaces.value, response.data]
.sort((left, right) => left.name.localeCompare(right.name));
activeWorkspaceId.value = response.data.id;
+ visibleWorkspaceIds.value = [response.data.id];
try {
await client.post('/api/clients', {
@@ -172,10 +207,62 @@ export const useWorkspaceStore = defineStore('workspace', () => {
if (workspaces.value.some(workspace => workspace.id === workspaceId)) {
activeWorkspaceId.value = workspaceId;
+ visibleWorkspaceIds.value = [workspaceId];
organizationStore.setSelectedOrganizationFromWorkspace(activeWorkspace.value);
}
}
+ function setAllWorkspaces() {
+ if (workspaces.value.length > 1) {
+ activeWorkspaceId.value = null;
+ visibleWorkspaceIds.value = allWorkspaceIds();
+ }
+ }
+
+ function isWorkspaceVisible(workspaceId) {
+ if (activeWorkspaceId.value) {
+ return workspaceId === activeWorkspaceId.value;
+ }
+
+ if (visibleWorkspaceIds.value.length === 0) {
+ return true;
+ }
+
+ return visibleWorkspaceIdSet.value.has(workspaceId);
+ }
+
+ function toggleWorkspaceVisibility(workspaceId) {
+ if (!workspaces.value.some(workspace => workspace.id === workspaceId)) {
+ return;
+ }
+
+ const wasFocusedOnSingleWorkspace = Boolean(activeWorkspaceId.value);
+ activeWorkspaceId.value = null;
+ const visibleIds = new Set(
+ wasFocusedOnSingleWorkspace || visibleWorkspaceIds.value.length === 0
+ ? allWorkspaceIds()
+ : visibleWorkspaceIds.value
+ );
+
+ if (visibleIds.has(workspaceId)) {
+ visibleIds.delete(workspaceId);
+ } else {
+ visibleIds.add(workspaceId);
+ }
+
+ if (visibleIds.size === 0) {
+ visibleIds.add(workspaceId);
+ }
+
+ const nextVisibleWorkspaceIds = allWorkspaceIds().filter(id => visibleIds.has(id));
+ if (nextVisibleWorkspaceIds.length === 1) {
+ setActiveWorkspace(nextVisibleWorkspaceIds[0]);
+ return;
+ }
+
+ visibleWorkspaceIds.value = nextVisibleWorkspaceIds;
+ }
+
async function fetchInvites(workspaceId = activeWorkspaceId.value) {
if (!authStore.isAuthenticated || !workspaceId) {
invitesByWorkspace.value = {};
@@ -257,6 +344,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
if (!isAuthenticated) {
workspaces.value = [];
activeWorkspaceId.value = null;
+ visibleWorkspaceIds.value = [];
error.value = null;
return;
}
@@ -270,6 +358,11 @@ export const useWorkspaceStore = defineStore('workspace', () => {
workspaces,
activeWorkspaceId,
activeWorkspace,
+ visibleWorkspaceIds,
+ isAllWorkspacesSelected,
+ visibleWorkspaceCount,
+ areAllWorkspacesVisible,
+ workspaceScopeKey,
isLoading,
isCreating,
isUpdating,
@@ -288,5 +381,8 @@ export const useWorkspaceStore = defineStore('workspace', () => {
fetchMembers,
inviteMember,
setActiveWorkspace,
+ setAllWorkspaces,
+ isWorkspaceVisible,
+ toggleWorkspaceVisibility,
};
});
diff --git a/frontend/src/layouts/main/WorkspaceSelector.vue b/frontend/src/layouts/main/WorkspaceSelector.vue
index 5f2fcb0..7131c6a 100644
--- a/frontend/src/layouts/main/WorkspaceSelector.vue
+++ b/frontend/src/layouts/main/WorkspaceSelector.vue
@@ -12,6 +12,8 @@
import {
mdiChevronDown,
mdiCogOutline,
+ mdiEyeOffOutline,
+ mdiEyeOutline,
mdiPlus,
mdiSwapHorizontal,
} from '@mdi/js';
@@ -37,6 +39,7 @@
);
});
const canSwitchWorkspaces = computed(() => visibleWorkspaces.value.length > 1);
+ const canSelectAllWorkspaces = computed(() => visibleWorkspaces.value.length > 1);
const canSwitchOrganizations = computed(() => organizationStore.organizations.length > 1);
const switchableOrganizations = computed(() =>
organizationStore.organizations.filter(
@@ -51,8 +54,19 @@
const canOpenWorkspaceMenu = computed(() =>
canSwitchWorkspaces.value || canSwitchOrganizations.value || canManageWorkspaces.value || Boolean(activeOrganization.value)
);
- const activeWorkspaceName = computed(() =>
- workspaceStore.activeWorkspace?.name || t('nav.noWorkspace')
+ const activeWorkspaceName = computed(() => {
+ if (workspaceStore.areAllWorkspacesVisible) {
+ return t('workspaceSelector.allWorkspaces');
+ }
+
+ if (workspaceStore.isAllWorkspacesSelected) {
+ return t('workspaceSelector.multipleWorkspaces');
+ }
+
+ return workspaceStore.activeWorkspace?.name || t('nav.noWorkspace');
+ });
+ const activeWorkspaceLogoUrl = computed(() =>
+ workspaceStore.isAllWorkspacesSelected ? null : workspaceStore.activeWorkspace?.logoUrl
);
const activeOrganizationName = computed(() =>
activeOrganization.value?.name || t('workspaceSelector.noOrganization')
@@ -73,13 +87,30 @@
isOrganizationListOpen.value = false;
}
+ function chooseAllWorkspaces() {
+ workspaceStore.setAllWorkspaces();
+ isWorkspaceMenuOpen.value = false;
+ isOrganizationListOpen.value = false;
+ }
+
+ function toggleWorkspaceVisibility(workspaceId) {
+ workspaceStore.toggleWorkspaceVisibility(workspaceId);
+ }
+
function chooseOrganization(organizationId) {
organizationStore.setSelectedOrganization(organizationId);
const nextWorkspace = workspaceStore.workspaces.find(
workspace => workspace.organizationId === organizationId
);
- workspaceStore.setActiveWorkspace(nextWorkspace?.id ?? null);
+ const organizationWorkspaceCount = workspaceStore.workspaces.filter(
+ workspace => workspace.organizationId === organizationId
+ ).length;
+ if (organizationWorkspaceCount > 1) {
+ workspaceStore.setAllWorkspaces();
+ } else {
+ workspaceStore.setActiveWorkspace(nextWorkspace?.id ?? null);
+ }
isOrganizationListOpen.value = false;
}
@@ -137,7 +168,7 @@
>
{{ activeWorkspaceName }}
@@ -153,11 +184,31 @@
v-if="isWorkspaceMenuOpen"
class="user-menu"
>
+
+