Add multi-workspace selector scope
This commit is contained in:
33
docs/TASKS/workspaces/001-all-workspaces-selector.md
Normal file
33
docs/TASKS/workspaces/001-all-workspaces-selector.md
Normal file
@@ -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`
|
||||||
@@ -15,7 +15,7 @@ export const useCampaignsStore = defineStore('campaigns', () => {
|
|||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
|
|
||||||
async function fetchCampaigns() {
|
async function fetchCampaigns() {
|
||||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
if (!authStore.isAuthenticated) {
|
||||||
campaigns.value = [];
|
campaigns.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
@@ -27,11 +27,13 @@ export const useCampaignsStore = defineStore('campaigns', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await client.get('/api/campaigns', {
|
const response = await client.get('/api/campaigns', {
|
||||||
params: {
|
params: {
|
||||||
workspaceId: workspaceStore.activeWorkspaceId,
|
workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
campaigns.value = response.data ?? [];
|
campaigns.value = (response.data ?? []).filter(campaign =>
|
||||||
|
workspaceStore.isWorkspaceVisible(campaign.workspaceId)
|
||||||
|
);
|
||||||
} catch (fetchError) {
|
} catch (fetchError) {
|
||||||
console.error('Failed to fetch campaigns:', fetchError);
|
console.error('Failed to fetch campaigns:', fetchError);
|
||||||
campaigns.value = [];
|
campaigns.value = [];
|
||||||
@@ -75,9 +77,9 @@ export const useCampaignsStore = defineStore('campaigns', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
() => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
|
||||||
async ([isAuthenticated, workspaceId]) => {
|
async ([isAuthenticated]) => {
|
||||||
if (!isAuthenticated || !workspaceId) {
|
if (!isAuthenticated) {
|
||||||
campaigns.value = [];
|
campaigns.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const useChannelsStore = defineStore('channels', () => {
|
|||||||
const isCreating = ref(false);
|
const isCreating = ref(false);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
const loadedWorkspaceId = ref(null);
|
const loadedWorkspaceId = ref(null);
|
||||||
|
const allWorkspacesKey = '__all__';
|
||||||
|
|
||||||
const availableNetworks = [
|
const availableNetworks = [
|
||||||
'Instagram',
|
'Instagram',
|
||||||
@@ -28,15 +29,16 @@ export const useChannelsStore = defineStore('channels', () => {
|
|||||||
|
|
||||||
async function fetchChannels({ force = false } = {}) {
|
async function fetchChannels({ force = false } = {}) {
|
||||||
const currentWorkspaceId = workspaceStore.activeWorkspaceId;
|
const currentWorkspaceId = workspaceStore.activeWorkspaceId;
|
||||||
|
const currentScopeKey = currentWorkspaceId ?? workspaceStore.workspaceScopeKey ?? allWorkspacesKey;
|
||||||
|
|
||||||
if (!authStore.isAuthenticated || !currentWorkspaceId) {
|
if (!authStore.isAuthenticated) {
|
||||||
channels.value = [];
|
channels.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
loadedWorkspaceId.value = null;
|
loadedWorkspaceId.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!force && loadedWorkspaceId.value === currentWorkspaceId) {
|
if (!force && loadedWorkspaceId.value === currentScopeKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +48,14 @@ export const useChannelsStore = defineStore('channels', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await client.get('/api/channels', {
|
const response = await client.get('/api/channels', {
|
||||||
params: {
|
params: {
|
||||||
workspaceId: currentWorkspaceId,
|
workspaceId: currentWorkspaceId ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
channels.value = response.data ?? [];
|
channels.value = (response.data ?? []).filter(channel =>
|
||||||
loadedWorkspaceId.value = currentWorkspaceId;
|
workspaceStore.isWorkspaceVisible(channel.workspaceId)
|
||||||
|
);
|
||||||
|
loadedWorkspaceId.value = currentScopeKey;
|
||||||
} catch (fetchError) {
|
} catch (fetchError) {
|
||||||
console.error('Failed to fetch channels:', fetchError);
|
console.error('Failed to fetch channels:', fetchError);
|
||||||
channels.value = [];
|
channels.value = [];
|
||||||
@@ -101,9 +105,9 @@ export const useChannelsStore = defineStore('channels', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
() => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
|
||||||
async ([isAuthenticated, workspaceId]) => {
|
async ([isAuthenticated]) => {
|
||||||
if (!isAuthenticated || !workspaceId) {
|
if (!isAuthenticated) {
|
||||||
channels.value = [];
|
channels.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
loadedWorkspaceId.value = null;
|
loadedWorkspaceId.value = null;
|
||||||
|
|||||||
@@ -47,10 +47,12 @@
|
|||||||
.filter(channel => channel.network)
|
.filter(channel => channel.network)
|
||||||
.map(channel => {
|
.map(channel => {
|
||||||
const metrics = buildMetrics(channel.name);
|
const metrics = buildMetrics(channel.name);
|
||||||
|
const workspace = workspaceStore.workspaces.find(candidate => candidate.id === channel.workspaceId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...channel,
|
...channel,
|
||||||
...metrics,
|
...metrics,
|
||||||
|
workspaceName: workspace?.name ?? t('nav.noWorkspace'),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -215,7 +217,7 @@
|
|||||||
>
|
>
|
||||||
<div class="channel-header">
|
<div class="channel-header">
|
||||||
<strong>{{ channel.name }}</strong>
|
<strong>{{ channel.name }}</strong>
|
||||||
<span>{{ workspaceStore.activeWorkspace?.name || t('nav.noWorkspace') }}</span>
|
<span>{{ channel.workspaceName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="channel-metrics">
|
<div class="channel-metrics">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const useClientsStore = defineStore('clients', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function fetchClients() {
|
async function fetchClients() {
|
||||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
if (!authStore.isAuthenticated) {
|
||||||
clients.value = [];
|
clients.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
@@ -37,11 +37,13 @@ export const useClientsStore = defineStore('clients', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await client.get('/api/clients', {
|
const response = await client.get('/api/clients', {
|
||||||
params: {
|
params: {
|
||||||
workspaceId: workspaceStore.activeWorkspaceId,
|
workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
clients.value = response.data ?? [];
|
clients.value = (response.data ?? []).filter(candidate =>
|
||||||
|
workspaceStore.isWorkspaceVisible(candidate.workspaceId)
|
||||||
|
);
|
||||||
} catch (fetchError) {
|
} catch (fetchError) {
|
||||||
console.error('Failed to fetch clients:', fetchError);
|
console.error('Failed to fetch clients:', fetchError);
|
||||||
clients.value = [];
|
clients.value = [];
|
||||||
@@ -153,9 +155,9 @@ export const useClientsStore = defineStore('clients', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
() => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
|
||||||
async ([isAuthenticated, workspaceId]) => {
|
async ([isAuthenticated]) => {
|
||||||
if (!isAuthenticated || !workspaceId) {
|
if (!isAuthenticated) {
|
||||||
clients.value = [];
|
clients.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
status: false,
|
status: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function currentItemWorkspaceId() {
|
||||||
|
return item.value?.workspaceId ?? workspaceStore.activeWorkspaceId;
|
||||||
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
item.value = null;
|
item.value = null;
|
||||||
revisions.value = [];
|
revisions.value = [];
|
||||||
@@ -54,7 +58,7 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
client.get('/api/approvals', { params: { contentItemId } }),
|
client.get('/api/approvals', { params: { contentItemId } }),
|
||||||
client.get('/api/notifications', {
|
client.get('/api/notifications', {
|
||||||
params: {
|
params: {
|
||||||
workspaceId: workspaceStore.activeWorkspaceId,
|
workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
|
||||||
contentItemId,
|
contentItemId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -97,7 +101,7 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
const response = await client.post('/api/assets/google-drive', {
|
const response = await client.post('/api/assets/google-drive', {
|
||||||
...payload,
|
...payload,
|
||||||
contentItemId,
|
contentItemId,
|
||||||
workspaceId: workspaceStore.activeWorkspaceId,
|
workspaceId: currentItemWorkspaceId(),
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
assets.value = [...assets.value, 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', {
|
const response = await client.post('/api/comments', {
|
||||||
...payload,
|
...payload,
|
||||||
contentItemId,
|
contentItemId,
|
||||||
workspaceId: workspaceStore.activeWorkspaceId,
|
workspaceId: currentItemWorkspaceId(),
|
||||||
});
|
});
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
comments.value = [...comments.value, response.data];
|
comments.value = [...comments.value, response.data];
|
||||||
@@ -202,7 +206,7 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
async function fetchNotifications(contentItemId) {
|
async function fetchNotifications(contentItemId) {
|
||||||
const response = await client.get('/api/notifications', {
|
const response = await client.get('/api/notifications', {
|
||||||
params: {
|
params: {
|
||||||
workspaceId: workspaceStore.activeWorkspaceId,
|
workspaceId: currentItemWorkspaceId() ?? undefined,
|
||||||
contentItemId,
|
contentItemId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const useContentItemsStore = defineStore('content-items', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function fetchContentItems(filters = {}) {
|
async function fetchContentItems(filters = {}) {
|
||||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
if (!authStore.isAuthenticated) {
|
||||||
items.value = [];
|
items.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
@@ -32,13 +32,15 @@ export const useContentItemsStore = defineStore('content-items', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await client.get('/api/content-items', {
|
const response = await client.get('/api/content-items', {
|
||||||
params: {
|
params: {
|
||||||
workspaceId: workspaceStore.activeWorkspaceId,
|
workspaceId: workspaceStore.activeWorkspaceId ?? undefined,
|
||||||
clientId: filters.clientId,
|
clientId: filters.clientId,
|
||||||
campaignId: filters.campaignId,
|
campaignId: filters.campaignId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
items.value = response.data ?? [];
|
items.value = (response.data ?? []).filter(item =>
|
||||||
|
workspaceStore.isWorkspaceVisible(item.workspaceId)
|
||||||
|
);
|
||||||
} catch (fetchError) {
|
} catch (fetchError) {
|
||||||
console.error('Failed to fetch content items:', fetchError);
|
console.error('Failed to fetch content items:', fetchError);
|
||||||
items.value = [];
|
items.value = [];
|
||||||
@@ -86,9 +88,9 @@ export const useContentItemsStore = defineStore('content-items', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
() => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
|
||||||
async ([isAuthenticated, workspaceId]) => {
|
async ([isAuthenticated]) => {
|
||||||
if (!isAuthenticated || !workspaceId) {
|
if (!isAuthenticated) {
|
||||||
items.value = [];
|
items.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
|
|
||||||
const workspaces = ref([]);
|
const workspaces = ref([]);
|
||||||
const activeWorkspaceId = ref(null);
|
const activeWorkspaceId = ref(null);
|
||||||
|
const visibleWorkspaceIds = ref([]);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isCreating = ref(false);
|
const isCreating = ref(false);
|
||||||
const isUpdating = ref(false);
|
const isUpdating = ref(false);
|
||||||
@@ -25,11 +26,43 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
const activeWorkspace = computed(() =>
|
const activeWorkspace = computed(() =>
|
||||||
workspaces.value.find(workspace => workspace.id === activeWorkspaceId.value) ?? null
|
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() {
|
async function fetchWorkspaces() {
|
||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
workspaces.value = [];
|
workspaces.value = [];
|
||||||
activeWorkspaceId.value = null;
|
activeWorkspaceId.value = null;
|
||||||
|
visibleWorkspaceIds.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -40,9 +73,10 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await client.get('/api/workspaces');
|
const response = await client.get('/api/workspaces');
|
||||||
workspaces.value = response.data ?? [];
|
workspaces.value = response.data ?? [];
|
||||||
|
normalizeVisibleWorkspaces();
|
||||||
|
|
||||||
if (!workspaces.value.some(workspace => workspace.id === activeWorkspaceId.value)) {
|
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);
|
organizationStore.setSelectedOrganizationFromWorkspace(activeWorkspace.value);
|
||||||
@@ -75,6 +109,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
workspaces.value = [...workspaces.value, response.data]
|
workspaces.value = [...workspaces.value, response.data]
|
||||||
.sort((left, right) => left.name.localeCompare(right.name));
|
.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
activeWorkspaceId.value = response.data.id;
|
activeWorkspaceId.value = response.data.id;
|
||||||
|
visibleWorkspaceIds.value = [response.data.id];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.post('/api/clients', {
|
await client.post('/api/clients', {
|
||||||
@@ -172,10 +207,62 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
|
|
||||||
if (workspaces.value.some(workspace => workspace.id === workspaceId)) {
|
if (workspaces.value.some(workspace => workspace.id === workspaceId)) {
|
||||||
activeWorkspaceId.value = workspaceId;
|
activeWorkspaceId.value = workspaceId;
|
||||||
|
visibleWorkspaceIds.value = [workspaceId];
|
||||||
organizationStore.setSelectedOrganizationFromWorkspace(activeWorkspace.value);
|
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) {
|
async function fetchInvites(workspaceId = activeWorkspaceId.value) {
|
||||||
if (!authStore.isAuthenticated || !workspaceId) {
|
if (!authStore.isAuthenticated || !workspaceId) {
|
||||||
invitesByWorkspace.value = {};
|
invitesByWorkspace.value = {};
|
||||||
@@ -257,6 +344,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
workspaces.value = [];
|
workspaces.value = [];
|
||||||
activeWorkspaceId.value = null;
|
activeWorkspaceId.value = null;
|
||||||
|
visibleWorkspaceIds.value = [];
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -270,6 +358,11 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
workspaces,
|
workspaces,
|
||||||
activeWorkspaceId,
|
activeWorkspaceId,
|
||||||
activeWorkspace,
|
activeWorkspace,
|
||||||
|
visibleWorkspaceIds,
|
||||||
|
isAllWorkspacesSelected,
|
||||||
|
visibleWorkspaceCount,
|
||||||
|
areAllWorkspacesVisible,
|
||||||
|
workspaceScopeKey,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCreating,
|
isCreating,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
@@ -288,5 +381,8 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
fetchMembers,
|
fetchMembers,
|
||||||
inviteMember,
|
inviteMember,
|
||||||
setActiveWorkspace,
|
setActiveWorkspace,
|
||||||
|
setAllWorkspaces,
|
||||||
|
isWorkspaceVisible,
|
||||||
|
toggleWorkspaceVisibility,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
import {
|
import {
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
|
mdiEyeOffOutline,
|
||||||
|
mdiEyeOutline,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiSwapHorizontal,
|
mdiSwapHorizontal,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
const canSwitchWorkspaces = computed(() => visibleWorkspaces.value.length > 1);
|
const canSwitchWorkspaces = computed(() => visibleWorkspaces.value.length > 1);
|
||||||
|
const canSelectAllWorkspaces = computed(() => visibleWorkspaces.value.length > 1);
|
||||||
const canSwitchOrganizations = computed(() => organizationStore.organizations.length > 1);
|
const canSwitchOrganizations = computed(() => organizationStore.organizations.length > 1);
|
||||||
const switchableOrganizations = computed(() =>
|
const switchableOrganizations = computed(() =>
|
||||||
organizationStore.organizations.filter(
|
organizationStore.organizations.filter(
|
||||||
@@ -51,8 +54,19 @@
|
|||||||
const canOpenWorkspaceMenu = computed(() =>
|
const canOpenWorkspaceMenu = computed(() =>
|
||||||
canSwitchWorkspaces.value || canSwitchOrganizations.value || canManageWorkspaces.value || Boolean(activeOrganization.value)
|
canSwitchWorkspaces.value || canSwitchOrganizations.value || canManageWorkspaces.value || Boolean(activeOrganization.value)
|
||||||
);
|
);
|
||||||
const activeWorkspaceName = computed(() =>
|
const activeWorkspaceName = computed(() => {
|
||||||
workspaceStore.activeWorkspace?.name || t('nav.noWorkspace')
|
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(() =>
|
const activeOrganizationName = computed(() =>
|
||||||
activeOrganization.value?.name || t('workspaceSelector.noOrganization')
|
activeOrganization.value?.name || t('workspaceSelector.noOrganization')
|
||||||
@@ -73,13 +87,30 @@
|
|||||||
isOrganizationListOpen.value = false;
|
isOrganizationListOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseAllWorkspaces() {
|
||||||
|
workspaceStore.setAllWorkspaces();
|
||||||
|
isWorkspaceMenuOpen.value = false;
|
||||||
|
isOrganizationListOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWorkspaceVisibility(workspaceId) {
|
||||||
|
workspaceStore.toggleWorkspaceVisibility(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
function chooseOrganization(organizationId) {
|
function chooseOrganization(organizationId) {
|
||||||
organizationStore.setSelectedOrganization(organizationId);
|
organizationStore.setSelectedOrganization(organizationId);
|
||||||
|
|
||||||
const nextWorkspace = workspaceStore.workspaces.find(
|
const nextWorkspace = workspaceStore.workspaces.find(
|
||||||
workspace => workspace.organizationId === organizationId
|
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;
|
isOrganizationListOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +168,7 @@
|
|||||||
>
|
>
|
||||||
<AppAvatar
|
<AppAvatar
|
||||||
:name="activeWorkspaceName"
|
:name="activeWorkspaceName"
|
||||||
:src="workspaceStore.activeWorkspace?.logoUrl"
|
:src="activeWorkspaceLogoUrl"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<span class="label workspace-trigger-label">{{ activeWorkspaceName }}</span>
|
<span class="label workspace-trigger-label">{{ activeWorkspaceName }}</span>
|
||||||
@@ -153,11 +184,31 @@
|
|||||||
v-if="isWorkspaceMenuOpen"
|
v-if="isWorkspaceMenuOpen"
|
||||||
class="user-menu"
|
class="user-menu"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
v-if="canSelectAllWorkspaces"
|
||||||
|
class="user-menu-item all-workspaces-item"
|
||||||
|
:class="{ 'user-menu-item-active': workspaceStore.isAllWorkspacesSelected }"
|
||||||
|
type="button"
|
||||||
|
@click="chooseAllWorkspaces"
|
||||||
|
>
|
||||||
|
<AppAvatar
|
||||||
|
:name="t('workspaceSelector.allWorkspaces')"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<span class="user-menu-item-copy">
|
||||||
|
<span>{{ t('workspaceSelector.allWorkspaces') }}</span>
|
||||||
|
<small>{{ t('workspaceSelector.allWorkspacesDescription') }}</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="workspace in visibleWorkspaces"
|
v-for="workspace in visibleWorkspaces"
|
||||||
:key="workspace.id"
|
:key="workspace.id"
|
||||||
class="workspace-menu-row"
|
class="workspace-menu-row"
|
||||||
:class="{ 'user-menu-item-active': workspace.id === workspaceStore.activeWorkspaceId }"
|
:class="{
|
||||||
|
'user-menu-item-active': workspace.id === workspaceStore.activeWorkspaceId,
|
||||||
|
'workspace-menu-row-muted': workspaceStore.isAllWorkspacesSelected && !workspaceStore.isWorkspaceVisible(workspace.id),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="user-menu-item workspace-menu-select"
|
class="user-menu-item workspace-menu-select"
|
||||||
@@ -175,6 +226,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="canSelectAllWorkspaces"
|
||||||
|
class="workspace-visibility-button"
|
||||||
|
type="button"
|
||||||
|
:aria-label="workspaceStore.isWorkspaceVisible(workspace.id) ? t('workspaceSelector.hideWorkspace') : t('workspaceSelector.showWorkspace')"
|
||||||
|
@click.stop="toggleWorkspaceVisibility(workspace.id)"
|
||||||
|
>
|
||||||
|
<v-icon :icon="workspaceStore.isWorkspaceVisible(workspace.id) ? mdiEyeOutline : mdiEyeOffOutline" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="canManageWorkspaces"
|
v-if="canManageWorkspaces"
|
||||||
class="workspace-settings-button"
|
class="workspace-settings-button"
|
||||||
@@ -326,6 +387,16 @@
|
|||||||
color: #172033;
|
color: #172033;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-menu-row-muted {
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-workspaces-item {
|
||||||
|
@apply mb-1 border;
|
||||||
|
border-color: rgba(23, 32, 51, 0.08);
|
||||||
|
background: rgba(23, 32, 51, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-menu-select {
|
.workspace-menu-select {
|
||||||
@apply min-w-0 flex-1;
|
@apply min-w-0 flex-1;
|
||||||
}
|
}
|
||||||
@@ -339,11 +410,18 @@
|
|||||||
color: #526178;
|
color: #526178;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-visibility-button {
|
||||||
|
@apply flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full transition-colors;
|
||||||
|
color: #526178;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-visibility-button:hover,
|
||||||
.workspace-settings-button:hover {
|
.workspace-settings-button:hover {
|
||||||
background: rgba(23, 32, 51, 0.1);
|
background: rgba(23, 32, 51, 0.1);
|
||||||
color: #172033;
|
color: #172033;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-visibility-button :deep(.v-icon),
|
||||||
.workspace-settings-button :deep(.v-icon) {
|
.workspace-settings-button :deep(.v-icon) {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,10 +360,15 @@
|
|||||||
"saving": "Saving..."
|
"saving": "Saving..."
|
||||||
},
|
},
|
||||||
"workspaceSelector": {
|
"workspaceSelector": {
|
||||||
|
"allWorkspaces": "All Workspaces",
|
||||||
|
"allWorkspacesDescription": "Show every workspace",
|
||||||
"createAction": "Add workspace",
|
"createAction": "Add workspace",
|
||||||
|
"hideWorkspace": "Hide workspace",
|
||||||
|
"multipleWorkspaces": "Multiple Workspaces",
|
||||||
"organizationLabel": "Organization",
|
"organizationLabel": "Organization",
|
||||||
"organizationSettings": "Organization settings",
|
"organizationSettings": "Organization settings",
|
||||||
"noOrganization": "No organization",
|
"noOrganization": "No organization",
|
||||||
|
"showWorkspace": "Show workspace",
|
||||||
"workspaceSettings": "Workspace settings"
|
"workspaceSettings": "Workspace settings"
|
||||||
},
|
},
|
||||||
"workspaceCreate": {
|
"workspaceCreate": {
|
||||||
|
|||||||
@@ -360,10 +360,15 @@
|
|||||||
"saving": "Enregistrement..."
|
"saving": "Enregistrement..."
|
||||||
},
|
},
|
||||||
"workspaceSelector": {
|
"workspaceSelector": {
|
||||||
|
"allWorkspaces": "Tous les espaces",
|
||||||
|
"allWorkspacesDescription": "Afficher tous les espaces",
|
||||||
"createAction": "Ajouter un espace",
|
"createAction": "Ajouter un espace",
|
||||||
|
"hideWorkspace": "Masquer l'espace",
|
||||||
|
"multipleWorkspaces": "Plusieurs espaces",
|
||||||
"organizationLabel": "Organisation",
|
"organizationLabel": "Organisation",
|
||||||
"organizationSettings": "Parametres de l'organisation",
|
"organizationSettings": "Parametres de l'organisation",
|
||||||
"noOrganization": "Aucune organisation",
|
"noOrganization": "Aucune organisation",
|
||||||
|
"showWorkspace": "Afficher l'espace",
|
||||||
"workspaceSettings": "Parametres de l'espace"
|
"workspaceSettings": "Parametres de l'espace"
|
||||||
},
|
},
|
||||||
"workspaceCreate": {
|
"workspaceCreate": {
|
||||||
|
|||||||
Reference in New Issue
Block a user