Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Expand Up @@ -49,14 +49,28 @@
:label="$tr('versionDescriptionLabel')"
:invalid="!isDescriptionValid"
:invalidText="$tr('descriptionRequiredMessage')"
:showInvalidText="showInvalidText"
:showInvalidText="showDescriptionInvalidText"
autofocus
textArea
/>
</KGridItem>
<KGridItem :layout="{ span: 1 }">
<HelpTooltip :text="$tr('descriptionDescriptionTooltip')" bottom />
</KGridItem>

<KGridItem v-show="showLanguageDropdown" :layout="{ span: 11 }">
<KSelect
v-model="language"
:label="$tr('languageLabel')"
:invalid="showLanguageInvalidText"
:invalidText="$tr('languageRequiredMessage')"
:options="languages"
@change="showLanguageInvalidText = !isLanguageValid"
/>
</KGridItem>
<KGridItem v-show="showLanguageDropdown" :layout="{ span: 1 }">
<HelpTooltip :text="$tr('languageDescriptionTooltip')" bottom />
</KGridItem>
</KFixedGrid>
</KModal>

Expand All @@ -71,6 +85,7 @@
import { mapActions, mapGetters } from 'vuex';
import HelpTooltip from 'shared/views/HelpTooltip';
import { forceServerSync } from 'shared/data/serverSync';
import { LanguagesList } from 'shared/leUtils/Languages';

