chore: add missing multi-level editor for approval workflow, rename projects to campaings.
This commit is contained in:
@@ -7,13 +7,13 @@
|
||||
import { useClientsStore } from '@/features/clients/stores/clientsStore.js';
|
||||
import { useContentItemDetailStore } from '@/features/content/stores/contentItemDetailStore.js';
|
||||
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
||||
import { useProjectsStore } from '@/features/projects/stores/projectsStore.js';
|
||||
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const campaignsStore = useCampaignsStore();
|
||||
const clientsStore = useClientsStore();
|
||||
const channelsStore = useChannelsStore();
|
||||
const contentItemsStore = useContentItemsStore();
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
projectId: '',
|
||||
campaignId: '',
|
||||
dueDate: '',
|
||||
body: '',
|
||||
hashtags: '',
|
||||
@@ -45,6 +45,14 @@
|
||||
});
|
||||
|
||||
const decisionForms = reactive({});
|
||||
const manualStatuses = [
|
||||
'Draft',
|
||||
'In production',
|
||||
'In approval',
|
||||
'Approved',
|
||||
'Scheduled',
|
||||
'Published',
|
||||
];
|
||||
const saveError = reactive({
|
||||
message: '',
|
||||
});
|
||||
@@ -52,7 +60,7 @@
|
||||
const isCreateMode = computed(() => route.name === 'content-item-create');
|
||||
const contentItemId = computed(() => isCreateMode.value ? null : route.params.id);
|
||||
const item = computed(() => detailStore.item);
|
||||
const availableProjects = computed(() => projectsStore.projects);
|
||||
const availableCampaigns = computed(() => campaignsStore.campaigns);
|
||||
const availableChannels = computed(() => channelsStore.channels);
|
||||
const groupedChannels = computed(() => {
|
||||
const groups = new Map();
|
||||
@@ -76,10 +84,11 @@
|
||||
.join(', ')
|
||||
);
|
||||
const operationalClient = computed(() => clientsStore.operationalClient);
|
||||
const projectNameById = computed(() =>
|
||||
new Map(projectsStore.projects.map(project => [project.id, project.name]))
|
||||
const campaignNameById = computed(() =>
|
||||
new Map(campaignsStore.campaigns.map(campaign => [campaign.id, campaign.name]))
|
||||
);
|
||||
const editorKey = computed(() => isCreateMode.value ? `new:${route.query.projectId ?? 'default'}` : String(route.params.id));
|
||||
const editorKey = computed(() => isCreateMode.value ? `new:${route.query.campaignId ?? 'default'}` : String(route.params.id));
|
||||
const isMultiLevelApproval = computed(() => workspaceStore.activeWorkspace?.approvalMode === 'Multi-level');
|
||||
|
||||
function blankPlacement(channel = null) {
|
||||
return {
|
||||
@@ -116,6 +125,16 @@
|
||||
return decisionForms[approvalId];
|
||||
}
|
||||
|
||||
function formatApprovalStepMeta(approval) {
|
||||
if (!approval.workflowInstanceId) {
|
||||
return `${approval.stage} · ${approval.state}`;
|
||||
}
|
||||
|
||||
const stepNumber = Number(approval.workflowStepSortOrder ?? 0) + 1;
|
||||
const requiredCount = approval.workflowStepRequiredApproverCount ?? 1;
|
||||
return `Step ${stepNumber} · ${approval.state} · ${requiredCount} required`;
|
||||
}
|
||||
|
||||
function syncPlacementChannel(placement, value) {
|
||||
const channel = availableChannels.value.find(candidate => candidate.id === value);
|
||||
placement.channelId = value;
|
||||
@@ -162,7 +181,7 @@
|
||||
function serializeDraft() {
|
||||
return JSON.parse(JSON.stringify({
|
||||
title: form.title,
|
||||
projectId: form.projectId,
|
||||
campaignId: form.campaignId,
|
||||
dueDate: form.dueDate,
|
||||
body: form.body,
|
||||
hashtags: form.hashtags,
|
||||
@@ -173,7 +192,7 @@
|
||||
|
||||
function restoreDraft(draft) {
|
||||
form.title = draft.title ?? '';
|
||||
form.projectId = draft.projectId ?? availableProjects.value[0]?.id ?? '';
|
||||
form.campaignId = draft.campaignId ?? availableCampaigns.value[0]?.id ?? '';
|
||||
form.dueDate = draft.dueDate ?? '';
|
||||
form.body = draft.body ?? '';
|
||||
form.hashtags = draft.hashtags ?? '';
|
||||
@@ -196,7 +215,7 @@
|
||||
}
|
||||
|
||||
function buildDraftFromItem() {
|
||||
const projectId = item.value?.projectId ?? '';
|
||||
const campaignId = item.value?.campaignId ?? '';
|
||||
const placements = parseTargets(item.value?.publicationTargets).map(target => {
|
||||
const channel = availableChannels.value.find(candidate => candidate.name.toLowerCase() === target.toLowerCase());
|
||||
|
||||
@@ -214,7 +233,7 @@
|
||||
|
||||
restoreDraft({
|
||||
title: item.value?.title ?? '',
|
||||
projectId,
|
||||
campaignId,
|
||||
dueDate: item.value?.dueDate ? new Date(item.value.dueDate).toISOString().slice(0, 10) : '',
|
||||
body: item.value?.publicationMessage ?? '',
|
||||
hashtags: item.value?.hashtags ?? '',
|
||||
@@ -224,13 +243,13 @@
|
||||
}
|
||||
|
||||
function buildDraftForNew() {
|
||||
const projectIdFromRoute = typeof route.query.projectId === 'string' ? route.query.projectId : '';
|
||||
const campaignIdFromRoute = typeof route.query.campaignId === 'string' ? route.query.campaignId : '';
|
||||
|
||||
restoreDraft({
|
||||
title: '',
|
||||
projectId: availableProjects.value.some(project => project.id === projectIdFromRoute)
|
||||
? projectIdFromRoute
|
||||
: availableProjects.value[0]?.id ?? '',
|
||||
campaignId: availableCampaigns.value.some(campaign => campaign.id === campaignIdFromRoute)
|
||||
? campaignIdFromRoute
|
||||
: availableCampaigns.value[0]?.id ?? '',
|
||||
dueDate: '',
|
||||
body: '',
|
||||
hashtags: '',
|
||||
@@ -283,7 +302,7 @@
|
||||
async function saveContent() {
|
||||
saveError.message = '';
|
||||
|
||||
if (!form.title.trim() || !form.projectId || !form.placements.length) {
|
||||
if (!form.title.trim() || !form.campaignId || !form.placements.length) {
|
||||
saveError.message = 'Title, campaign, and at least one channel are required.';
|
||||
return;
|
||||
}
|
||||
@@ -295,7 +314,7 @@
|
||||
|
||||
const payload = {
|
||||
title: form.title.trim(),
|
||||
projectId: form.projectId,
|
||||
campaignId: form.campaignId,
|
||||
publicationMessage: form.body.trim(),
|
||||
publicationTargets: placementSummary.value,
|
||||
hashtags: form.hashtags.trim(),
|
||||
@@ -389,8 +408,8 @@
|
||||
() => [
|
||||
isCreateMode.value,
|
||||
route.params.id,
|
||||
route.query.projectId,
|
||||
availableProjects.value.length,
|
||||
route.query.campaignId,
|
||||
availableCampaigns.value.length,
|
||||
availableChannels.value.length,
|
||||
],
|
||||
async () => {
|
||||
@@ -402,7 +421,7 @@
|
||||
watch(
|
||||
() => [
|
||||
form.title,
|
||||
form.projectId,
|
||||
form.campaignId,
|
||||
form.dueDate,
|
||||
form.body,
|
||||
form.hashtags,
|
||||
@@ -448,7 +467,7 @@
|
||||
<div class="eyebrow">{{ isCreateMode ? 'New content' : 'Content item' }}</div>
|
||||
<h1>{{ form.title || 'Untitled content' }}</h1>
|
||||
<p>
|
||||
{{ projectNameById.get(form.projectId) || 'Choose a campaign' }}
|
||||
{{ campaignNameById.get(form.campaignId) || 'Choose a campaign' }}
|
||||
<template v-if="!isCreateMode && item">
|
||||
· {{ item.status }}
|
||||
</template>
|
||||
@@ -488,33 +507,21 @@
|
||||
class="quick-actions"
|
||||
>
|
||||
<button
|
||||
v-for="status in manualStatuses"
|
||||
:key="status"
|
||||
class="secondary-button"
|
||||
:disabled="detailStore.actions.status"
|
||||
@click="moveStatus('Ready to publish')"
|
||||
:disabled="detailStore.actions.status || item.status === status"
|
||||
@click="moveStatus(status)"
|
||||
>
|
||||
Ready to publish
|
||||
</button>
|
||||
<button
|
||||
class="secondary-button"
|
||||
:disabled="detailStore.actions.status"
|
||||
@click="moveStatus('Published')"
|
||||
>
|
||||
Published
|
||||
</button>
|
||||
<button
|
||||
class="secondary-button"
|
||||
:disabled="detailStore.actions.status"
|
||||
@click="moveStatus('Archived')"
|
||||
>
|
||||
Archive
|
||||
{{ status }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-grid">
|
||||
<aside class="panel side-panel">
|
||||
<div class="panel-heading">
|
||||
<strong>Approval</strong>
|
||||
<span v-if="!isCreateMode">{{ detailStore.approvals.length }} requests</span>
|
||||
<div class="panel-heading">
|
||||
<strong>Approval</strong>
|
||||
<span v-if="!isCreateMode">{{ detailStore.approvals.length }} {{ isMultiLevelApproval ? 'steps' : 'requests' }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -525,7 +532,17 @@
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-stack">
|
||||
<div
|
||||
v-if="isMultiLevelApproval"
|
||||
class="empty-note"
|
||||
>
|
||||
Move this content to In approval to start the configured workflow steps.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="panel-stack"
|
||||
>
|
||||
<label class="field">
|
||||
<span>Stage</span>
|
||||
<select v-model="approvalForm.stage">
|
||||
@@ -572,7 +589,7 @@
|
||||
<div class="sub-card-header">
|
||||
<div>
|
||||
<strong>{{ approval.reviewerName }}</strong>
|
||||
<span>{{ approval.stage }} · {{ approval.state }}</span>
|
||||
<span>{{ formatApprovalStepMeta(approval) }}</span>
|
||||
</div>
|
||||
<small>{{ formatDate(approval.dueAt) }}</small>
|
||||
</div>
|
||||
@@ -607,8 +624,6 @@
|
||||
<span>Decision</span>
|
||||
<select v-model="getDecisionForm(approval.id).decision">
|
||||
<option value="Approved">Approved</option>
|
||||
<option value="Changes requested">Changes requested</option>
|
||||
<option value="Rejected">Rejected</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
@@ -649,7 +664,7 @@
|
||||
|
||||
<label class="field">
|
||||
<span>Campaign</span>
|
||||
<select v-model="form.projectId">
|
||||
<select v-model="form.campaignId">
|
||||
<option
|
||||
disabled
|
||||
value=""
|
||||
@@ -657,11 +672,11 @@
|
||||
Select a campaign
|
||||
</option>
|
||||
<option
|
||||
v-for="project in availableProjects"
|
||||
:key="project.id"
|
||||
:value="project.id"
|
||||
v-for="campaign in availableCampaigns"
|
||||
:key="campaign.id"
|
||||
:value="campaign.id"
|
||||
>
|
||||
{{ project.name }}
|
||||
{{ campaign.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user