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 >
0 commit comments