export default {
name: 'PublishModal',
Expand All @@ -88,9 +103,13 @@
step: 0,
publishDescription: '',
size: 0,
showInvalidText: false, // lazy validation
showDescriptionInvalidText: false, // lazy validation
showLanguageInvalidText: false,
loading: false,
loadingTaskId: null,
language: {},
channelLanguages: [],
channelLanguageExists: true,
};
},
computed: {
Expand All @@ -112,9 +131,34 @@
isDescriptionValid() {
return this.publishDescription && this.publishDescription.trim();
},
isLanguageValid() {
return Object.keys(this.language).length > 0;
},
sizeCalculationTask() {
return this.loadingTaskId ? this.getAsyncTask(this.loadingTaskId) : null;
},
isCheffedChannel() {
return Boolean(this.currentChannel.ricecooker_version);
},
isPrivateChannel() {
return this.currentChannel.public;
},
isFirstPublish() {
return this.currentChannel.version === 0;
},
defaultLanguage() {
const channelLang = this.filterLanguages(l => l.id === this.currentChannel.language)[0];
return this.languages.some(lang => lang.value === channelLang.value) ? channelLang : {};
},
showLanguageDropdown() {
return (
((this.isCheffedChannel || this.isPrivateChannel) && this.isFirstPublish) ||
!this.channelLanguageExists
);
},
languages() {
return this.filterLanguages(l => this.channelLanguages.includes(l.id));
},
},
watch: {
sizeCalculationTask(task) {
Expand All @@ -139,26 +183,56 @@
// this.loading = response.stale;
// this.loadingTaskId = response.changes.length ? response.changes[0].key : null;
// });
this.channelLanguageExistsInResources().then(exists => {
this.channelLanguageExists = exists;
if (!exists) {
this.getLanguagesInChannelResources().then(languages => {
this.channelLanguages = languages.length ? languages : [this.currentChannel.language];
this.language = this.defaultLanguage;
});
} else {
this.channelLanguages = [this.currentChannel.language];
this.language = this.defaultLanguage;
}
});
},
methods: {
...mapActions('currentChannel', ['publishChannel']),
...mapActions('channel', ['updateChannel']),
...mapActions('currentChannel', [
'publishChannel',
'channelLanguageExistsInResources',
'getLanguagesInChannelResources',
]),
close() {
this.publishDescription = '';
this.language = this.defaultLanguage;
this.dialog = false;
},
validate() {
this.showInvalidText = true;
return this.isDescriptionValid;
this.showDescriptionInvalidText = !this.isDescriptionValid;
this.showLanguageInvalidText = !this.isLanguageValid;
return !this.showDescriptionInvalidText && !this.showLanguageInvalidText;
},
async handlePublish() {
if (this.validate()) {
if (!this.areAllChangesSaved) {
await forceServerSync();
}

this.publishChannel(this.publishDescription).then(this.close);
this.updateChannel({
id: this.currentChannel.id,
language: this.language.value,
}).then(() => {
this.publishChannel(this.publishDescription).then(this.close);
});
}
},
filterLanguages(filterFn) {
return LanguagesList.filter(filterFn).map(l => ({
value: l.id,
label: l.native_name,
}));
},
},
$trs: {
// Incomplete channel window
Expand All @@ -174,8 +248,11 @@
descriptionRequiredMessage: "Please describe what's new in this version before publishing",
descriptionDescriptionTooltip:
'This description will be shown to Kolibri admins before they update channel versions',
languageDescriptionTooltip: 'The default language for a channel and its resources',
Copy link
Member Author

Choose a reason for hiding this comment

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

@marcellamaki a heads up on the strings added

cancelButton: 'Cancel',
publishButton: 'Publish',
languageLabel: 'Language',
languageRequiredMessage: 'Please select a language for this channel',
Copy link
Member Author

Choose a reason for hiding this comment

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

@marcellamaki a heads up on the strings added

},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,21 @@ describe('publishModal', () => {
});
});
describe('on publish step', () => {
const updateChannel = jest.fn();
const publishChannel = jest.fn();

beforeEach(() => {
wrapper.setData({ step: steps.PUBLISH });
wrapper.setMethods({
publishChannel: () => {
updateChannel: () => {
return new Promise(resolve => {
updateChannel();
publishChannel();
resolve();
});
},
});
updateChannel.mockReset();
publishChannel.mockReset();
});
it('publish button should trigger form validation', () => {
Expand All @@ -110,20 +114,26 @@ describe('publishModal', () => {
"Please describe what's new in this version before publishing"
);
});
it('publishing should be blocked if no description is given', () => {
it('publishing should be blocked if no description & language are given', () => {
wrapper
.find('[data-test="confirm-publish-modal"]')
.find('form')
.trigger('submit');
expect(updateChannel).not.toHaveBeenCalled();
expect(publishChannel).not.toHaveBeenCalled();
});
it('publish button should call publishChannel if description is given', () => {
it('publish button should call updateChannel if description and language are given', () => {
const description = 'Version notes';
wrapper.setData({ publishDescription: description });
const language = {
value: 'en',
label: 'English',
};
wrapper.setData({ publishDescription: description, language });
wrapper
.find('[data-test="confirm-publish-modal"]')
.find('form')
.trigger('submit');
expect(updateChannel).toHaveBeenCalled();
expect(publishChannel).toHaveBeenCalled();
});
it('cancel button on publish step should also close modal', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,23 @@ describe('currentChannel store', () => {
spy.mockRestore();
});
});
it('channelLanguageExistsInResources action should call `language_exists` endpoint', () => {
const spy = jest
.spyOn(Channel, 'languageExistsInResources')
.mockImplementation(() => Promise.resolve());
return store.dispatch('currentChannel/channelLanguageExistsInResources').then(() => {
expect(spy.mock.calls[0][0]).toBe(store.state.currentChannel.currentChannelId);
spy.mockRestore();
});
});
it('getLanguagesInChannelResources action should call `languages` endpoint', () => {
const spy = jest
.spyOn(Channel, 'languagesInResources')
.mockImplementation(() => Promise.resolve());
return store.dispatch('currentChannel/getLanguagesInChannelResources').then(() => {
expect(spy.mock.calls[0][0]).toBe(store.state.currentChannel.currentChannelId);
spy.mockRestore();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,13 @@ export function deployCurrentChannel(context) {
export function publishChannel(context, version_notes) {
return Channel.publish(context.state.currentChannelId, version_notes);
}

export function channelLanguageExistsInResources(context) {
const channelId = context.state.currentChannelId;
return Channel.languageExistsInResources(channelId);
}

export function getLanguagesInChannelResources(context) {
const channelId = context.state.currentChannelId;
return Channel.languagesInResources(channelId);
}
39 changes: 39 additions & 0 deletions contentcuration/contentcuration/frontend/shared/data/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import matches from 'lodash/matches';
import overEvery from 'lodash/overEvery';
import pick from 'lodash/pick';
import sortBy from 'lodash/sortBy';
import compact from 'lodash/compact';
import uniq from 'lodash/uniq';
import uniqBy from 'lodash/uniqBy';

Expand Down Expand Up @@ -1235,6 +1236,44 @@ export const Channel = new CreateModelResource({
// For channels, the appropriate channel_id for a change is just the key
return obj.id;
},
async languageExistsInResources(id) {
let langExists = await this.transaction(
{ mode: 'r' },
TABLE_NAMES.CHANNEL,
TABLE_NAMES.CONTENTNODE,
() => {
return Channel.table.get(id).then(async channel => {
return (
(await ContentNode.table
.where({
channel_id: id,
language: channel.language,
})
.count()) > 0
);
});
}
);
if (!langExists) {
langExists = await client
.get(this.getUrlFunction('language_exists')(id))
.then(response => response.data.exists);
}
return langExists;
},
async languagesInResources(id) {
const localLanguages = await this.transaction({ mode: 'r' }, TABLE_NAMES.CONTENTNODE, () => {
return ContentNode.table
.where({ channel_id: id })
.filter(node => node.language !== null)
.toArray()
.then(nodes => nodes.map(node => node.language));
});
const remoteLanguages = await client
.get(this.getUrlFunction('languages')(id))
.then(response => response.data.languages);
return uniq(compact(localLanguages.concat(remoteLanguages)));
},
});

function getChannelFromChannelScope() {
Expand Down
Loading