1085 lines
39 KiB
Vue
1085 lines
39 KiB
Vue
<script setup>
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
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 {
|
|
mdiAccountGroupOutline,
|
|
mdiCheckCircleOutline,
|
|
mdiCogOutline,
|
|
mdiFolderGoogleDrive,
|
|
mdiImageMultipleOutline,
|
|
mdiTuneVariant,
|
|
} from '@mdi/js';
|
|
|
|
const { t } = useI18n();
|
|
const workspaceStore = useWorkspaceStore();
|
|
const activeTab = ref('general');
|
|
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);
|
|
|
|
const inviteForm = reactive({
|
|
email: '',
|
|
role: 'workspace-member',
|
|
});
|
|
|
|
const pendingInvites = computed(() =>
|
|
workspaceStore.invitesByWorkspace[workspaceStore.activeWorkspaceId] ?? []
|
|
);
|
|
const workspaceMembers = computed(() =>
|
|
workspaceStore.membersByWorkspace[workspaceStore.activeWorkspaceId] ?? []
|
|
);
|
|
const normalizedApprovalSteps = computed(() => normalizeApprovalSteps(settingsForm.approvalSteps));
|
|
const isSettingsDirty = computed(() => {
|
|
const workspace = workspaceStore.activeWorkspace;
|
|
|
|
if (!workspace) {
|
|
return false;
|
|
}
|
|
|
|
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 },
|
|
{ key: 'members', label: t('workspaceSettings.tabs.members'), icon: mdiAccountGroupOutline },
|
|
{ key: 'workflow', label: t('workspaceSettings.tabs.workflow'), icon: mdiTuneVariant },
|
|
{ key: 'connectors', label: t('workspaceSettings.tabs.connectors'), icon: mdiFolderGoogleDrive },
|
|
]);
|
|
const activeTabDetail = computed(() => {
|
|
if (activeTab.value === 'members') {
|
|
return {
|
|
title: t('workspaceSettings.tabs.members'),
|
|
description: t('workspaceSettings.inviteDescription'),
|
|
};
|
|
}
|
|
|
|
if (activeTab.value === 'workflow') {
|
|
return {
|
|
title: t('workspaceSettings.tabs.workflow'),
|
|
description: t('workspaceSettings.approvals.flowDescription'),
|
|
};
|
|
}
|
|
|
|
if (activeTab.value === 'connectors') {
|
|
return {
|
|
title: t('workspaceSettings.tabs.connectors'),
|
|
description: t('workspaceSettings.connectors.description'),
|
|
};
|
|
}
|
|
|
|
return {
|
|
title: t('workspaceSettings.tabs.general'),
|
|
description: t('workspaceSettings.general.detailsDescription'),
|
|
};
|
|
});
|
|
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;
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => workspaceStore.activeWorkspaceId,
|
|
async workspaceId => {
|
|
if (!workspaceId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await workspaceStore.fetchInvites(workspaceId);
|
|
await workspaceStore.fetchMembers(workspaceId);
|
|
} catch (error) {
|
|
console.error('Failed to load workspace people data:', error);
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
async function submitWorkspaceSettings() {
|
|
const workspace = workspaceStore.activeWorkspace;
|
|
|
|
if (!workspace || workspaceStore.isUpdating) {
|
|
return;
|
|
}
|
|
|
|
settingsError.value = null;
|
|
settingsStatus.value = null;
|
|
|
|
const name = settingsForm.name.trim();
|
|
const timeZone = settingsForm.timeZone.trim();
|
|
|
|
if (!name || !timeZone) {
|
|
settingsError.value = t('workspaceSettings.errors.required');
|
|
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 = 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');
|
|
}
|
|
}
|
|
|
|
async function saveWorkspaceLogo(result) {
|
|
const workspace = workspaceStore.activeWorkspace;
|
|
|
|
if (!workspace || workspaceStore.isUploadingLogo) {
|
|
return;
|
|
}
|
|
|
|
logoError.value = null;
|
|
logoStatus.value = null;
|
|
|
|
try {
|
|
await workspaceStore.uploadWorkspaceLogo(workspace.id, result.file);
|
|
logoStatus.value = t('workspaceSettings.logo.saved');
|
|
isLogoDialogOpen.value = false;
|
|
} catch (error) {
|
|
console.error('Failed to update workspace logo:', error);
|
|
logoError.value = t('workspaceSettings.errors.logoUploadFailed');
|
|
}
|
|
}
|
|
|
|
async function submitInvite() {
|
|
if (!inviteForm.email.trim() || !inviteForm.role) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await workspaceStore.inviteMember({
|
|
email: inviteForm.email.trim(),
|
|
role: inviteForm.role,
|
|
});
|
|
|
|
inviteForm.email = '';
|
|
inviteForm.role = 'workspace-member';
|
|
} catch (error) {
|
|
console.error('Failed to invite workspace member:', error);
|
|
}
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
return new Date(value).toLocaleString();
|
|
}
|
|
|
|
function translateRole(role) {
|
|
if (!role) {
|
|
return '';
|
|
}
|
|
|
|
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>
|
|
<section class="workspace-settings-shell">
|
|
<div class="workspace-settings-hero">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.currentWorkspace') }}</span>
|
|
<h1>{{ workspaceStore.activeWorkspace?.name || t('workspaceSettings.noWorkspaceSelected') }}</h1>
|
|
<p>{{ t('workspaceSettings.description') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="workspaceStore.activeWorkspace"
|
|
class="workspace-settings-page"
|
|
>
|
|
<nav
|
|
class="tab-strip"
|
|
aria-label="Workspace settings sections"
|
|
>
|
|
<button
|
|
v-for="tab in settingsTabs"
|
|
:key="tab.key"
|
|
type="button"
|
|
class="tab-button"
|
|
:class="{ 'tab-button-active': activeTab === tab.key }"
|
|
@click="activeTab = tab.key"
|
|
>
|
|
<v-icon :icon="tab.icon" />
|
|
<span>{{ tab.label }}</span>
|
|
</button>
|
|
</nav>
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-heading">
|
|
<h2>{{ activeTabDetail.title }}</h2>
|
|
<p>{{ activeTabDetail.description }}</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="activeTab === 'general'"
|
|
class="workspace-settings-grid workspace-settings-grid-single"
|
|
>
|
|
<article class="settings-card">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.general.detailsTitle') }}</span>
|
|
<p>{{ t('workspaceSettings.general.detailsDescription') }}</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="settingsError"
|
|
class="page-message error"
|
|
>
|
|
{{ settingsError }}
|
|
</div>
|
|
|
|
<div
|
|
v-if="settingsStatus"
|
|
class="page-message success"
|
|
>
|
|
{{ settingsStatus }}
|
|
</div>
|
|
|
|
<form
|
|
class="form-stack"
|
|
@submit.prevent="submitWorkspaceSettings"
|
|
>
|
|
<div class="logo-picker-card">
|
|
<AppAvatar
|
|
:name="settingsForm.name || workspaceStore.activeWorkspace.name"
|
|
:src="workspaceStore.activeWorkspace.logoUrl"
|
|
size="lg"
|
|
/>
|
|
<div class="logo-picker-copy">
|
|
<strong>{{ t('workspaceSettings.logo.title') }}</strong>
|
|
<small>{{ t('workspaceSettings.logo.description') }}</small>
|
|
<small
|
|
v-if="logoError"
|
|
class="field-error"
|
|
>
|
|
{{ logoError }}
|
|
</small>
|
|
<small
|
|
v-if="logoStatus"
|
|
class="field-success"
|
|
>
|
|
{{ logoStatus }}
|
|
</small>
|
|
</div>
|
|
<button
|
|
class="secondary-button"
|
|
type="button"
|
|
:disabled="workspaceStore.isUploadingLogo"
|
|
@click="isLogoDialogOpen = true"
|
|
>
|
|
{{ workspaceStore.isUploadingLogo ? t('common.saving') : t('workspaceSettings.logo.changeAction') }}
|
|
</button>
|
|
</div>
|
|
|
|
<label class="field">
|
|
<span>{{ t('workspaceSettings.fields.name') }}</span>
|
|
<input
|
|
v-model="settingsForm.name"
|
|
type="text"
|
|
:disabled="workspaceStore.isUpdating"
|
|
/>
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>{{ t('workspaceSettings.fields.timeZone') }}</span>
|
|
<TimeZoneSelect
|
|
v-model="settingsForm.timeZone"
|
|
:disabled="workspaceStore.isUpdating"
|
|
/>
|
|
</label>
|
|
|
|
<button
|
|
class="primary-button"
|
|
type="submit"
|
|
:disabled="workspaceStore.isUpdating || !isSettingsDirty"
|
|
>
|
|
{{ workspaceStore.isUpdating ? t('common.saving') : t('workspaceSettings.general.saveAction') }}
|
|
</button>
|
|
</form>
|
|
|
|
</article>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="activeTab === 'members'"
|
|
class="workspace-settings-grid workspace-settings-grid-single"
|
|
>
|
|
<article class="settings-card">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.members.inviteTitle') }}</span>
|
|
<p>{{ t('workspaceSettings.inviteDescription') }}</p>
|
|
</div>
|
|
|
|
<form
|
|
class="form-stack"
|
|
@submit.prevent="submitInvite"
|
|
>
|
|
<label class="field">
|
|
<span>{{ t('workspaceSettings.fields.memberEmail') }}</span>
|
|
<input
|
|
v-model="inviteForm.email"
|
|
type="email"
|
|
/>
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>{{ t('workspaceSettings.fields.memberRole') }}</span>
|
|
<select v-model="inviteForm.role">
|
|
<option value="workspace-member">{{ t('workspaceSettings.roles.workspace-member') }}</option>
|
|
<option value="client">{{ t('workspaceSettings.roles.client') }}</option>
|
|
<option value="provider">{{ t('workspaceSettings.roles.provider') }}</option>
|
|
</select>
|
|
</label>
|
|
|
|
<button
|
|
class="primary-button"
|
|
type="submit"
|
|
>
|
|
{{ workspaceStore.isInviting ? t('common.creating') : t('workspaceSettings.sendInvite') }}
|
|
</button>
|
|
</form>
|
|
</article>
|
|
|
|
<article class="settings-card">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.members.pendingTitle') }}</span>
|
|
<p>{{ t('workspaceSettings.members.pendingDescription') }}</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="workspaceStore.isInvitesLoading"
|
|
class="empty-state"
|
|
>
|
|
{{ t('loading') }}
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="pendingInvites.length"
|
|
class="invite-list"
|
|
>
|
|
<div
|
|
v-for="invite in pendingInvites"
|
|
:key="invite.id"
|
|
class="invite-row"
|
|
>
|
|
<div>
|
|
<strong>{{ invite.email }}</strong>
|
|
<span>{{ t(`workspaceSettings.roles.${invite.role}`) }}</span>
|
|
</div>
|
|
<small>{{ formatDate(invite.createdAt) }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="empty-state"
|
|
>
|
|
{{ t('workspaceSettings.inviteEmpty') }}
|
|
</div>
|
|
</article>
|
|
|
|
<article class="settings-card">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.members.activeTitle') }}</span>
|
|
<p>{{ t('workspaceSettings.members.activeDescription') }}</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="workspaceStore.isMembersLoading"
|
|
class="empty-state"
|
|
>
|
|
{{ t('loading') }}
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="workspaceMembers.length"
|
|
class="invite-list"
|
|
>
|
|
<div
|
|
v-for="member in workspaceMembers"
|
|
:key="member.id"
|
|
class="invite-row"
|
|
>
|
|
<div>
|
|
<strong>{{ member.displayName }}</strong>
|
|
<span>{{ member.email }}</span>
|
|
<span>{{ member.roles.map(translateRole).join(' · ') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="empty-state"
|
|
>
|
|
{{ t('workspaceSettings.members.activeEmpty') }}
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="activeTab === 'workflow'"
|
|
class="workflow-grid"
|
|
>
|
|
<article class="settings-card">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.approvals.flowTitle') }}</span>
|
|
<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>{{ 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>
|
|
|
|
<article class="settings-card">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.approvals.previewTitle') }}</span>
|
|
<p>{{ t('workspaceSettings.approvals.previewDescription') }}</p>
|
|
</div>
|
|
|
|
<div class="workflow-steps">
|
|
<div
|
|
v-for="step in workflowSteps"
|
|
:key="step.key"
|
|
class="workflow-step"
|
|
>
|
|
<div class="workflow-step-icon">
|
|
<v-icon :icon="mdiCheckCircleOutline" />
|
|
</div>
|
|
<div class="workflow-step-copy">
|
|
<strong>{{ step.title }}</strong>
|
|
<span>{{ step.detail }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="workspace-settings-grid"
|
|
>
|
|
<article class="settings-card">
|
|
<div class="section-copy">
|
|
<span class="section-kicker">{{ t('workspaceSettings.connectors.title') }}</span>
|
|
<p>{{ t('workspaceSettings.connectors.description') }}</p>
|
|
</div>
|
|
|
|
<div class="connector-list">
|
|
<div class="connector-row">
|
|
<div class="connector-main">
|
|
<div class="connector-icon">
|
|
<v-icon :icon="mdiFolderGoogleDrive" />
|
|
</div>
|
|
|
|
<div class="connector-copy">
|
|
<strong>{{ t('workspaceSettings.connectors.googleDrive.title') }}</strong>
|
|
<span>{{ t('workspaceSettings.connectors.googleDrive.description') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="connector-status">
|
|
{{ t('workspaceSettings.connectors.googleDrive.status') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<router-link
|
|
:to="{ name: 'media-library' }"
|
|
class="connector-link"
|
|
>
|
|
<v-icon :icon="mdiImageMultipleOutline" />
|
|
<span>{{ t('workspaceSettings.connectors.openMediaLibrary') }}</span>
|
|
</router-link>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="empty-state"
|
|
>
|
|
{{ t('workspaceSettings.noWorkspaceSelected') }}
|
|
</div>
|
|
|
|
<ImageCropperDialog
|
|
v-model="isLogoDialogOpen"
|
|
:title="t('workspaceSettings.logo.cropperTitle')"
|
|
:confirm-label="t('workspaceSettings.logo.saveAction')"
|
|
:upload-label="t('workspaceSettings.logo.chooseAction')"
|
|
:initial-url="workspaceStore.activeWorkspace?.logoUrl"
|
|
:is-saving="workspaceStore.isUploadingLogo"
|
|
@save="saveWorkspaceLogo"
|
|
/>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@reference "@/assets/main.css";
|
|
.workspace-settings-shell {
|
|
@apply mx-auto flex w-full max-w-6xl flex-col gap-6 px-5 py-8 md:px-8;
|
|
}
|
|
|
|
.workspace-settings-hero {
|
|
@apply flex flex-col;
|
|
}
|
|
|
|
.workspace-settings-page,
|
|
.tab-content {
|
|
@apply flex flex-col gap-4;
|
|
}
|
|
|
|
.workspace-settings-grid {
|
|
@apply grid gap-4 lg:grid-cols-2;
|
|
}
|
|
|
|
.workflow-grid {
|
|
@apply grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)];
|
|
}
|
|
|
|
.workspace-settings-grid-single {
|
|
@apply lg:grid-cols-1;
|
|
}
|
|
|
|
.settings-card {
|
|
@apply flex flex-col gap-5 rounded-[0.75rem] border p-5;
|
|
background: rgba(255, 255, 255, 0.94);
|
|
border-color: rgba(23, 32, 51, 0.08);
|
|
}
|
|
|
|
.section-copy {
|
|
@apply flex flex-col gap-2;
|
|
}
|
|
|
|
.tab-strip {
|
|
@apply flex flex-wrap gap-2 border-b pb-3;
|
|
border-color: rgba(23, 32, 51, 0.1);
|
|
}
|
|
|
|
.tab-button {
|
|
@apply inline-flex h-10 items-center gap-2 rounded-[0.75rem] px-3 text-sm font-semibold transition-colors;
|
|
color: #526178;
|
|
}
|
|
|
|
.tab-button:hover {
|
|
background: rgba(23, 32, 51, 0.06);
|
|
color: #172033;
|
|
}
|
|
|
|
.tab-button-active {
|
|
background: #172033;
|
|
color: #fffaf2;
|
|
}
|
|
|
|
.tab-button :deep(.v-icon) {
|
|
@apply text-lg;
|
|
}
|
|
|
|
.tab-heading {
|
|
@apply flex flex-col gap-1;
|
|
}
|
|
|
|
.tab-heading h2 {
|
|
@apply text-2xl font-black;
|
|
color: #172033;
|
|
}
|
|
|
|
.section-kicker {
|
|
@apply text-xs font-bold uppercase tracking-[0.2em];
|
|
color: #c2410c;
|
|
}
|
|
|
|
.section-copy h1,
|
|
.tab-heading h2,
|
|
.invite-row strong,
|
|
.connector-copy strong,
|
|
.connector-status,
|
|
.workflow-rule strong,
|
|
.workflow-step-copy strong {
|
|
color: #172033;
|
|
}
|
|
|
|
.section-copy h1 {
|
|
@apply text-3xl font-black md:text-4xl;
|
|
}
|
|
|
|
.section-copy p,
|
|
.tab-heading p,
|
|
.invite-row span,
|
|
.invite-row small,
|
|
.empty-state,
|
|
.connector-copy span,
|
|
.connector-link span,
|
|
.workflow-rule span,
|
|
.workflow-step-copy span {
|
|
@apply text-sm leading-6;
|
|
color: #526178;
|
|
}
|
|
|
|
.logo-picker-card {
|
|
@apply flex flex-col gap-4 rounded-[0.75rem] border p-4 sm:flex-row sm:items-center;
|
|
background: rgba(23, 32, 51, 0.04);
|
|
border-color: rgba(23, 32, 51, 0.08);
|
|
}
|
|
|
|
.logo-picker-copy {
|
|
@apply flex min-w-0 flex-1 flex-col gap-1;
|
|
}
|
|
|
|
.logo-picker-copy strong {
|
|
color: #172033;
|
|
}
|
|
|
|
.logo-picker-copy small,
|
|
.field-error,
|
|
.field-success {
|
|
@apply text-sm leading-6;
|
|
}
|
|
|
|
.field-error {
|
|
color: #b91c1c;
|
|
}
|
|
|
|
.field-success {
|
|
color: #0f766e;
|
|
}
|
|
|
|
.form-stack {
|
|
@apply flex flex-col gap-4;
|
|
}
|
|
|
|
.field {
|
|
@apply flex flex-col gap-2;
|
|
}
|
|
|
|
.field span {
|
|
@apply text-sm font-semibold;
|
|
color: #172033;
|
|
}
|
|
|
|
.field input,
|
|
.field select {
|
|
@apply h-11 rounded-[0.5rem] border px-3 text-sm outline-none transition-colors;
|
|
background: #ffffff;
|
|
border-color: rgba(23, 32, 51, 0.1);
|
|
color: #172033;
|
|
}
|
|
|
|
.field input:focus,
|
|
.field select:focus {
|
|
border-color: #0f766e;
|
|
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.12);
|
|
}
|
|
|
|
.primary-button {
|
|
@apply inline-flex h-11 items-center justify-center rounded-[0.5rem] px-4 text-sm font-bold transition-colors;
|
|
background: #172033;
|
|
color: #fffaf2;
|
|
}
|
|
|
|
.secondary-button {
|
|
@apply inline-flex h-11 items-center justify-center rounded-[0.5rem] border px-4 text-sm font-bold transition-colors;
|
|
background: #ffffff;
|
|
border-color: rgba(23, 32, 51, 0.14);
|
|
color: #172033;
|
|
}
|
|
|
|
.primary-button:hover:not(:disabled) {
|
|
background: #0f766e;
|
|
}
|
|
|
|
.secondary-button:hover:not(:disabled) {
|
|
border-color: #0f766e;
|
|
color: #0f766e;
|
|
}
|
|
|
|
.primary-button:disabled,
|
|
.secondary-button:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.56;
|
|
}
|
|
|
|
.invite-list,
|
|
.connector-list,
|
|
.workflow-rule-list,
|
|
.workflow-steps {
|
|
@apply flex flex-col gap-3;
|
|
}
|
|
|
|
.invite-row,
|
|
.empty-state,
|
|
.connector-row,
|
|
.workflow-rule,
|
|
.workflow-toggle,
|
|
.workflow-step {
|
|
@apply rounded-[0.75rem] border px-4 py-4;
|
|
background: rgba(23, 32, 51, 0.04);
|
|
border-color: rgba(23, 32, 51, 0.08);
|
|
}
|
|
|
|
.invite-row {
|
|
@apply flex items-start justify-between gap-4;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.connector-main,
|
|
.workflow-step {
|
|
@apply flex items-start gap-4;
|
|
}
|
|
|
|
.connector-icon,
|
|
.workflow-step-icon {
|
|
@apply inline-flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[0.75rem];
|
|
background: rgba(15, 118, 110, 0.1);
|
|
color: #0f766e;
|
|
}
|
|
|
|
.connector-status {
|
|
@apply inline-flex w-fit items-center rounded-full px-3 py-1 text-xs font-bold uppercase tracking-[0.18em];
|
|
background: rgba(245, 158, 11, 0.14);
|
|
}
|
|
|
|
.connector-link {
|
|
@apply inline-flex h-11 w-fit items-center gap-3 rounded-[0.5rem] px-4 text-sm font-bold no-underline transition;
|
|
background: #172033;
|
|
color: #fffaf2;
|
|
}
|
|
|
|
.connector-link:hover {
|
|
background: #0f766e;
|
|
}
|
|
</style>
|