Skip to content

Commit 8352770

Browse files
committed
Update channel publishing UX to support publishing channel draft versions
fix some typo fix a bug modal fix some bugs fix frame bug
1 parent 0fd741d commit 8352770

5 files changed

Lines changed: 390 additions & 21 deletions

File tree

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
<template>
2+
<div>
3+
<SidePanelModal
4+
alignment="right"
5+
:ariaLabel="$tr('publishPanelAria')"
6+
@keyup.esc="onClose"
7+
@closePanel="onClose"
8+
>
9+
<template #header>
10+
<h2 style="margin: 0">{{ getPanelTitle() }}</h2>
11+
</template>
12+
13+
<template #default>
14+
<div class="form-section">
15+
<VRadioGroup v-model="mode">
16+
<VRadio
17+
:label="$tr('modeLive')"
18+
value="live"
19+
/>
20+
<div class="radio-description">{{ getLiveModeDescription() }}</div>
21+
22+
<!-- Live mode content nested under the radio button -->
23+
<div v-if="mode === 'live'" class="live-mode-content" style="margin-left: 24px; margin-top: 16px;">
24+
<div class="info-section">
25+
<VIconWrapper class="info-icon">info</VIconWrapper>
26+
<span>{{ $tr('publishingInfo', { version: currentChannel.version, time: formattedPublishTime }) }}</span>
27+
</div>
28+
29+
<div class="form-section" style="width: 100%; max-width: 100%;">
30+
<div class="textarea-container">
31+
<label class="label">{{ $tr('versionNotesLabel') }}</label>
32+
<textarea
33+
v-model="version_notes"
34+
:rows="4"
35+
:maxlength="30"
36+
class="custom-textarea"
37+
:placeholder="$tr('versionNotesLabel')"
38+
></textarea>
39+
<div class="char-counter">{{ version_notes.length }} / 30</div>
40+
</div>
41+
</div>
42+
43+
<div v-if="incompleteResourcesCount > 0" class="form-section warning-section">
44+
<div class="warning-content">
45+
<VIconWrapper class="warning-icon">warning</VIconWrapper>
46+
<div class="warning-text">
47+
<div class="warning-title">{{ $tr('incompleteResourcesWarning', { count: incompleteResourcesCount }) }}</div>
48+
<div class="warning-description">{{ $tr('incompleteResourcesDescription') }}</div>
49+
</div>
50+
</div>
51+
</div>
52+
</div>
53+
54+
<VRadio
55+
:label="$tr('modeDraft')"
56+
value="draft"
57+
/>
58+
<div class="radio-description">{{ $tr('modeDraftDescription') }}</div>
59+
</VRadioGroup>
60+
</div>
61+
</template>
62+
63+
<template #bottomNavigation>
64+
<div class="footer">
65+
<VBtn flat @click="onClose">{{ $tr('cancel') }}</VBtn>
66+
<VBtn
67+
color="primary"
68+
:disabled="submitting"
69+
@click="submit"
70+
>
71+
{{ getButtonText() }}
72+
</VBtn>
73+
</div>
74+
</template>
75+
</SidePanelModal>
76+
</div>
77+
</template>
78+
79+
<script>
80+
import SidePanelModal from 'shared/views/SidePanelModal';
81+
import VIconWrapper from 'shared/views/VIconWrapper';
82+
import { Channel } from 'shared/data/resources';
83+
import { forceServerSync } from 'shared/data/serverSync';
84+
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
85+
86+
import { mapGetters } from 'vuex';
87+
88+
export default {
89+
name: 'PublishSidePanel',
90+
components: {
91+
SidePanelModal,
92+
VIconWrapper,
93+
},
94+
props: {
95+
open: { type: Boolean, required: true },
96+
channelId: { type: String, required: true },
97+
},
98+
emits: ['close', 'submitted'],
99+
data() {
100+
return {
101+
mode: 'live',
102+
version_notes: '',
103+
submitting: false,
104+
};
105+
},
106+
computed: {
107+
...mapGetters('currentChannel', ['currentChannel']),
108+
...mapGetters('contentNode', ['getContentNode']),
109+
formattedPublishTime() {
110+
if (!this.currentChannel) return '';
111+
const now = new Date();
112+
const timeString = now.toLocaleTimeString('en-US', {
113+
hour: 'numeric',
114+
minute: '2-digit',
115+
hour12: true
116+
});
117+
const dateString = now.toLocaleDateString('en-US', {
118+
month: 'numeric',
119+
day: 'numeric',
120+
year: 'numeric'
121+
});
122+
return `${timeString} - ${dateString}`;
123+
},
124+
incompleteResourcesCount() {
125+
if (!this.currentChannel) return 0;
126+
const rootNode = this.getContentNode(this.currentChannel.root_id);
127+
return rootNode ? rootNode.error_count || 0 : 0;
128+
},
129+
},
130+
methods: {
131+
onClose() {
132+
if (!this.submitting) this.$emit('close');
133+
},
134+
async submit() {
135+
try {
136+
this.submitting = true;
137+
if (this.mode === 'draft') {
138+
await Channel.publishDraft(this.currentChannel.id, { use_staging_tree: false });
139+
await forceServerSync();
140+
this.$emit('submitted');
141+
this.$emit('close');
142+
} else {
143+
await Channel.publish(this.channelId, {
144+
version_notes: this.version_notes,
145+
});
146+
this.$emit('submitted');
147+
this.$emit('close');
148+
}
149+
} catch (error) {
150+
this.$store.dispatch('shared/handleAxiosError', error);
151+
} finally {
152+
this.submitting = false;
153+
}
154+
},
155+
getPanelTitle() {
156+
return this.mode === 'draft' ? this.$tr('publishToLibrary') : this.$tr('publishChannel');
157+
},
158+
getLiveModeDescription() {
159+
return this.$tr('modeLiveDescription');
160+
},
161+
getButtonText() {
162+
return this.mode === 'draft' ? this.$tr('saveDraft') : this.$tr('publishLive');
163+
},
164+
},
165+
$trs: {
166+
publishToLibrary: 'Publish to library',
167+
publishChannel: 'Publish channel',
168+
publishPanelAria: 'Publish channel side panel',
169+
publishLive: 'PUBLISH',
170+
saveDraft: 'SAVE DRAFT',
171+
modeLive: 'Live',
172+
modeDraft: 'Draft (staging)',
173+
versionNotesLabel: 'Describe what\'s new in this channel version',
174+
cancel: 'CANCEL',
175+
modeLiveDescription: 'This edition will be accessible to the public through the Kolibri public library.',
176+
modeDraftDescription: 'Your channel will be saved as a draft, allowing you to review or conduct quality checks without altering the published version on Kolibri public library.',
177+
publishingInfo: 'You\'re publishing: Version {version} ({time})',
178+
incompleteResourcesWarning: '{count} incomplete resources.',
179+
incompleteResourcesDescription: 'Incomplete resources will not be published and made available for download in Kolibri. Click \'Publish\' to confirm that you would like to publish anyway.',
180+
maxLengthError: 'Maximum 30 characters allowed',
181+
},
182+
};
183+
</script>
184+
185+
<style scoped>
186+
.form-section {
187+
margin: 16px 0;
188+
}
189+
.label {
190+
display: block;
191+
font-weight: 600;
192+
margin-bottom: 6px;
193+
}
194+
.footer {
195+
display: flex;
196+
justify-content: flex-end;
197+
gap: 8px;
198+
padding: 12px 0;
199+
}
200+
.radio-description {
201+
margin-left: 24px;
202+
margin-bottom: 16px;
203+
color: rgba(0, 0, 0, 0.6);
204+
font-size: 14px;
205+
}
206+
.info-section {
207+
display: flex;
208+
align-items: center;
209+
gap: 8px;
210+
padding: 12px;
211+
background-color: #f5f5f5;
212+
border-radius: 4px;
213+
}
214+
.info-icon {
215+
color: #1976d2;
216+
}
217+
.warning-section {
218+
margin-top: 16px;
219+
}
220+
.warning-content {
221+
display: flex;
222+
align-items: flex-start;
223+
gap: 12px;
224+
padding: 16px;
225+
background-color: #fff3cd;
226+
border: 1px solid #ffeaa7;
227+
border-radius: 8px;
228+
}
229+
.warning-icon {
230+
color: #f39c12;
231+
flex-shrink: 0;
232+
margin-top: 2px;
233+
}
234+
.warning-text {
235+
flex: 1;
236+
}
237+
.warning-title {
238+
font-weight: bold;
239+
color: #e74c3c;
240+
margin-bottom: 8px;
241+
}
242+
.warning-description {
243+
color: #6c757d;
244+
line-height: 1.4;
245+
}
246+
.textarea-container {
247+
width: 100%;
248+
}
249+
.custom-textarea {
250+
width: 100%;
251+
min-height: 100px;
252+
padding: 12px;
253+
border: 1px solid #e0e0e0;
254+
border-radius: 4px;
255+
font-family: inherit;
256+
font-size: 14px;
257+
line-height: 1.5;
258+
resize: vertical;
259+
box-sizing: border-box;
260+
}
261+
.char-counter {
262+
text-align: center;
263+
color: #666;
264+
font-size: 12px;
265+
margin-top: 4px;
266+
}
267+
</style>

contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
</KRouterLink>
5454
</VToolbarItems>
5555
<VSpacer />
56-
<SavingIndicator v-if="!offline" />
56+
<SavingIndicator v-if="!offline && !isDraftPublishing" />
5757
<OfflineText indicator />
5858
<ProgressModal />
5959
<div
@@ -207,9 +207,12 @@
207207
/>
208208
<slot></slot>
209209

210-
<PublishModal
211-
v-if="showPublishModal"
212-
v-model="showPublishModal"
210+
<PublishSidePanel
211+
v-if="showPublishSidePanel && currentChannel && currentChannel.id"
212+
:open="showPublishSidePanel"
213+
:channelId="currentChannel.id"
214+
@close="showPublishSidePanel = false"
215+
@submitted="showPublishSidePanel = false"
213216
/>
214217
<template v-if="isPublished">
215218
<ChannelTokenModal
@@ -313,7 +316,7 @@
313316
import Clipboard from '../../components/Clipboard';
314317
import SyncResourcesModal from '../sync/SyncResourcesModal';
315318
import ProgressModal from '../progress/ProgressModal';
316-
import PublishModal from '../../components/publish/PublishModal';
319+
317320
import QuickEditModal from '../../components/QuickEditModal';
318321
import SavingIndicator from '../../components/edit/SavingIndicator';
319322
import { DraggableRegions, DraggableUniverses, RouteNames } from '../../constants';
@@ -328,14 +331,15 @@
328331
import DraggableRegion from 'shared/views/draggable/DraggableRegion';
329332
import { DropEffect } from 'shared/mixins/draggable/constants';
330333
import DraggablePlaceholder from 'shared/views/draggable/DraggablePlaceholder';
334+
import PublishSidePanel from '../../components/sidePanels/PublishSidePanel';
331335
332336
export default {
333337
name: 'TreeViewBase',
334338
components: {
335339
DraggableRegion,
336340
MainNavigationDrawer,
337341
ToolBar,
338-
PublishModal,
342+
PublishSidePanel,
339343
ProgressModal,
340344
ChannelTokenModal,
341345
SyncResourcesModal,
@@ -357,7 +361,7 @@
357361
data() {
358362
return {
359363
drawer: false,
360-
showPublishModal: false,
364+
showPublishSidePanel: false,
361365
showTokenModal: false,
362366
showSyncModal: false,
363367
showClipboard: false,
@@ -389,9 +393,12 @@
389393
isRicecooker() {
390394
return Boolean(this.currentChannel.ricecooker_version);
391395
},
396+
isDraftPublishing() {
397+
return this.currentChannel && this.currentChannel.publishing && this.currentChannel.publishing_draft;
398+
},
392399
disablePublish() {
393400
return (
394-
this.currentChannel.publishing ||
401+
(this.currentChannel.publishing && !this.currentChannel.publishing_draft) ||
395402
!this.isChanged ||
396403
!this.currentChannel.language ||
397404
(this.rootNode && !this.rootNode.resource_count)
@@ -502,7 +509,9 @@
502509
this.trackClickEvent('Delete channel');
503510
},
504511
publishChannel() {
505-
this.showPublishModal = true;
512+
console.log('publishChannel called, currentChannel:', this.currentChannel);
513+
console.log('Setting showPublishSidePanel to true');
514+
this.showPublishSidePanel = true;
506515
this.trackClickEvent('Publish');
507516
},
508517
trackClickEvent(eventLabel) {

0 commit comments

Comments
 (0)