feat: pivot to social media workflow app
This commit is contained in:
@@ -24,6 +24,20 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value);
|
||||
const userId = computed(() => tokenClaims.value?.sub);
|
||||
const userRoles = computed(() => {
|
||||
const claims = tokenClaims.value ?? {};
|
||||
const candidates = [
|
||||
claims.role,
|
||||
claims.roles,
|
||||
claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'],
|
||||
].flatMap(value => Array.isArray(value) ? value : value ? [value] : []);
|
||||
|
||||
return [...new Set(candidates)];
|
||||
});
|
||||
const persona = computed(() => tokenClaims.value?.persona ?? null);
|
||||
const isManager = computed(() => userRoles.value.includes('Administrator') || userRoles.value.includes('Manager'));
|
||||
const isClient = computed(() => userRoles.value.includes('Client'));
|
||||
const isProvider = computed(() => userRoles.value.includes('Provider'));
|
||||
|
||||
function updateTokens(data) {
|
||||
if (!data?.accessToken || !data?.refreshToken) {
|
||||
@@ -259,11 +273,21 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function hasAnyRole(roles) {
|
||||
return roles.some(role => userRoles.value.includes(role));
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
isAuthenticated,
|
||||
userId,
|
||||
userRoles,
|
||||
persona,
|
||||
hasAnyRole,
|
||||
isManager,
|
||||
isClient,
|
||||
isProvider,
|
||||
isRefreshing,
|
||||
login,
|
||||
loginWithGoogle,
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useSessionStorage} from "@vueuse/core";
|
||||
import {ref, watch} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
export const useBrandingStore = defineStore(
|
||||
'branding',
|
||||
() => {
|
||||
|
||||
const currentBrand = ref(undefined)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
const notFound = ref(false)
|
||||
|
||||
const value = useSessionStorage(
|
||||
'branding',
|
||||
{},
|
||||
{writeDefaults: false})
|
||||
|
||||
const presentationInfos = ref([])
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
watch(
|
||||
() => route.params.creator,
|
||||
async (creator) => {
|
||||
// Extract just the creator name from the path (remove any additional segments)
|
||||
const creatorName = creator ? creator.split('/')[0] : undefined;
|
||||
await updateBrand(creatorName);
|
||||
}
|
||||
)
|
||||
|
||||
async function updateBrand(newBrand) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
notFound.value = false
|
||||
|
||||
if (newBrand !== currentBrand.value) {
|
||||
if (newBrand !== undefined) {
|
||||
const result = await fetchCreatorData(newBrand)
|
||||
if (result.success) {
|
||||
value.value = result.data
|
||||
currentBrand.value = newBrand
|
||||
presentationInfos.value = result.data?.presentationInfos
|
||||
} else {
|
||||
// Handle different error types
|
||||
if (result.status === 404) {
|
||||
notFound.value = true
|
||||
error.value = 'Creator not found'
|
||||
} else {
|
||||
error.value = result.error || 'Failed to load creator'
|
||||
}
|
||||
value.value = {}
|
||||
currentBrand.value = undefined
|
||||
presentationInfos.value = []
|
||||
}
|
||||
} else {
|
||||
value.value = {}
|
||||
currentBrand.value = undefined
|
||||
presentationInfos.value = []
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const fetchCreatorData = async (creatorAlias) => {
|
||||
try {
|
||||
const client = useClient()
|
||||
const response = await client.get(`/api/creators/@${creatorAlias}`)
|
||||
return { success: true, data: response.data }
|
||||
} catch (error) {
|
||||
console.error('Error fetching creator data:', error)
|
||||
|
||||
if (error.response?.status === 404) {
|
||||
return { success: false, status: 404, error: 'Creator not found' }
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: error.response?.status || 500,
|
||||
error: error.message || 'Unknown error occurred'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentBrand,
|
||||
value,
|
||||
loading,
|
||||
error,
|
||||
notFound,
|
||||
updateBrand,
|
||||
presentationInfos
|
||||
}
|
||||
})
|
||||
122
frontend/src/stores/channelsStore.js
Normal file
122
frontend/src/stores/channelsStore.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { computed } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
|
||||
import { useContentItemsStore } from '@/stores/contentItemsStore.js';
|
||||
|
||||
export const useChannelsStore = defineStore('channels', () => {
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const contentItemsStore = useContentItemsStore();
|
||||
const customChannelsByWorkspace = useSessionStorage('workspace-custom-channels', {}, {
|
||||
serializer: {
|
||||
read: value => (value ? JSON.parse(value) : {}),
|
||||
write: value => JSON.stringify(value ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
const channels = computed(() => {
|
||||
const currentWorkspaceId = workspaceStore.activeWorkspaceId;
|
||||
|
||||
if (!currentWorkspaceId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const derivedChannels = new Map();
|
||||
const customChannels = customChannelsByWorkspace.value[currentWorkspaceId] ?? [];
|
||||
|
||||
for (const item of contentItemsStore.items) {
|
||||
for (const name of parseTargets(item.publicationTargets)) {
|
||||
const key = slugify(name);
|
||||
const existing = derivedChannels.get(key) ?? {
|
||||
id: key,
|
||||
name,
|
||||
network: null,
|
||||
source: 'derived',
|
||||
};
|
||||
|
||||
derivedChannels.set(key, existing);
|
||||
}
|
||||
}
|
||||
|
||||
for (const channel of customChannels) {
|
||||
derivedChannels.set(channel.id, {
|
||||
...channel,
|
||||
source: 'custom',
|
||||
});
|
||||
}
|
||||
|
||||
return [...derivedChannels.values()].sort((left, right) => left.name.localeCompare(right.name));
|
||||
});
|
||||
|
||||
const availableNetworks = [
|
||||
'Instagram',
|
||||
'TikTok',
|
||||
'Facebook',
|
||||
'LinkedIn',
|
||||
'YouTube',
|
||||
'X',
|
||||
'Reddit',
|
||||
'Website',
|
||||
];
|
||||
|
||||
function createChannel(payload) {
|
||||
const currentWorkspaceId = workspaceStore.activeWorkspaceId;
|
||||
|
||||
if (!currentWorkspaceId) {
|
||||
throw new Error('An active workspace is required to create a channel.');
|
||||
}
|
||||
|
||||
const normalizedName = payload.name.trim();
|
||||
const normalizedNetwork = payload.network.trim();
|
||||
|
||||
if (!normalizedName) {
|
||||
throw new Error('Channel name is required.');
|
||||
}
|
||||
|
||||
if (!normalizedNetwork) {
|
||||
throw new Error('Network is required.');
|
||||
}
|
||||
|
||||
if (!availableNetworks.includes(normalizedNetwork)) {
|
||||
throw new Error('Selected network is invalid.');
|
||||
}
|
||||
|
||||
const existing = channels.value.some(channel =>
|
||||
channel.name.toLowerCase() === normalizedName.toLowerCase()
|
||||
&& (channel.network ?? '').toLowerCase() === normalizedNetwork.toLowerCase()
|
||||
);
|
||||
if (existing) {
|
||||
throw new Error('A channel with this name already exists for the selected network.');
|
||||
}
|
||||
|
||||
const next = customChannelsByWorkspace.value[currentWorkspaceId] ?? [];
|
||||
customChannelsByWorkspace.value = {
|
||||
...customChannelsByWorkspace.value,
|
||||
[currentWorkspaceId]: [
|
||||
...next,
|
||||
{
|
||||
id: slugify(`${normalizedNetwork}-${normalizedName}`),
|
||||
name: normalizedName,
|
||||
network: normalizedNetwork,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function parseTargets(value) {
|
||||
return (value ?? '')
|
||||
.split(/[,\n]+/)
|
||||
.map(target => target.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
return {
|
||||
availableNetworks,
|
||||
channels,
|
||||
createChannel,
|
||||
};
|
||||
});
|
||||
182
frontend/src/stores/clientsStore.js
Normal file
182
frontend/src/stores/clientsStore.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAuthStore } from '@/stores/authStore.js';
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useClientsStore = defineStore('clients', () => {
|
||||
const authStore = useAuthStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const client = useClient();
|
||||
|
||||
const clients = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const isUpdating = ref(false);
|
||||
const isUploadingPortrait = ref(false);
|
||||
const error = ref(null);
|
||||
const operationalClient = computed(() => {
|
||||
if (!clients.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return clients.value.find(candidate => candidate.name === workspaceStore.activeWorkspace?.name)
|
||||
?? clients.value[0];
|
||||
});
|
||||
|
||||
async function fetchClients() {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
clients.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.get('/api/clients', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
clients.value = response.data ?? [];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch clients:', fetchError);
|
||||
clients.value = [];
|
||||
error.value = 'Failed to load clients.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createClient(payload) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
throw new Error('You must be authenticated to create a client.');
|
||||
}
|
||||
|
||||
if (isCreating.value) {
|
||||
throw new Error('A client creation request is already in progress.');
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/clients', {
|
||||
...payload,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
clients.value = [...clients.value, response.data]
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (createError) {
|
||||
console.error('Failed to create client:', createError);
|
||||
error.value = 'Failed to create client.';
|
||||
throw createError;
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateClient(clientId, payload) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
throw new Error('You must be authenticated to update a client.');
|
||||
}
|
||||
|
||||
if (isUpdating.value) {
|
||||
throw new Error('A client update request is already in progress.');
|
||||
}
|
||||
|
||||
isUpdating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.put(`/api/clients/${clientId}`, payload);
|
||||
|
||||
if (response.data) {
|
||||
clients.value = clients.value
|
||||
.map(candidate => candidate.id === clientId ? response.data : candidate)
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (updateError) {
|
||||
console.error('Failed to update client:', updateError);
|
||||
error.value = 'Failed to update client.';
|
||||
throw updateError;
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadClientPortrait(clientId, file) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
throw new Error('You must be authenticated to upload a client logo.');
|
||||
}
|
||||
|
||||
if (isUploadingPortrait.value) {
|
||||
throw new Error('A client logo upload is already in progress.');
|
||||
}
|
||||
|
||||
isUploadingPortrait.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name || 'client-logo.png');
|
||||
|
||||
const response = await client.post(`/api/clients/${clientId}/portrait`, formData);
|
||||
const blobUrl = response.data?.blobUrl;
|
||||
|
||||
if (blobUrl) {
|
||||
clients.value = clients.value.map(candidate =>
|
||||
candidate.id === clientId
|
||||
? { ...candidate, portraitUrl: `${blobUrl}?${Date.now()}` }
|
||||
: candidate
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (uploadError) {
|
||||
console.error('Failed to upload client logo:', uploadError);
|
||||
error.value = 'Failed to upload client logo.';
|
||||
throw uploadError;
|
||||
} finally {
|
||||
isUploadingPortrait.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
||||
async ([isAuthenticated, workspaceId]) => {
|
||||
if (!isAuthenticated || !workspaceId) {
|
||||
clients.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchClients();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
clients,
|
||||
operationalClient,
|
||||
isLoading,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
isUploadingPortrait,
|
||||
error,
|
||||
fetchClients,
|
||||
createClient,
|
||||
updateClient,
|
||||
uploadClientPortrait,
|
||||
};
|
||||
});
|
||||
255
frontend/src/stores/contentItemDetailStore.js
Normal file
255
frontend/src/stores/contentItemDetailStore.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useContentItemDetailStore = defineStore('content-item-detail', () => {
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const client = useClient();
|
||||
|
||||
const item = ref(null);
|
||||
const revisions = ref([]);
|
||||
const assets = ref([]);
|
||||
const comments = ref([]);
|
||||
const approvals = ref([]);
|
||||
const notifications = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
const actions = reactive({
|
||||
revision: false,
|
||||
asset: false,
|
||||
assetRevision: false,
|
||||
comment: false,
|
||||
approval: false,
|
||||
decision: false,
|
||||
status: false,
|
||||
});
|
||||
|
||||
function reset() {
|
||||
item.value = null;
|
||||
revisions.value = [];
|
||||
assets.value = [];
|
||||
comments.value = [];
|
||||
approvals.value = [];
|
||||
notifications.value = [];
|
||||
error.value = null;
|
||||
}
|
||||
|
||||
async function fetchContentItemDetail(contentItemId) {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const [
|
||||
itemResponse,
|
||||
revisionsResponse,
|
||||
assetsResponse,
|
||||
commentsResponse,
|
||||
approvalsResponse,
|
||||
notificationsResponse,
|
||||
] = await Promise.all([
|
||||
client.get(`/api/content-items/${contentItemId}`),
|
||||
client.get(`/api/content-items/${contentItemId}/revisions`),
|
||||
client.get('/api/assets', { params: { contentItemId } }),
|
||||
client.get('/api/comments', { params: { contentItemId } }),
|
||||
client.get('/api/approvals', { params: { contentItemId } }),
|
||||
client.get('/api/notifications', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
contentItemId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
item.value = itemResponse.data;
|
||||
revisions.value = revisionsResponse.data ?? [];
|
||||
assets.value = assetsResponse.data ?? [];
|
||||
comments.value = commentsResponse.data ?? [];
|
||||
approvals.value = approvalsResponse.data ?? [];
|
||||
notifications.value = notificationsResponse.data ?? [];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to load content item detail:', fetchError);
|
||||
reset();
|
||||
error.value = 'Failed to load the content item detail.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createRevision(contentItemId, payload) {
|
||||
actions.revision = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/content-items/${contentItemId}/revisions`, payload);
|
||||
if (response.data) {
|
||||
revisions.value = [response.data, ...revisions.value];
|
||||
await fetchContentItemDetail(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.revision = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addGoogleDriveAsset(contentItemId, payload) {
|
||||
actions.asset = true;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/assets/google-drive', {
|
||||
...payload,
|
||||
contentItemId,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
if (response.data) {
|
||||
assets.value = [...assets.value, response.data];
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.asset = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addAssetRevision(contentItemId, assetId, payload) {
|
||||
actions.assetRevision = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/assets/${assetId}/revisions`, payload);
|
||||
if (response.data) {
|
||||
await fetchAssets(contentItemId);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.assetRevision = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addComment(contentItemId, payload) {
|
||||
actions.comment = true;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/comments', {
|
||||
...payload,
|
||||
contentItemId,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
if (response.data) {
|
||||
comments.value = [...comments.value, response.data];
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.comment = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveComment(contentItemId, commentId) {
|
||||
actions.comment = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/comments/${commentId}/resolve`);
|
||||
if (response.data) {
|
||||
comments.value = comments.value.map(comment => comment.id === commentId ? response.data : comment);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.comment = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createApproval(contentItemId, payload) {
|
||||
actions.approval = true;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/approvals', {
|
||||
...payload,
|
||||
contentItemId,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
if (response.data) {
|
||||
approvals.value = [response.data, ...approvals.value];
|
||||
await fetchContentItem(contentItemId);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.approval = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDecision(contentItemId, approvalId, payload) {
|
||||
actions.decision = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/approvals/${approvalId}/decisions`, payload);
|
||||
if (response.data) {
|
||||
approvals.value = approvals.value.map(approval => approval.id === approvalId ? response.data : approval);
|
||||
await fetchContentItem(contentItemId);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.decision = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStatus(contentItemId, status) {
|
||||
actions.status = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/content-items/${contentItemId}/status`, { status });
|
||||
item.value = response.data;
|
||||
await fetchNotifications(contentItemId);
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.status = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContentItem(contentItemId) {
|
||||
const response = await client.get(`/api/content-items/${contentItemId}`);
|
||||
item.value = response.data;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function fetchAssets(contentItemId) {
|
||||
const response = await client.get('/api/assets', { params: { contentItemId } });
|
||||
assets.value = response.data ?? [];
|
||||
return assets.value;
|
||||
}
|
||||
|
||||
async function fetchNotifications(contentItemId) {
|
||||
const response = await client.get('/api/notifications', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
contentItemId,
|
||||
},
|
||||
});
|
||||
notifications.value = response.data ?? [];
|
||||
return notifications.value;
|
||||
}
|
||||
|
||||
return {
|
||||
item,
|
||||
revisions,
|
||||
assets,
|
||||
comments,
|
||||
approvals,
|
||||
notifications,
|
||||
isLoading,
|
||||
error,
|
||||
actions,
|
||||
reset,
|
||||
fetchContentItemDetail,
|
||||
createRevision,
|
||||
addGoogleDriveAsset,
|
||||
addAssetRevision,
|
||||
addComment,
|
||||
resolveComment,
|
||||
createApproval,
|
||||
submitDecision,
|
||||
updateStatus,
|
||||
};
|
||||
});
|
||||
112
frontend/src/stores/contentItemsStore.js
Normal file
112
frontend/src/stores/contentItemsStore.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAuthStore } from '@/stores/authStore.js';
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useContentItemsStore = defineStore('content-items', () => {
|
||||
const authStore = useAuthStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const client = useClient();
|
||||
|
||||
const items = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const activeCount = computed(() =>
|
||||
items.value.filter(item => item.status !== 'Approved' && item.status !== 'Published' && item.status !== 'Archived')
|
||||
.length
|
||||
);
|
||||
|
||||
async function fetchContentItems(filters = {}) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
items.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.get('/api/content-items', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
clientId: filters.clientId,
|
||||
projectId: filters.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
items.value = response.data ?? [];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch content items:', fetchError);
|
||||
items.value = [];
|
||||
error.value = 'Failed to load content items.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createContentItem(payload) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
throw new Error('You must be authenticated to create a content item.');
|
||||
}
|
||||
|
||||
if (isCreating.value) {
|
||||
throw new Error('A content item creation request is already in progress.');
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/content-items', {
|
||||
...payload,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
items.value = [response.data, ...items.value];
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (createError) {
|
||||
console.error('Failed to create content item:', createError);
|
||||
error.value = 'Failed to create content item.';
|
||||
throw createError;
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContentItem(id) {
|
||||
const response = await client.get(`/api/content-items/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
||||
async ([isAuthenticated, workspaceId]) => {
|
||||
if (!isAuthenticated || !workspaceId) {
|
||||
items.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchContentItems();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
isCreating,
|
||||
error,
|
||||
activeCount,
|
||||
fetchContentItems,
|
||||
fetchContentItem,
|
||||
createContentItem,
|
||||
};
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {useAuthStore} from '@/stores/authStore.js';
|
||||
import {useSessionStorage} from '@vueuse/core';
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, watch} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
|
||||
export const useCreatorProfileStore = defineStore(
|
||||
'creator-profile',
|
||||
() => {
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
watch(
|
||||
() => authStore.isAuthenticated,
|
||||
async (newValue) => {
|
||||
if (newValue) {
|
||||
await fetchCreatorProfile();
|
||||
if (value.value && value.value.name !== undefined) {
|
||||
await router.push(`/@${value.value.slug}`);
|
||||
} else {
|
||||
await router.push('/');
|
||||
}
|
||||
} else if (!authStore.isRefreshing) {
|
||||
value.value = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const value = useSessionStorage(
|
||||
'creator-profile',
|
||||
{},
|
||||
{
|
||||
writeDefaults: false,
|
||||
storage: window.sessionStorage,
|
||||
serializer: {
|
||||
read: (value) => value ? JSON.parse(value) : undefined,
|
||||
write: (value) => value ? JSON.stringify(value) : undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const hasCreator = computed(
|
||||
() => value.value && Object.getOwnPropertyNames(value.value).length >= 1
|
||||
);
|
||||
|
||||
const client = useClient();
|
||||
|
||||
async function fetchCreatorProfile() {
|
||||
try {
|
||||
const response = await client.get(`/api/creators/profile`);
|
||||
value.value = response.data;
|
||||
} catch (error) {
|
||||
value.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeCreatorPage() {
|
||||
try {
|
||||
await client.delete(`/api/creators/@${value.value.slug}`)
|
||||
await fetchCreatorProfile();
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreCreatorPage() {
|
||||
try {
|
||||
await client.put(`/api/creators/@${value.value.slug}/restore`, {})
|
||||
await fetchCreatorProfile();
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
creator: value,
|
||||
hasCreator,
|
||||
removeCreatorPage,
|
||||
restoreCreatorPage,
|
||||
fetchCreatorProfile
|
||||
};
|
||||
});
|
||||
@@ -1,15 +1,13 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { i18n } from '@/plugins/i18n.js';
|
||||
|
||||
const ALLOWED_LOCALES = ['en', 'fr'];
|
||||
const DEFAULT_LOCALE = 'fr';
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
|
||||
export const useLanguageStore = defineStore('language', () => {
|
||||
const storedLocale = useSessionStorage('user-locale', DEFAULT_LOCALE);
|
||||
|
||||
// Get i18n instance (provided globally)
|
||||
const { locale } = useI18n();
|
||||
const locale = i18n.global.locale;
|
||||
|
||||
function sanitizeLocale(value) {
|
||||
return ALLOWED_LOCALES.includes(value) ? value : DEFAULT_LOCALE;
|
||||
@@ -18,15 +16,11 @@ export const useLanguageStore = defineStore('language', () => {
|
||||
// Initialize locale with a sanitized value
|
||||
const initial = sanitizeLocale(storedLocale.value);
|
||||
storedLocale.value = initial;
|
||||
if (locale) {
|
||||
locale.value = initial;
|
||||
}
|
||||
locale.value = initial;
|
||||
|
||||
function setLocale(newLocale) {
|
||||
const next = sanitizeLocale(newLocale);
|
||||
if (locale) {
|
||||
locale.value = next;
|
||||
}
|
||||
locale.value = next;
|
||||
storedLocale.value = next;
|
||||
}
|
||||
|
||||
|
||||
89
frontend/src/stores/notificationsStore.js
Normal file
89
frontend/src/stores/notificationsStore.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAuthStore } from '@/stores/authStore.js';
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useNotificationsStore = defineStore('notifications', () => {
|
||||
const authStore = useAuthStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const client = useClient();
|
||||
|
||||
const items = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const unreadCount = computed(() =>
|
||||
items.value.filter(item => !item.readAt).length
|
||||
);
|
||||
|
||||
const recentItems = computed(() => items.value.slice(0, 6));
|
||||
|
||||
function reset() {
|
||||
items.value = [];
|
||||
error.value = null;
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.get('/api/notifications', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
items.value = response.data ?? [];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch notifications:', fetchError);
|
||||
items.value = [];
|
||||
error.value = 'Failed to load notifications.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function markAsRead(notificationId) {
|
||||
try {
|
||||
await client.post(`/api/notifications/${notificationId}/read`);
|
||||
items.value = items.value.map(item =>
|
||||
item.id === notificationId
|
||||
? { ...item, readAt: item.readAt ?? new Date().toISOString() }
|
||||
: item
|
||||
);
|
||||
} catch (markError) {
|
||||
console.error('Failed to mark notification as read:', markError);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
||||
async ([isAuthenticated, workspaceId]) => {
|
||||
if (!isAuthenticated || !workspaceId) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchNotifications();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
recentItems,
|
||||
unreadCount,
|
||||
isLoading,
|
||||
error,
|
||||
reset,
|
||||
fetchNotifications,
|
||||
markAsRead,
|
||||
};
|
||||
});
|
||||
99
frontend/src/stores/projectsStore.js
Normal file
99
frontend/src/stores/projectsStore.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAuthStore } from '@/stores/authStore.js';
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useProjectsStore = defineStore('projects', () => {
|
||||
const authStore = useAuthStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const client = useClient();
|
||||
|
||||
const projects = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
async function fetchProjects() {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
projects.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.get('/api/projects', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
projects.value = response.data ?? [];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch projects:', fetchError);
|
||||
projects.value = [];
|
||||
error.value = 'Failed to load projects.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createProject(payload) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
throw new Error('You must be authenticated to create a project.');
|
||||
}
|
||||
|
||||
if (isCreating.value) {
|
||||
throw new Error('A project creation request is already in progress.');
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/projects', {
|
||||
...payload,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
projects.value = [...projects.value, response.data]
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (createError) {
|
||||
console.error('Failed to create project:', createError);
|
||||
error.value = 'Failed to create project.';
|
||||
throw createError;
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
||||
async ([isAuthenticated, workspaceId]) => {
|
||||
if (!isAuthenticated || !workspaceId) {
|
||||
projects.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchProjects();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
projects,
|
||||
isLoading,
|
||||
isCreating,
|
||||
error,
|
||||
fetchProjects,
|
||||
createProject,
|
||||
};
|
||||
});
|
||||
49
frontend/src/stores/reviewQueueStore.js
Normal file
49
frontend/src/stores/reviewQueueStore.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { computed } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useContentItemsStore } from '@/stores/contentItemsStore.js';
|
||||
import { useProjectsStore } from '@/stores/projectsStore.js';
|
||||
|
||||
const stageByStatus = {
|
||||
Draft: 'Draft',
|
||||
'In internal review': 'Internal review',
|
||||
'Changes requested internally': 'Internal changes requested',
|
||||
'Internal changes in progress': 'Internal revision',
|
||||
'Ready for client review': 'Ready for client review',
|
||||
'In client review': 'Client review',
|
||||
'Changes requested by client': 'Client changes requested',
|
||||
'Client changes in progress': 'Client revision',
|
||||
Approved: 'Approved',
|
||||
Rejected: 'Rejected',
|
||||
'Ready to publish': 'Ready to publish',
|
||||
Published: 'Published',
|
||||
Archived: 'Archived',
|
||||
};
|
||||
|
||||
export const useReviewQueueStore = defineStore('review-queue', () => {
|
||||
const contentItemsStore = useContentItemsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const items = computed(() =>
|
||||
contentItemsStore.items
|
||||
.filter(item => item.status !== 'Draft' && item.status !== 'Published' && item.status !== 'Archived')
|
||||
.map(item => {
|
||||
const project = projectsStore.projects.find(candidate => candidate.id === item.projectId);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
projectName: project?.name ?? 'Unknown campaign',
|
||||
stage: stageByStatus[item.status] ?? item.status,
|
||||
status: item.status,
|
||||
dueLabel: item.dueDate ? `Due ${new Date(item.dueDate).toLocaleDateString()}` : 'No due date',
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const urgentItems = computed(() => items.value.slice(0, 5));
|
||||
|
||||
return {
|
||||
items,
|
||||
urgentItems,
|
||||
};
|
||||
});
|
||||
@@ -50,9 +50,15 @@ export const useUserProfileStore = defineStore(
|
||||
const portraitUrl = computed(() => {
|
||||
return value.value && value.value.portraitUrl
|
||||
? value.value.portraitUrl
|
||||
: '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'
|
||||
: null
|
||||
})
|
||||
|
||||
const roles = computed(() => value.value?.userRoles ?? [])
|
||||
const persona = computed(() => value.value?.persona ?? null)
|
||||
const authorizedWorkspaceIds = computed(() => value.value?.authorizedWorkspaceIds ?? [])
|
||||
const authorizedClientIds = computed(() => value.value?.authorizedClientIds ?? [])
|
||||
const authorizedProjectIds = computed(() => value.value?.authorizedProjectIds ?? [])
|
||||
|
||||
async function fetchCurrentUserProfile() {
|
||||
try {
|
||||
const client = useClient()
|
||||
@@ -153,7 +159,7 @@ export const useUserProfileStore = defineStore(
|
||||
try {
|
||||
const client = useClient()
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile)
|
||||
formData.append('file', selectedFile, selectedFile.name || 'portrait.png')
|
||||
|
||||
const response = await client.post(
|
||||
`/api/users/portrait`,
|
||||
@@ -170,6 +176,11 @@ export const useUserProfileStore = defineStore(
|
||||
alias,
|
||||
fullname,
|
||||
portraitUrl,
|
||||
roles,
|
||||
persona,
|
||||
authorizedWorkspaceIds,
|
||||
authorizedClientIds,
|
||||
authorizedProjectIds,
|
||||
changeFullname,
|
||||
changeAlias,
|
||||
changeBirthday,
|
||||
|
||||
208
frontend/src/stores/workspaceStore.js
Normal file
208
frontend/src/stores/workspaceStore.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAuthStore } from '@/stores/authStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
const authStore = useAuthStore();
|
||||
const client = useClient();
|
||||
|
||||
const workspaces = ref([]);
|
||||
const activeWorkspaceId = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const invitesByWorkspace = ref({});
|
||||
const membersByWorkspace = ref({});
|
||||
const isInvitesLoading = ref(false);
|
||||
const isMembersLoading = ref(false);
|
||||
const isInviting = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const activeWorkspace = computed(() =>
|
||||
workspaces.value.find(workspace => workspace.id === activeWorkspaceId.value) ?? null
|
||||
);
|
||||
|
||||
async function fetchWorkspaces() {
|
||||
if (!authStore.isAuthenticated) {
|
||||
workspaces.value = [];
|
||||
activeWorkspaceId.value = null;
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.get('/api/workspaces');
|
||||
workspaces.value = response.data ?? [];
|
||||
|
||||
if (!workspaces.value.some(workspace => workspace.id === activeWorkspaceId.value)) {
|
||||
activeWorkspaceId.value = workspaces.value[0]?.id ?? null;
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch workspaces:', fetchError);
|
||||
workspaces.value = [];
|
||||
activeWorkspaceId.value = null;
|
||||
error.value = 'Failed to load workspaces.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createWorkspace(payload) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
throw new Error('You must be authenticated to create a workspace.');
|
||||
}
|
||||
|
||||
if (isCreating.value) {
|
||||
throw new Error('A workspace creation request is already in progress.');
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/workspaces', payload);
|
||||
|
||||
if (response.data) {
|
||||
workspaces.value = [...workspaces.value, response.data]
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
activeWorkspaceId.value = response.data.id;
|
||||
|
||||
try {
|
||||
await client.post('/api/clients', {
|
||||
workspaceId: response.data.id,
|
||||
name: response.data.name,
|
||||
});
|
||||
} catch (hiddenClientError) {
|
||||
console.error('Failed to provision operational client for workspace:', hiddenClientError);
|
||||
}
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (createError) {
|
||||
console.error('Failed to create workspace:', createError);
|
||||
error.value = 'Failed to create workspace.';
|
||||
throw createError;
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveWorkspace(workspaceId) {
|
||||
if (workspaces.value.some(workspace => workspace.id === workspaceId)) {
|
||||
activeWorkspaceId.value = workspaceId;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchInvites(workspaceId = activeWorkspaceId.value) {
|
||||
if (!authStore.isAuthenticated || !workspaceId) {
|
||||
invitesByWorkspace.value = {};
|
||||
return [];
|
||||
}
|
||||
|
||||
isInvitesLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await client.get(`/api/workspaces/${workspaceId}/invites`);
|
||||
invitesByWorkspace.value = {
|
||||
...invitesByWorkspace.value,
|
||||
[workspaceId]: response.data ?? [],
|
||||
};
|
||||
|
||||
return invitesByWorkspace.value[workspaceId];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch workspace invites:', fetchError);
|
||||
throw fetchError;
|
||||
} finally {
|
||||
isInvitesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMembers(workspaceId = activeWorkspaceId.value) {
|
||||
if (!authStore.isAuthenticated || !workspaceId) {
|
||||
membersByWorkspace.value = {};
|
||||
return [];
|
||||
}
|
||||
|
||||
isMembersLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await client.get(`/api/workspaces/${workspaceId}/members`);
|
||||
membersByWorkspace.value = {
|
||||
...membersByWorkspace.value,
|
||||
[workspaceId]: response.data ?? [],
|
||||
};
|
||||
|
||||
return membersByWorkspace.value[workspaceId];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch workspace members:', fetchError);
|
||||
throw fetchError;
|
||||
} finally {
|
||||
isMembersLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function inviteMember(payload) {
|
||||
if (!authStore.isAuthenticated || !activeWorkspaceId.value) {
|
||||
throw new Error('You must be authenticated to invite a workspace member.');
|
||||
}
|
||||
|
||||
if (isInviting.value) {
|
||||
throw new Error('A workspace invite request is already in progress.');
|
||||
}
|
||||
|
||||
isInviting.value = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/workspaces/${activeWorkspaceId.value}/invites`, payload);
|
||||
invitesByWorkspace.value = {
|
||||
...invitesByWorkspace.value,
|
||||
[activeWorkspaceId.value]: [response.data, ...(invitesByWorkspace.value[activeWorkspaceId.value] ?? [])],
|
||||
};
|
||||
|
||||
return response.data;
|
||||
} catch (inviteError) {
|
||||
console.error('Failed to create workspace invite:', inviteError);
|
||||
throw inviteError;
|
||||
} finally {
|
||||
isInviting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => authStore.isAuthenticated,
|
||||
async isAuthenticated => {
|
||||
if (!isAuthenticated) {
|
||||
workspaces.value = [];
|
||||
activeWorkspaceId.value = null;
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchWorkspaces();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
workspaces,
|
||||
activeWorkspaceId,
|
||||
activeWorkspace,
|
||||
isLoading,
|
||||
isCreating,
|
||||
invitesByWorkspace,
|
||||
membersByWorkspace,
|
||||
isInvitesLoading,
|
||||
isMembersLoading,
|
||||
isInviting,
|
||||
error,
|
||||
fetchWorkspaces,
|
||||
createWorkspace,
|
||||
fetchInvites,
|
||||
fetchMembers,
|
||||
inviteMember,
|
||||
setActiveWorkspace,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user