Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<template>

<KModal :title="previewYourDraftTitle$()">
<template #default>
<div>
<strong>{{ draftTokenLabel$() }}</strong>
<StudioCopyToken
:token="channel.draft_token"
:showLabel="false"
/>
<p class="mt-16">{{ channelTokenDescription$() }}</p>
</div>
</template>
<template #actions>
<KButton
:text="dismissAction$()"
@click="handleDismiss"
/>
</template>
</KModal>

</template>


<script setup>

import { onMounted } from 'vue';
import StudioCopyToken from 'shared/views/StudioCopyToken';
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
import { commonStrings } from 'shared/strings/commonStrings';
import logging from 'shared/logging';

const props = defineProps({
channel: {
type: Object,
required: true,
},
});

const emit = defineEmits(['close']);

const { dismissAction$ } = commonStrings;
const { previewYourDraftTitle$, draftTokenLabel$, channelTokenDescription$ } =
communityChannelsStrings;

function handleDismiss() {
emit('close');
}

onMounted(() => {
if (!props.channel.draft_token) {
// If there is no draft token, we can't preview the draft, so we just close the modal
logging.error(
'Attempted to open PreviewDraftChannelModal without a draft token. Closing modal.',
{ channelId: props.channel.id },
);
handleDismiss();
}
});

</script>


<style lang="scss" scoped>

.mt-16 {
margin-top: 16px;
}

</style>
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@

<template #default>
<div>
<KRadioButtonGroup>
<component :is="showDraftMode ? 'KRadioButtonGroup' : 'div'">
<KRadioButton
v-if="showDraftMode"
:label="modeLive$()"
:buttonValue="PublishModes.LIVE"
:currentValue="mode"
Expand All @@ -26,7 +27,7 @@
<div
v-if="mode === PublishModes.LIVE"
class="live-mode-content"
style="margin-top: 16px; margin-left: 24px"
:style="showDraftMode ? { marginLeft: '24px', marginTop: '16px' } : {}"
>
<div class="live-publish-info">
<KIcon
Expand Down Expand Up @@ -121,13 +122,14 @@
</div>

<KRadioButton
v-if="showDraftMode"
:label="modeDraft$()"
:buttonValue="PublishModes.DRAFT"
:currentValue="mode"
:description="modeDraftDescription$()"
@input="mode = PublishModes.DRAFT"
/>
</KRadioButtonGroup>
</component>
</div>
</template>

Expand Down Expand Up @@ -160,6 +162,7 @@
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
import { LanguagesList } from 'shared/leUtils/Languages';
import logging from 'shared/logging';
import { FeatureFlagKeys } from 'shared/constants';

export default {
name: 'PublishSidePanel',
Expand Down Expand Up @@ -208,12 +211,14 @@
cancelAction$,
languageLabel$,
languageRequiredMessage$,
draftBeingPublishedNotice$,
versionNotesRequiredMessage$,
} = communityChannelsStrings;

const currentChannel = computed(() => store.getters['currentChannel/currentChannel']);
const getContentNode = computed(() => store.getters['contentNode/getContentNode']);
const areAllChangesSaved = computed(() => store.getters['areAllChangesSaved']);
const hasFeatureEnabled = computed(() => store.getters['hasFeatureEnabled']);

const incompleteResourcesCount = computed(() => {
if (!currentChannel.value) return 0;
Expand Down Expand Up @@ -252,6 +257,10 @@
return true;
});

const showDraftMode = computed(() =>
hasFeatureEnabled.value(FeatureFlagKeys.test_dev_feature),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Clean feature-flag gating of the draft publish mode. The dynamic component :is pattern to switch between KRadioButtonGroup and div is a nice way to preserve the live-publish UI for non-flagged users.

);

const submitText = computed(() => {
return mode.value === PublishModes.DRAFT ? saveDraft$() : publishAction$();
});
Expand Down Expand Up @@ -354,7 +363,10 @@
}

