wip
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import AppAvatar from '@/components/AppAvatar.vue';
|
||||
import ImageCropperDialog from '@/components/ImageCropperDialog.vue';
|
||||
import ApprovalWorkflowEditor from '@/features/workspaces/components/ApprovalWorkflowEditor.vue';
|
||||
import TimeZoneSelect from '@/features/workspaces/components/TimeZoneSelect.vue';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
import {
|
||||
@@ -20,9 +21,15 @@
|
||||
const settingsForm = reactive({
|
||||
name: '',
|
||||
timeZone: '',
|
||||
approvalMode: 'Required',
|
||||
schedulePostsAutomaticallyOnApproval: false,
|
||||
lockContentAfterApproval: false,
|
||||
sendAutomaticApprovalReminders: false,
|
||||
approvalSteps: [],
|
||||
});
|
||||
const settingsError = ref(null);
|
||||
const settingsStatus = ref(null);
|
||||
const approvalStepErrors = ref([]);
|
||||
const logoError = ref(null);
|
||||
const logoStatus = ref(null);
|
||||
const isLogoDialogOpen = ref(false);
|
||||
@@ -38,6 +45,7 @@
|
||||
const workspaceMembers = computed(() =>
|
||||
workspaceStore.membersByWorkspace[workspaceStore.activeWorkspaceId] ?? []
|
||||
);
|
||||
const normalizedApprovalSteps = computed(() => normalizeApprovalSteps(settingsForm.approvalSteps));
|
||||
const isSettingsDirty = computed(() => {
|
||||
const workspace = workspaceStore.activeWorkspace;
|
||||
|
||||
@@ -45,7 +53,15 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
return settingsForm.name.trim() !== workspace.name || settingsForm.timeZone.trim() !== workspace.timeZone;
|
||||
const workspaceApprovalSteps = normalizeApprovalSteps(workspace.approvalSteps ?? []);
|
||||
|
||||
return settingsForm.name.trim() !== workspace.name ||
|
||||
settingsForm.timeZone.trim() !== workspace.timeZone ||
|
||||
settingsForm.approvalMode !== (workspace.approvalMode ?? 'Required') ||
|
||||
settingsForm.schedulePostsAutomaticallyOnApproval !== Boolean(workspace.schedulePostsAutomaticallyOnApproval) ||
|
||||
settingsForm.lockContentAfterApproval !== Boolean(workspace.lockContentAfterApproval) ||
|
||||
settingsForm.sendAutomaticApprovalReminders !== Boolean(workspace.sendAutomaticApprovalReminders) ||
|
||||
JSON.stringify(normalizedApprovalSteps.value) !== JSON.stringify(workspaceApprovalSteps);
|
||||
});
|
||||
const settingsTabs = computed(() => [
|
||||
{ key: 'general', label: t('workspaceSettings.tabs.general'), icon: mdiCogOutline },
|
||||
@@ -53,29 +69,113 @@
|
||||
{ key: 'workflow', label: t('workspaceSettings.tabs.workflow'), icon: mdiTuneVariant },
|
||||
{ key: 'connectors', label: t('workspaceSettings.tabs.connectors'), icon: mdiFolderGoogleDrive },
|
||||
]);
|
||||
const workflowSteps = computed(() => [
|
||||
{
|
||||
key: 'internal',
|
||||
title: t('workspaceSettings.approvals.steps.internal'),
|
||||
detail: t('workspaceSettings.approvals.stepDetail.approverCount', { count: 1 }),
|
||||
},
|
||||
{
|
||||
key: 'client',
|
||||
title: t('workspaceSettings.approvals.steps.client'),
|
||||
detail: t('workspaceSettings.approvals.stepDetail.approverCount', { count: 1 }),
|
||||
},
|
||||
{
|
||||
key: 'publish',
|
||||
title: t('workspaceSettings.approvals.steps.publish'),
|
||||
detail: t('workspaceSettings.approvals.stepDetail.manualPublish'),
|
||||
},
|
||||
const approvalModeOptions = computed(() => [
|
||||
{ value: 'None', label: t('workspaceSettings.approvals.modes.none'), description: t('workspaceSettings.approvals.modeHelp.none') },
|
||||
{ value: 'Optional', label: t('workspaceSettings.approvals.modes.optional'), description: t('workspaceSettings.approvals.modeHelp.optional') },
|
||||
{ value: 'Required', label: t('workspaceSettings.approvals.modes.required'), description: t('workspaceSettings.approvals.modeHelp.required') },
|
||||
{ value: 'Multi-level', label: t('workspaceSettings.approvals.modes.multiLevel'), description: t('workspaceSettings.approvals.modeHelp.multiLevel') },
|
||||
]);
|
||||
const activeApprovalModeOption = computed(() =>
|
||||
approvalModeOptions.value.find(option => option.value === settingsForm.approvalMode) ?? approvalModeOptions.value[2]
|
||||
);
|
||||
const approvalWorkflowEditorLabels = computed(() => ({
|
||||
title: t('workspaceSettings.approvals.editor.title'),
|
||||
description: t('workspaceSettings.approvals.editor.description'),
|
||||
addStep: t('workspaceSettings.approvals.editor.addStep'),
|
||||
empty: t('workspaceSettings.approvals.editor.empty'),
|
||||
unnamedStep: t('workspaceSettings.approvals.editor.unnamedStep'),
|
||||
moveUp: t('workspaceSettings.approvals.editor.moveUp'),
|
||||
moveDown: t('workspaceSettings.approvals.editor.moveDown'),
|
||||
removeStep: t('workspaceSettings.approvals.editor.removeStep'),
|
||||
selectMember: t('workspaceSettings.approvals.editor.selectMember'),
|
||||
selectMembers: t('workspaceSettings.approvals.editor.selectMembers'),
|
||||
defaultStepName: number => t('workspaceSettings.approvals.editor.defaultStepName', { number }),
|
||||
stepNumber: number => t('workspaceSettings.approvals.editor.stepNumber', { number }),
|
||||
fields: {
|
||||
name: t('workspaceSettings.approvals.editor.fields.name'),
|
||||
targetType: t('workspaceSettings.approvals.editor.fields.targetType'),
|
||||
targetValue: t('workspaceSettings.approvals.editor.fields.targetValue'),
|
||||
requiredApproverCount: t('workspaceSettings.approvals.editor.fields.requiredApproverCount'),
|
||||
},
|
||||
targetTypes: {
|
||||
Role: t('workspaceSettings.approvals.editor.targetTypes.role'),
|
||||
Membership: t('workspaceSettings.approvals.editor.targetTypes.membership'),
|
||||
Member: t('workspaceSettings.approvals.editor.targetTypes.member'),
|
||||
},
|
||||
roles: {
|
||||
administrator: t('workspaceSettings.roles.administrator'),
|
||||
manager: t('workspaceSettings.roles.manager'),
|
||||
'workspace-member': t('workspaceSettings.roles.workspace-member'),
|
||||
client: t('workspaceSettings.roles.client'),
|
||||
provider: t('workspaceSettings.roles.provider'),
|
||||
},
|
||||
memberships: {
|
||||
Team: t('workspaceSettings.approvals.editor.memberships.team'),
|
||||
Client: t('workspaceSettings.approvals.editor.memberships.client'),
|
||||
},
|
||||
}));
|
||||
const workflowSteps = computed(() => {
|
||||
if (settingsForm.approvalMode === 'None') {
|
||||
return [
|
||||
{
|
||||
key: 'none',
|
||||
title: t('workspaceSettings.approvals.steps.none'),
|
||||
detail: t('workspaceSettings.approvals.stepDetail.none'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (settingsForm.approvalMode === 'Multi-level') {
|
||||
const configuredSteps = normalizedApprovalSteps.value.map((step, index) => ({
|
||||
key: `approval-${index}`,
|
||||
title: step.name || t('workspaceSettings.approvals.editor.unnamedStep'),
|
||||
detail: t('workspaceSettings.approvals.stepDetail.multiLevelTarget', {
|
||||
count: step.requiredApproverCount,
|
||||
target: formatApprovalTarget(step),
|
||||
}),
|
||||
}));
|
||||
|
||||
return [
|
||||
...configuredSteps,
|
||||
{
|
||||
key: 'publish',
|
||||
title: t('workspaceSettings.approvals.steps.publish'),
|
||||
detail: settingsForm.schedulePostsAutomaticallyOnApproval
|
||||
? t('workspaceSettings.approvals.stepDetail.autoSchedule')
|
||||
: t('workspaceSettings.approvals.stepDetail.manualSchedule'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'approval',
|
||||
title: t('workspaceSettings.approvals.steps.approval'),
|
||||
detail: settingsForm.approvalMode === 'Optional'
|
||||
? t('workspaceSettings.approvals.stepDetail.optional')
|
||||
: t('workspaceSettings.approvals.stepDetail.approverCount', { count: 1 }),
|
||||
},
|
||||
{
|
||||
key: 'publish',
|
||||
title: t('workspaceSettings.approvals.steps.publish'),
|
||||
detail: settingsForm.schedulePostsAutomaticallyOnApproval
|
||||
? t('workspaceSettings.approvals.stepDetail.autoSchedule')
|
||||
: t('workspaceSettings.approvals.stepDetail.manualSchedule'),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => workspaceStore.activeWorkspace,
|
||||
workspace => {
|
||||
settingsForm.name = workspace?.name ?? '';
|
||||
settingsForm.timeZone = workspace?.timeZone ?? '';
|
||||
settingsForm.approvalMode = workspace?.approvalMode ?? 'Required';
|
||||
settingsForm.schedulePostsAutomaticallyOnApproval = Boolean(workspace?.schedulePostsAutomaticallyOnApproval);
|
||||
settingsForm.lockContentAfterApproval = Boolean(workspace?.lockContentAfterApproval);
|
||||
settingsForm.sendAutomaticApprovalReminders = Boolean(workspace?.sendAutomaticApprovalReminders);
|
||||
settingsForm.approvalSteps = normalizeApprovalSteps(workspace?.approvalSteps ?? []);
|
||||
approvalStepErrors.value = [];
|
||||
settingsError.value = null;
|
||||
settingsStatus.value = null;
|
||||
},
|
||||
@@ -117,12 +217,28 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsForm.approvalMode === 'Multi-level' && !validateApprovalSteps()) {
|
||||
settingsError.value ||= t('workspaceSettings.approvals.editor.errors.fixInvalidSteps');
|
||||
return;
|
||||
}
|
||||
|
||||
approvalStepErrors.value = [];
|
||||
|
||||
try {
|
||||
await workspaceStore.updateWorkspace(workspace.id, {
|
||||
name,
|
||||
timeZone,
|
||||
approvalMode: settingsForm.approvalMode,
|
||||
schedulePostsAutomaticallyOnApproval: settingsForm.schedulePostsAutomaticallyOnApproval,
|
||||
lockContentAfterApproval: settingsForm.lockContentAfterApproval,
|
||||
sendAutomaticApprovalReminders: settingsForm.sendAutomaticApprovalReminders,
|
||||
approvalSteps: settingsForm.approvalMode === 'Multi-level'
|
||||
? normalizedApprovalSteps.value
|
||||
: undefined,
|
||||
});
|
||||
settingsStatus.value = t('workspaceSettings.general.saved');
|
||||
settingsStatus.value = activeTab.value === 'workflow'
|
||||
? t('workspaceSettings.approvals.saved')
|
||||
: t('workspaceSettings.general.saved');
|
||||
} catch (error) {
|
||||
console.error('Failed to update workspace settings:', error);
|
||||
settingsError.value = t('workspaceSettings.errors.updateFailed');
|
||||
@@ -183,6 +299,77 @@
|
||||
const normalizedRole = role.charAt(0).toLowerCase() + role.slice(1);
|
||||
return t(`workspaceSettings.roles.${normalizedRole}`, role);
|
||||
}
|
||||
|
||||
function normalizeApprovalSteps(steps) {
|
||||
return [...steps]
|
||||
.sort((left, right) => Number(left.sortOrder ?? 0) - Number(right.sortOrder ?? 0))
|
||||
.map((step, index) => ({
|
||||
name: step.name ?? '',
|
||||
sortOrder: index,
|
||||
targetType: step.targetType ?? 'Role',
|
||||
targetValue: step.targetValue ?? '',
|
||||
requiredApproverCount: Number(step.requiredApproverCount ?? 1),
|
||||
}));
|
||||
}
|
||||
|
||||
function validateApprovalSteps() {
|
||||
const errors = normalizedApprovalSteps.value.map(step => {
|
||||
const stepErrors = {};
|
||||
|
||||
if (!step.name.trim()) {
|
||||
stepErrors.name = t('workspaceSettings.approvals.editor.errors.nameRequired');
|
||||
}
|
||||
|
||||
if (!step.targetValue?.trim()) {
|
||||
stepErrors.targetValue = t('workspaceSettings.approvals.editor.errors.targetRequired');
|
||||
}
|
||||
|
||||
if (step.targetType === 'Member' && getMemberTargetIds(step).length < step.requiredApproverCount) {
|
||||
stepErrors.targetValue = t('workspaceSettings.approvals.editor.errors.notEnoughMembers');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(step.requiredApproverCount) || step.requiredApproverCount < 1) {
|
||||
stepErrors.requiredApproverCount = t('workspaceSettings.approvals.editor.errors.requiredApproverCount');
|
||||
}
|
||||
|
||||
return stepErrors;
|
||||
});
|
||||
|
||||
if (!errors.length) {
|
||||
settingsError.value = t('workspaceSettings.approvals.editor.errors.atLeastOneStep');
|
||||
approvalStepErrors.value = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
approvalStepErrors.value = errors;
|
||||
settingsError.value = null;
|
||||
return !errors.some(error => Object.keys(error).length > 0);
|
||||
}
|
||||
|
||||
function formatApprovalTarget(step) {
|
||||
if (step.targetType === 'Membership') {
|
||||
return t(`workspaceSettings.approvals.editor.memberships.${step.targetValue.toLowerCase()}`, step.targetValue);
|
||||
}
|
||||
|
||||
if (step.targetType === 'Member') {
|
||||
const selectedNames = getMemberTargetIds(step)
|
||||
.map(memberId => workspaceMembers.value.find(candidate => candidate.id === memberId)?.displayName)
|
||||
.filter(Boolean);
|
||||
|
||||
return selectedNames.length
|
||||
? selectedNames.join(', ')
|
||||
: t('workspaceSettings.approvals.editor.targetTypes.member');
|
||||
}
|
||||
|
||||
return t(`workspaceSettings.roles.${step.targetValue}`, step.targetValue);
|
||||
}
|
||||
|
||||
function getMemberTargetIds(step) {
|
||||
return (step.targetValue ?? '')
|
||||
.split(',')
|
||||
.map(value => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -432,19 +619,95 @@
|
||||
<p>{{ t('workspaceSettings.approvals.flowDescription') }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="settingsError"
|
||||
class="page-message error"
|
||||
>
|
||||
{{ settingsError }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="settingsStatus"
|
||||
class="page-message success"
|
||||
>
|
||||
{{ settingsStatus }}
|
||||
</div>
|
||||
|
||||
<div class="workflow-rule-list">
|
||||
<label class="field">
|
||||
<span>{{ t('workspaceSettings.approvals.fields.approvalMode') }}</span>
|
||||
<select
|
||||
v-model="settingsForm.approvalMode"
|
||||
:disabled="workspaceStore.isUpdating"
|
||||
>
|
||||
<option
|
||||
v-for="option in approvalModeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="workflow-rule">
|
||||
<strong>{{ t('workspaceSettings.approvals.fields.requireInternalReview') }}</strong>
|
||||
<span>{{ t('workspaceSettings.approvals.fieldHelp.requireInternalReview') }}</span>
|
||||
</div>
|
||||
<div class="workflow-rule">
|
||||
<strong>{{ t('workspaceSettings.approvals.fields.requireClientReview') }}</strong>
|
||||
<span>{{ t('workspaceSettings.approvals.fieldHelp.requireClientReview') }}</span>
|
||||
</div>
|
||||
<div class="workflow-rule">
|
||||
<strong>{{ t('workspaceSettings.approvals.fields.publishBehaviour') }}</strong>
|
||||
<span>{{ t('workspaceSettings.approvals.publishBehaviour.manual') }}</span>
|
||||
<strong>{{ activeApprovalModeOption.label }}</strong>
|
||||
<span>{{ activeApprovalModeOption.description }}</span>
|
||||
</div>
|
||||
|
||||
<ApprovalWorkflowEditor
|
||||
v-if="settingsForm.approvalMode === 'Multi-level'"
|
||||
v-model="settingsForm.approvalSteps"
|
||||
:members="workspaceMembers"
|
||||
:errors="approvalStepErrors"
|
||||
:disabled="workspaceStore.isUpdating"
|
||||
:labels="approvalWorkflowEditorLabels"
|
||||
/>
|
||||
|
||||
<label class="workflow-toggle">
|
||||
<input
|
||||
v-model="settingsForm.schedulePostsAutomaticallyOnApproval"
|
||||
type="checkbox"
|
||||
:disabled="workspaceStore.isUpdating"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ t('workspaceSettings.approvals.fields.schedulePostsAutomaticallyOnApproval') }}</strong>
|
||||
<small>{{ t('workspaceSettings.approvals.fieldHelp.schedulePostsAutomaticallyOnApproval') }}</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="workflow-toggle">
|
||||
<input
|
||||
v-model="settingsForm.lockContentAfterApproval"
|
||||
type="checkbox"
|
||||
:disabled="workspaceStore.isUpdating"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ t('workspaceSettings.approvals.fields.lockContentAfterApproval') }}</strong>
|
||||
<small>{{ t('workspaceSettings.approvals.fieldHelp.lockContentAfterApproval') }}</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="workflow-toggle">
|
||||
<input
|
||||
v-model="settingsForm.sendAutomaticApprovalReminders"
|
||||
type="checkbox"
|
||||
:disabled="workspaceStore.isUpdating"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ t('workspaceSettings.approvals.fields.sendAutomaticApprovalReminders') }}</strong>
|
||||
<small>{{ t('workspaceSettings.approvals.fieldHelp.sendAutomaticApprovalReminders') }}</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="primary-button"
|
||||
type="button"
|
||||
:disabled="workspaceStore.isUpdating || !isSettingsDirty"
|
||||
@click="submitWorkspaceSettings"
|
||||
>
|
||||
{{ workspaceStore.isUpdating ? t('common.saving') : t('workspaceSettings.approvals.saveAction') }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -683,6 +946,7 @@
|
||||
.empty-state,
|
||||
.connector-row,
|
||||
.workflow-rule,
|
||||
.workflow-toggle,
|
||||
.workflow-step {
|
||||
@apply rounded-[1rem] border px-4 py-4;
|
||||
background: #fffaf2;
|
||||
@@ -696,10 +960,19 @@
|
||||
.invite-row div,
|
||||
.connector-copy,
|
||||
.workflow-rule,
|
||||
.workflow-toggle span,
|
||||
.workflow-step-copy {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
|
||||
.workflow-toggle {
|
||||
@apply flex items-start gap-3 text-sm;
|
||||
}
|
||||
|
||||
.workflow-toggle input {
|
||||
@apply mt-1 h-4 w-4 accent-teal-700;
|
||||
}
|
||||
|
||||
.connector-row {
|
||||
@apply flex flex-col gap-4 md:flex-row md:items-center md:justify-between;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user