feat: refine content editor layout
This commit is contained in:
@@ -21,6 +21,7 @@ The editor should use one shared content body for every selected target channel,
|
|||||||
- Keep the editor focused on post text and target channels by removing confusing title, calendar, change summary, and base-caption fields.
|
- Keep the editor focused on post text and target channels by removing confusing title, calendar, change summary, and base-caption fields.
|
||||||
- Make target preview tabs compact.
|
- Make target preview tabs compact.
|
||||||
- Move the create content entry point into the top app menu bar.
|
- Move the create content entry point into the top app menu bar.
|
||||||
|
- Remove the asset-management tab from the content detail production panel.
|
||||||
- Preserve existing save payloads and backend contracts.
|
- Preserve existing save payloads and backend contracts.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
@@ -42,3 +43,4 @@ npm run build
|
|||||||
- [x] The editor no longer shows title, calendar, change summary, or base-caption fields.
|
- [x] The editor no longer shows title, calendar, change summary, or base-caption fields.
|
||||||
- [x] Target channel tabs use compact icon-only buttons with channel names as accessible labels and tooltips.
|
- [x] Target channel tabs use compact icon-only buttons with channel names as accessible labels and tooltips.
|
||||||
- [x] Create content is available from the top app menu bar.
|
- [x] Create content is available from the top app menu bar.
|
||||||
|
- [x] Assets are no longer shown in the content detail production panel.
|
||||||
|
|||||||
@@ -335,7 +335,8 @@
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
@reference "@/assets/main.css";
|
@reference "@/assets/main.css";
|
||||||
.approval-panel {
|
.approval-panel {
|
||||||
@apply relative flex w-11 justify-center self-start;
|
@apply relative z-0 flex w-11 justify-center self-start;
|
||||||
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-empty strong,
|
.approval-empty strong,
|
||||||
@@ -365,7 +366,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.approval-step:not(:last-child)::after {
|
.approval-step:not(:last-child)::after {
|
||||||
@apply absolute bottom-[-3.5rem] top-10 border-l-2 border-dashed;
|
@apply absolute bottom-[-3.5rem] top-10 z-0 border-l-2 border-dashed;
|
||||||
content: '';
|
content: '';
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
@@ -377,12 +378,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.step-circle {
|
.step-circle {
|
||||||
@apply relative z-10 flex h-9 w-9 items-center justify-center rounded-full border text-xs font-black transition;
|
@apply relative z-[1] flex items-center justify-center rounded-full border text-xs font-black transition;
|
||||||
|
width: 2.25rem;
|
||||||
|
min-width: 2.25rem;
|
||||||
|
max-width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
max-height: 2.25rem;
|
||||||
|
padding: 0;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
background: var(--app-color-surface);
|
background: var(--app-color-surface);
|
||||||
border-color: rgba(23, 32, 51, 0.16);
|
border-color: rgba(23, 32, 51, 0.16);
|
||||||
color: var(--app-text-muted);
|
color: var(--app-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.step-circle :deep(.v-btn__content) {
|
||||||
|
@apply flex h-full w-full items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
button.step-circle:not(:disabled) {
|
button.step-circle:not(:disabled) {
|
||||||
@apply cursor-pointer shadow-sm;
|
@apply cursor-pointer shadow-sm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
|
|
||||||
const item = ref(null);
|
const item = ref(null);
|
||||||
const revisions = ref([]);
|
const revisions = ref([]);
|
||||||
const assets = ref([]);
|
|
||||||
const comments = ref([]);
|
const comments = ref([]);
|
||||||
const approvals = ref([]);
|
const approvals = ref([]);
|
||||||
const notifications = ref([]);
|
const notifications = ref([]);
|
||||||
@@ -18,8 +17,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
const actions = reactive({
|
const actions = reactive({
|
||||||
revision: false,
|
revision: false,
|
||||||
asset: false,
|
|
||||||
assetRevision: false,
|
|
||||||
comment: false,
|
comment: false,
|
||||||
decision: false,
|
decision: false,
|
||||||
status: false,
|
status: false,
|
||||||
@@ -32,7 +29,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
function reset() {
|
function reset() {
|
||||||
item.value = null;
|
item.value = null;
|
||||||
revisions.value = [];
|
revisions.value = [];
|
||||||
assets.value = [];
|
|
||||||
comments.value = [];
|
comments.value = [];
|
||||||
approvals.value = [];
|
approvals.value = [];
|
||||||
notifications.value = [];
|
notifications.value = [];
|
||||||
@@ -48,14 +44,12 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
const [
|
const [
|
||||||
itemResponse,
|
itemResponse,
|
||||||
revisionsResponse,
|
revisionsResponse,
|
||||||
assetsResponse,
|
|
||||||
commentsResponse,
|
commentsResponse,
|
||||||
approvalsResponse,
|
approvalsResponse,
|
||||||
activityResponse,
|
activityResponse,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
client.get(`/api/content-items/${contentItemId}`),
|
client.get(`/api/content-items/${contentItemId}`),
|
||||||
client.get(`/api/content-items/${contentItemId}/revisions`),
|
client.get(`/api/content-items/${contentItemId}/revisions`),
|
||||||
client.get('/api/assets', { params: { contentItemId } }),
|
|
||||||
client.get('/api/comments', { params: { contentItemId } }),
|
client.get('/api/comments', { params: { contentItemId } }),
|
||||||
client.get('/api/approvals', { params: { contentItemId } }),
|
client.get('/api/approvals', { params: { contentItemId } }),
|
||||||
client.get(`/api/content-items/${contentItemId}/activity`),
|
client.get(`/api/content-items/${contentItemId}/activity`),
|
||||||
@@ -63,7 +57,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
|
|
||||||
item.value = itemResponse.data;
|
item.value = itemResponse.data;
|
||||||
revisions.value = revisionsResponse.data ?? [];
|
revisions.value = revisionsResponse.data ?? [];
|
||||||
assets.value = assetsResponse.data ?? [];
|
|
||||||
comments.value = commentsResponse.data ?? [];
|
comments.value = commentsResponse.data ?? [];
|
||||||
approvals.value = approvalsResponse.data ?? [];
|
approvals.value = approvalsResponse.data ?? [];
|
||||||
activity.value = activityResponse.data ?? [];
|
activity.value = activityResponse.data ?? [];
|
||||||
@@ -91,40 +84,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addGoogleDriveAsset(contentItemId, payload) {
|
|
||||||
actions.asset = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client.post('/api/assets/google-drive', {
|
|
||||||
...payload,
|
|
||||||
contentItemId,
|
|
||||||
workspaceId: currentItemWorkspaceId(),
|
|
||||||
});
|
|
||||||
if (response.data) {
|
|
||||||
assets.value = [...assets.value, response.data];
|
|
||||||
await fetchActivity(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 fetchActivity(contentItemId);
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
} finally {
|
|
||||||
actions.assetRevision = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addComment(contentItemId, payload) {
|
async function addComment(contentItemId, payload) {
|
||||||
actions.comment = true;
|
actions.comment = true;
|
||||||
|
|
||||||
@@ -192,12 +151,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
return 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) {
|
async function fetchNotifications(contentItemId) {
|
||||||
const response = await client.get('/api/notifications', {
|
const response = await client.get('/api/notifications', {
|
||||||
params: {
|
params: {
|
||||||
@@ -218,7 +171,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
return {
|
return {
|
||||||
item,
|
item,
|
||||||
revisions,
|
revisions,
|
||||||
assets,
|
|
||||||
comments,
|
comments,
|
||||||
approvals,
|
approvals,
|
||||||
notifications,
|
notifications,
|
||||||
@@ -229,8 +181,6 @@ export const useContentItemDetailStore = defineStore('content-item-detail', () =
|
|||||||
reset,
|
reset,
|
||||||
fetchContentItemDetail,
|
fetchContentItemDetail,
|
||||||
createRevision,
|
createRevision,
|
||||||
addGoogleDriveAsset,
|
|
||||||
addAssetRevision,
|
|
||||||
addComment,
|
addComment,
|
||||||
submitDecision,
|
submitDecision,
|
||||||
updateStatus,
|
updateStatus,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useSessionStorage } from '@vueuse/core';
|
import { useSessionStorage } from '@vueuse/core';
|
||||||
@@ -53,15 +53,6 @@
|
|||||||
placements: [],
|
placements: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const assetForm = reactive({
|
|
||||||
assetType: 'Image',
|
|
||||||
displayName: '',
|
|
||||||
googleDriveFileId: '',
|
|
||||||
googleDriveLink: '',
|
|
||||||
previewUrl: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const assetRevisionForms = reactive({});
|
|
||||||
const activeProductionTab = ref('comments');
|
const activeProductionTab = ref('comments');
|
||||||
const activeCalendarEvent = ref(null);
|
const activeCalendarEvent = ref(null);
|
||||||
const activePlacementId = ref('');
|
const activePlacementId = ref('');
|
||||||
@@ -177,7 +168,6 @@
|
|||||||
const productionTabs = computed(() => [
|
const productionTabs = computed(() => [
|
||||||
{ key: 'comments', label: 'Comments', count: detailStore.comments.length },
|
{ key: 'comments', label: 'Comments', count: detailStore.comments.length },
|
||||||
{ key: 'revisions', label: 'Revisions', count: detailStore.revisions.length },
|
{ key: 'revisions', label: 'Revisions', count: detailStore.revisions.length },
|
||||||
{ key: 'assets', label: 'Assets', count: detailStore.assets.length },
|
|
||||||
{ key: 'activity', label: 'Activity', count: detailStore.activity.length },
|
{ key: 'activity', label: 'Activity', count: detailStore.activity.length },
|
||||||
]);
|
]);
|
||||||
const workspaceMembers = computed(() =>
|
const workspaceMembers = computed(() =>
|
||||||
@@ -624,6 +614,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAppBarSave() {
|
||||||
|
void saveContent();
|
||||||
|
}
|
||||||
|
|
||||||
async function submitDecision(approvalId, payload) {
|
async function submitDecision(approvalId, payload) {
|
||||||
if (!contentItemId.value) {
|
if (!contentItemId.value) {
|
||||||
return;
|
return;
|
||||||
@@ -640,76 +634,6 @@
|
|||||||
await detailStore.addComment(contentItemId.value, payload);
|
await detailStore.addComment(contentItemId.value, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferGoogleDriveFileId(value) {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
const filePathMatch = trimmed.match(/\/d\/([^/]+)/);
|
|
||||||
const queryMatch = trimmed.match(/[?&]id=([^&]+)/);
|
|
||||||
|
|
||||||
return filePathMatch?.[1] ?? queryMatch?.[1] ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetAssetForm() {
|
|
||||||
assetForm.assetType = 'Image';
|
|
||||||
assetForm.displayName = '';
|
|
||||||
assetForm.googleDriveFileId = '';
|
|
||||||
assetForm.googleDriveLink = '';
|
|
||||||
assetForm.previewUrl = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function linkGoogleDriveAsset() {
|
|
||||||
if (!contentItemId.value || !assetForm.displayName.trim() || !assetForm.googleDriveLink.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const googleDriveFileId = assetForm.googleDriveFileId.trim() || inferGoogleDriveFileId(assetForm.googleDriveLink);
|
|
||||||
|
|
||||||
if (!googleDriveFileId) {
|
|
||||||
saveError.message = 'A Google Drive file id is required when it cannot be read from the link.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await detailStore.addGoogleDriveAsset(contentItemId.value, {
|
|
||||||
assetType: assetForm.assetType,
|
|
||||||
displayName: assetForm.displayName.trim(),
|
|
||||||
googleDriveFileId,
|
|
||||||
googleDriveLink: assetForm.googleDriveLink.trim(),
|
|
||||||
previewUrl: assetForm.previewUrl.trim() || null,
|
|
||||||
});
|
|
||||||
resetAssetForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
function assetRevisionForm(assetId) {
|
|
||||||
if (!assetRevisionForms[assetId]) {
|
|
||||||
assetRevisionForms[assetId] = {
|
|
||||||
sourceReference: '',
|
|
||||||
previewUrl: '',
|
|
||||||
notes: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return assetRevisionForms[assetId];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addAssetRevision(asset) {
|
|
||||||
const draft = assetRevisionForm(asset.id);
|
|
||||||
|
|
||||||
if (!contentItemId.value || !draft.sourceReference.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await detailStore.addAssetRevision(contentItemId.value, asset.id, {
|
|
||||||
sourceReference: draft.sourceReference.trim(),
|
|
||||||
previewUrl: draft.previewUrl.trim() || null,
|
|
||||||
notes: draft.notes.trim() || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
assetRevisionForms[asset.id] = {
|
|
||||||
sourceReference: '',
|
|
||||||
previewUrl: '',
|
|
||||||
notes: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function navigateBackToContent() {
|
async function navigateBackToContent() {
|
||||||
const returnTo = typeof route.query.returnTo === 'string' ? route.query.returnTo : '';
|
const returnTo = typeof route.query.returnTo === 'string' ? route.query.returnTo : '';
|
||||||
const previousPath = router.options.history.state.back;
|
const previousPath = router.options.history.state.back;
|
||||||
@@ -942,8 +866,13 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('content-editor:save', handleAppBarSave);
|
||||||
detailStore.reset();
|
detailStore.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('content-editor:save', handleAppBarSave);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -978,26 +907,6 @@
|
|||||||
<h1>{{ isCreateMode ? 'Compose post' : 'Edit post' }}</h1>
|
<h1>{{ isCreateMode ? 'Compose post' : 'Edit post' }}</h1>
|
||||||
<p>{{ placementSummary || 'Choose target channels and write the post once.' }}</p>
|
<p>{{ placementSummary || 'Choose target channels and write the post once.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions">
|
|
||||||
<div
|
|
||||||
v-if="!isCreateMode && item"
|
|
||||||
class="status-badges"
|
|
||||||
>
|
|
||||||
<span class="meta-chip">{{ item.status }}</span>
|
|
||||||
<span class="meta-chip">{{ item.currentRevisionLabel }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-btn variant="text" :ripple="false"
|
|
||||||
class="primary-button"
|
|
||||||
:disabled="contentItemsStore.isCreating || detailStore.actions.revision"
|
|
||||||
@click="saveContent"
|
|
||||||
>
|
|
||||||
{{ isCreateMode
|
|
||||||
? (contentItemsStore.isCreating ? 'Creating...' : 'Create content')
|
|
||||||
: (detailStore.actions.revision ? 'Saving...' : 'Save revision') }}
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -1253,7 +1162,7 @@
|
|||||||
v-if="isCreateMode"
|
v-if="isCreateMode"
|
||||||
class="empty-note"
|
class="empty-note"
|
||||||
>
|
>
|
||||||
Save the content first to start comments, revisions, assets, and activity.
|
Save the content first to start comments, revisions, and activity.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -1311,126 +1220,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeProductionTab === 'assets'">
|
|
||||||
<div class="panel-stack asset-form">
|
|
||||||
<v-select
|
|
||||||
v-model="assetForm.assetType"
|
|
||||||
:items="['Image', 'Video', 'Document', 'Other']"
|
|
||||||
label="Type"
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="assetForm.displayName"
|
|
||||||
label="Name"
|
|
||||||
placeholder="Final reel, cover image..."
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="assetForm.googleDriveLink"
|
|
||||||
class="field-wide"
|
|
||||||
label="Google Drive link"
|
|
||||||
type="url"
|
|
||||||
placeholder="https://drive.google.com/..."
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="assetForm.googleDriveFileId"
|
|
||||||
label="File id"
|
|
||||||
placeholder="Optional if link includes it"
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="assetForm.previewUrl"
|
|
||||||
label="Preview URL"
|
|
||||||
type="url"
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-btn variant="text" :ripple="false"
|
|
||||||
class="primary-button field-wide"
|
|
||||||
:disabled="detailStore.actions.asset"
|
|
||||||
@click="linkGoogleDriveAsset"
|
|
||||||
>
|
|
||||||
{{ detailStore.actions.asset ? 'Linking...' : 'Link asset' }}
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timeline-list">
|
|
||||||
<article
|
|
||||||
v-for="asset in detailStore.assets"
|
|
||||||
:key="asset.id"
|
|
||||||
class="asset-card"
|
|
||||||
>
|
|
||||||
<div class="timeline-row-header">
|
|
||||||
<div>
|
|
||||||
<strong>{{ asset.displayName }}</strong>
|
|
||||||
<span>{{ asset.assetType }} · {{ asset.sourceType }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="revision-pill">v{{ asset.currentRevisionNumber }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="asset-link"
|
|
||||||
:href="asset.googleDriveLink"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Open source
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="asset-revisions">
|
|
||||||
<div
|
|
||||||
v-for="revision in asset.revisions"
|
|
||||||
:key="revision.id"
|
|
||||||
class="asset-revision-row"
|
|
||||||
>
|
|
||||||
<span>v{{ revision.revisionNumber }}</span>
|
|
||||||
<small>{{ formatDateTime(revision.createdAt) }}</small>
|
|
||||||
<p v-if="revision.notes">{{ revision.notes }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-stack compact-form">
|
|
||||||
<v-text-field
|
|
||||||
v-model="assetRevisionForm(asset.id).sourceReference"
|
|
||||||
class="field-wide"
|
|
||||||
label="New revision reference"
|
|
||||||
type="url"
|
|
||||||
placeholder="Updated Drive link or production reference"
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="assetRevisionForm(asset.id).notes"
|
|
||||||
class="field-wide"
|
|
||||||
label="Notes"
|
|
||||||
placeholder="What changed?"
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
<v-btn variant="text" :ripple="false"
|
|
||||||
class="secondary-button"
|
|
||||||
:disabled="detailStore.actions.assetRevision"
|
|
||||||
@click="addAssetRevision(asset)"
|
|
||||||
>
|
|
||||||
{{ detailStore.actions.assetRevision ? 'Adding...' : 'Add revision' }}
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!detailStore.assets.length"
|
|
||||||
class="empty-note"
|
|
||||||
>
|
|
||||||
No production assets have been linked yet.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="timeline-list">
|
<div class="timeline-list">
|
||||||
<article
|
<article
|
||||||
@@ -1515,9 +1304,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
@apply flex flex-col gap-4 rounded-[1.75rem] border p-6 lg:flex-row lg:items-start lg:justify-between;
|
@apply flex flex-col gap-2 px-1;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
border-color: var(--app-border-subtle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
@@ -1526,7 +1313,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-header h1 {
|
.editor-header h1 {
|
||||||
@apply mt-2 text-4xl font-black;
|
@apply mt-1 text-3xl font-black;
|
||||||
color: var(--app-color-on-surface);
|
color: var(--app-color-on-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1541,19 +1328,7 @@
|
|||||||
color: var(--app-text-muted);
|
color: var(--app-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions,
|
|
||||||
.status-badges {
|
|
||||||
@apply flex flex-wrap items-center gap-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-chip {
|
|
||||||
@apply rounded-full px-4 py-2 text-xs font-bold uppercase tracking-[0.16em];
|
|
||||||
background: var(--app-border-subtle);
|
|
||||||
color: var(--app-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-button,
|
.back-button,
|
||||||
.primary-button,
|
|
||||||
.secondary-button {
|
.secondary-button {
|
||||||
@apply inline-flex items-center justify-center gap-2 rounded-full px-5 py-3 text-sm font-bold transition;
|
@apply inline-flex items-center justify-center gap-2 rounded-full px-5 py-3 text-sm font-bold transition;
|
||||||
}
|
}
|
||||||
@@ -1570,11 +1345,6 @@
|
|||||||
color: var(--app-color-on-primary);
|
color: var(--app-color-on-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button {
|
|
||||||
background: var(--app-color-on-surface);
|
|
||||||
color: var(--app-color-on-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-button {
|
.secondary-button {
|
||||||
background: var(--app-control-hover);
|
background: var(--app-control-hover);
|
||||||
color: var(--app-color-on-surface);
|
color: var(--app-color-on-surface);
|
||||||
@@ -2021,8 +1791,7 @@
|
|||||||
@apply flex items-start justify-between gap-3;
|
@apply flex items-start justify-between gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-row p,
|
.timeline-row p {
|
||||||
.asset-revision-row p {
|
|
||||||
@apply text-sm leading-6;
|
@apply text-sm leading-6;
|
||||||
color: var(--app-color-on-surface);
|
color: var(--app-color-on-surface);
|
||||||
}
|
}
|
||||||
@@ -2056,48 +1825,12 @@
|
|||||||
background: rgba(255, 255, 255, 0.22);
|
background: rgba(255, 255, 255, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-form {
|
|
||||||
@apply grid gap-4 sm:grid-cols-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-card {
|
|
||||||
@apply flex flex-col gap-4 rounded-[1rem] border p-4;
|
|
||||||
background: var(--app-color-on-primary);
|
|
||||||
border-color: var(--app-border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-card .timeline-row-header span,
|
|
||||||
.asset-revision-row small {
|
|
||||||
@apply text-sm leading-6;
|
|
||||||
color: var(--app-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-link {
|
|
||||||
@apply w-fit text-sm font-semibold;
|
|
||||||
color: var(--app-color-on-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.revision-pill {
|
.revision-pill {
|
||||||
@apply rounded-full px-3 py-1 text-xs font-bold;
|
@apply rounded-full px-3 py-1 text-xs font-bold;
|
||||||
background: rgba(15, 118, 110, 0.12);
|
background: rgba(15, 118, 110, 0.12);
|
||||||
color: var(--app-color-on-tertiary);
|
color: var(--app-color-on-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-revisions {
|
|
||||||
@apply flex flex-col gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-revision-row {
|
|
||||||
@apply rounded-[0.875rem] border px-3 py-2;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
border-color: var(--app-border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-revision-row span {
|
|
||||||
@apply mr-2 text-sm font-bold;
|
|
||||||
color: var(--app-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-form {
|
.compact-form {
|
||||||
@apply gap-3;
|
@apply gap-3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import WorkspaceSelector from './WorkspaceSelector.vue';
|
import WorkspaceSelector from './WorkspaceSelector.vue';
|
||||||
import {
|
import {
|
||||||
mdiCalendar,
|
mdiCalendar,
|
||||||
|
mdiCheck,
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
mdiEmailOutline,
|
mdiEmailOutline,
|
||||||
@@ -91,6 +92,10 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dispatchContentEditorSave() {
|
||||||
|
window.dispatchEvent(new CustomEvent('content-editor:save'));
|
||||||
|
}
|
||||||
|
|
||||||
const appBarActions = computed(() => {
|
const appBarActions = computed(() => {
|
||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
return [];
|
return [];
|
||||||
@@ -99,7 +104,6 @@
|
|||||||
switch (route.name) {
|
switch (route.name) {
|
||||||
case 'workspace-dashboard':
|
case 'workspace-dashboard':
|
||||||
case 'content-items':
|
case 'content-items':
|
||||||
case 'content-item-detail':
|
|
||||||
return authStore.isManager || authStore.isProvider
|
return authStore.isManager || authStore.isProvider
|
||||||
? [{
|
? [{
|
||||||
key: 'create-content',
|
key: 'create-content',
|
||||||
@@ -108,6 +112,14 @@
|
|||||||
route: { name: 'content-item-create' },
|
route: { name: 'content-item-create' },
|
||||||
}]
|
}]
|
||||||
: [];
|
: [];
|
||||||
|
case 'content-item-create':
|
||||||
|
case 'content-item-detail':
|
||||||
|
return [{
|
||||||
|
key: 'save-content',
|
||||||
|
label: route.name === 'content-item-create' ? 'Create content' : 'Save revision',
|
||||||
|
icon: mdiCheck,
|
||||||
|
handler: dispatchContentEditorSave,
|
||||||
|
}];
|
||||||
case 'campaigns':
|
case 'campaigns':
|
||||||
return [{
|
return [{
|
||||||
key: 'create-campaign',
|
key: 'create-campaign',
|
||||||
|
|||||||
Reference in New Issue
Block a user