if (mode.value === PublishModes.DRAFT) {
await Channel.publishDraft(currentChannel.value.id, { use_staging_tree: false });
await Channel.publishDraft(currentChannel.value.id, {
use_staging_tree: false,
});
store.dispatch('showSnackbarSimple', draftBeingPublishedNotice$());
emit('close');
} else {
// `newChannelLanguage.value` is a KSelect option { value, label }, so we need to
Expand Down Expand Up @@ -383,6 +395,7 @@
isChannelLanguageLoading,
PublishModes,
mode,
showDraftMode,
version_notes,
submitting,
languageOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const renderComponent = (props = {}) => {
store.commit('channel/ADD_CHANNEL', currentChannel);
store.commit('contentNode/ADD_CONTENTNODE', rootNode);
store.commit('SET_UNSAVED_CHANGES', props.areAllChangesSaved === false);
store.commit('UPDATE_SESSION', { is_admin: true });

const router = new VueRouter();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@
</VListTileTitle>
</VListTile>
</template>
<VListTile
v-if="currentChannel && currentChannel.draft_token"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This menu item is only gated by currentChannel.draft_token being truthy, not by the test_dev_feature feature flag. If user A (with the flag) publishes a draft, and user B (without the flag) opens the same channel, user B would see this menu item and the draft token. Is this intentional? If draft tokens should only be visible to users with the feature flag, this should also check hasFeatureEnabled(FeatureFlagKeys.test_dev_feature). The same applies to the draft token row in StudioDetailsPanel.vue (line 51).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is okay, yes. We will only hide the draft option radio button for users who do not have the test_dev_feature feature flag.

@click="showPreviewDraftModal = true"
>
<VListTileTitle>{{ getDraftTokenAction$() }}</VListTileTitle>
</VListTile>
<VListTile
v-if="isPublished"
@click="showTokenModal = true"
Expand Down Expand Up @@ -277,6 +283,11 @@
@delete="handleDelete"
@close="showDeleteModal = false"
/>
<PreviewDraftChannelModal
v-if="showPreviewDraftModal && currentChannel"
:channel="currentChannel"
@close="showPreviewDraftModal = false"
/>
<VSpeedDial
v-if="showClipboardSpeedDial"
v-model="showClipboard"
Expand Down Expand Up @@ -340,6 +351,7 @@
<script>

import { mapActions, mapGetters, mapState } from 'vuex';
import PreviewDraftChannelModal from '../../components/modals/PreviewDraftChannelModal.vue';
import Clipboard from '../../components/Clipboard';
import SyncResourcesModal from '../sync/SyncResourcesModal';
import ProgressModal from '../progress/ProgressModal';
Expand All @@ -361,6 +373,8 @@
import DraggableRegion from 'shared/views/draggable/DraggableRegion';
import { DropEffect } from 'shared/mixins/draggable/constants';
import DraggablePlaceholder from 'shared/views/draggable/DraggablePlaceholder';
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
import { commonStrings } from 'shared/strings/commonStrings';

export default {
name: 'TreeViewBase',
Expand All @@ -381,8 +395,15 @@
DraggablePlaceholder,
SavingIndicator,
QuickEditModal,
PreviewDraftChannelModal,
},
mixins: [titleMixin],
setup() {
const { getDraftTokenAction$ } = communityChannelsStrings;
return {
getDraftTokenAction$,
};
},
props: {
loading: {
type: Boolean,
Expand All @@ -398,6 +419,7 @@
showSyncModal: false,
showClipboard: false,
showDeleteModal: false,
showPreviewDraftModal: false,
syncing: false,
resubmitToCommunityLibraryModalData: null,
};
Expand Down Expand Up @@ -533,6 +555,22 @@
},
immediate: true,
},
isDraftPublishing(newVal, oldVal) {
if (!newVal && oldVal) {
const { draftPublishedNotice$ } = communityChannelsStrings;
const { previewAction$ } = commonStrings;
const snackbarData = {
text: draftPublishedNotice$(),
};
if (this.currentChannel.draft_token) {
snackbarData.actionText = previewAction$();
snackbarData.actionCallback = () => {
this.showPreviewDraftModal = true;
};
}
this.$store.dispatch('showSnackbar', snackbarData);
}
},
},
methods: {
...mapActions('channel', ['deleteChannel']),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ export const commonStrings = createTranslator('CommonStrings', {
message: 'Sorry! Something went wrong, please try again.',
context: 'Default error message for operation errors.',
},
previewAction: {
message: 'Preview',
context: 'A label for an action that opens a preview of content',
},
dismissAction: {
message: 'Dismiss',
context: 'A label for an action that dismisses a notification or message',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,32 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin
context:
'Notice for screen readers on the new notifications badge to indicate that new notifications have arrived',
},

// Draft channel strings
draftBeingPublishedNotice: {
message: 'Draft version is being published',
context: 'Label indicating that a draft version of the channel is currently being published',
},
draftPublishedNotice: {
message: 'Draft published successfully',
context: 'Label indicating that a draft version of the channel has been successfully published',
},
previewYourDraftTitle: {
message: 'Preview your draft channel in Kolibri',
context: 'Title for the modal that shows instructions to preview a draft channel in Kolibri',
},
channelTokenDescription: {
message:
'To preview your draft channel right away, simply copy the unique draft channel token. This is the sole method to access the channel.',
context: 'Description for the channel token field in the draft preview instructions modal',
},
getDraftTokenAction: {
message: 'Get draft token',
context:
'Button text for the action to retrieve the draft token in the draft preview instructions modal',
},
draftTokenLabel: {
message: 'Draft token',
context: 'Label for the draft token field in the draft preview instructions modal',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,23 @@
:showLabel="false"
/>
<span v-else>
{{ _details.primary_token.slice(0, 5) + '-' + _details.primary_token.slice(5) }}
{{ hyphenateToken(_details.primary_token) }}
</span>
</template>
</StudioDetailsRow>
<StudioDetailsRow
v-if="_details.draft_token"
:label="draftTokenLabel$()"
>
<template #default>
<StudioCopyToken
v-if="!printing"
:token="_details.draft_token"
:style="{ maxWidth: 'max-content' }"
:showLabel="false"
/>
<span v-else>
{{ hyphenateToken(_details.draft_token) }}
</span>
</template>
</StudioDetailsRow>
Expand Down Expand Up @@ -390,6 +406,8 @@
import StudioDetailsRow from './StudioDetailsRow';
import StudioThumbnail from 'shared/views/files/StudioThumbnail';
import StudioCopyToken from 'shared/views/StudioCopyToken';
import useToken from 'shared/composables/useToken';
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';

const DEFAULT_DETAILS = {
name: '',
Expand Down Expand Up @@ -442,6 +460,14 @@
titleMixin,
metadataTranslationMixin,
],
setup() {
const { hyphenateToken } = useToken();
const { draftTokenLabel$ } = communityChannelsStrings;
return {
hyphenateToken,
draftTokenLabel$,
};
},
props: {
// Object matching that returned by the channel details and
// node details API endpoints, see backend for details of the
Expand Down
6 changes: 6 additions & 0 deletions contentcuration/contentcuration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,12 @@ def get_resource_count(self):
def get_human_token(self):
return self.secret_tokens.get(is_primary=True)

def get_draft_token(self):
draft_version = self.channel_versions.filter(version=None).first()
if not draft_version:
return None
return draft_version.secret_token

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (UNADDRESSED from prior review): get_draft_token() and the _annotate_draft_token subquery in channel.py both rely on the version=None convention for identifying draft channel versions. A backend test covering this method (and the annotation returning the correct token) would guard against regressions if the ChannelVersion schema or draft lifecycle changes.


def get_channel_id_token(self):
return self.secret_tokens.get(token=self.id)

Expand Down
Loading