diff --git a/.eslintrc b/.eslintrc index a17938ce3b..0ab1e29f68 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,9 @@ rules: vue/v-bind-style: 2 vue/v-on-style: 2 vue/html-quotes: [2, 'double'] + require-path-exists/exists: + - 2 + - webpackConfigPath: webpack.config.js env: browser: true commonjs: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ca46c241..f6a49b5b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,15 @@ ## Upcoming release #### Changes * [[@jayoshih](https://github.com/jayoshih)] Updated README -* +* [[@jayoshih](https://github.com/jayoshih)] Vueified channel list page #### Issues Resolved * [#875 Make sure files referenced by both orphan and non-orphan nodes aren't deleted](https://github.com/learningequality/studio/issues/875) -* +* [#1120](https://github.com/learningequality/studio/issues/1120) +* [#900](https://github.com/learningequality/studio/issues/900) +* [#875 Make sure files referenced by both orphan and non-orphan nodes aren't deleted](https://github.com/learningequality/studio/issues/875) + + ## 2019-02-11 Release #### Changes diff --git a/__mocks__/vue.js b/__mocks__/vue.js new file mode 100644 index 0000000000..1b4435843a --- /dev/null +++ b/__mocks__/vue.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +var vueIntl = require("vue-intl"); + +// Just copy/pasted code from utils/translations.js here +// as trying to import the actual file led to some errors + +var translations = window.ALL_MESSAGES || {}; // Set in django + +// Flatten translation dictionary +var unnested_translations = {}; +Object.keys(translations).forEach(function (key) { + Object.keys(translations[key]).forEach(function(nestedKey) { + unnested_translations[key + "." + nestedKey] = translations[key][nestedKey]; + }); +}); + +Vue.use(vueIntl, {"defaultLocale": "en"}); + +var currentLanguage = "en"; +if (global.languageCode) { + currentLanguage = global.languageCode; + Vue.setLocale(currentLanguage); +} + +Vue.registerMessages(currentLanguage, unnested_translations); +Vue.prototype.$tr = function $tr(messageId, args) { + const nameSpace = this.$options.name; + if (args) { + if (!Array.isArray(args) && typeof args !== 'object') { + logging.error(`The $tr functions take either an array of positional + arguments or an object of named options.`); + } + } + const defaultMessageText = this.$options.$trs[messageId]; + const message = { + id: `${nameSpace}.${messageId}`, + defaultMessage: defaultMessageText, + }; + + return this.$formatMessage(message, args); +}; + +module.exports = Vue; diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/constants.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/constants.js new file mode 100644 index 0000000000..670efdbbf7 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/constants.js @@ -0,0 +1,19 @@ +export const ListTypes = { + EDITABLE: 'EDITABLE', + STARRED: 'STARRED', + VIEW_ONLY: 'VIEW_ONLY', + PUBLIC: 'PUBLIC', + CHANNEL_SETS: 'CHANNEL_SETS' +}; + +export const ChannelListUrls = { + [ListTypes.EDITABLE]: window.Urls.get_user_edit_channels(), + [ListTypes.STARRED]: window.Urls.get_user_bookmarked_channels(), + [ListTypes.VIEW_ONLY]: window.Urls.get_user_view_channels(), + [ListTypes.PUBLIC]: window.Urls.get_user_public_channels() +} + +export const ChannelInvitationMapping = { + 'edit': ListTypes.EDITABLE, + 'view': ListTypes.VIEW_ONLY +} diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/mixins.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/mixins.js new file mode 100644 index 0000000000..5b6cb833f0 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/mixins.js @@ -0,0 +1,59 @@ +import _ from 'underscore'; +import { mapGetters, mapActions, mapMutations, mapState } from 'vuex'; +import { createTranslator } from 'utils/i18n'; +import { dialog, alert } from 'edit_channel/utils/dialog'; + +const channelStrings = createTranslator('ChannelStrings', { + unsavedChanges: "Unsaved Changes!", + unsavedChangesText: "Exiting now will undo any new changes. Are you sure you want to exit?", + dontSave: "Discard Changes", + keepOpen: "Keep Editing", + save: "Save", + errorChannelSave: "Error Saving Channel" +}); + +exports.setChannelMixin = { + computed: { + ...mapState('channel_list', ['activeChannel']), + ...mapState('channel_list', ['changed']), + channelStrings() { + return channelStrings; + } + }, + methods: { + ...mapActions('channel_list', ['saveChannel']), + ...mapMutations('channel_list', { + setActiveChannel: 'SET_ACTIVE_CHANNEL', + cancelChanges: 'CANCEL_CHANNEL_CHANGES' + }), + setChannel: function (channelID) { + // Check for changes here when user switches or closes panel + if(this.changed && channelID !== this.activeChannel.id) { + dialog(this.channelStrings("unsavedChanges"), this.channelStrings("unsavedChangesText"), { + [this.channelStrings("dontSave")]: () => { + this.cancelChanges(); + this.setActiveChannel(channelID); + }, + [this.channelStrings("keepOpen")]:() => {}, + [this.channelStrings("save")]: () => { + this.saveChannel().then(() => { + this.setActiveChannel(channelID); + }).catch( (error) => { + alert(this.channelStrings('errorChannelSave'), error.responseText || error); + }); + }, + }, null); + } else { + this.setActiveChannel(channelID); + } + } + } +}; + +exports.tabMixin = { + mounted() { + this.$nextTick(() => { + this.$refs.firstTab.focus(); + }); + } +}; diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDeleteSection.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDeleteSection.spec.js new file mode 100644 index 0000000000..d2a60ad6d7 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDeleteSection.spec.js @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils'; +import ChannelDeleteSection from './../views/ChannelDeleteSection.vue'; +import _ from 'underscore'; +import { localStore, mockFunctions } from './data.js'; + + +function makeWrapper(props = {}) { + return mount(ChannelDeleteSection, { + store: localStore + }) +} + +describe('channelDeleteSection', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + + it('clicking delete channel should open a dialog warning', () => { + wrapper.find('.delete-channel').trigger('click'); + expect(mockFunctions.deleteChannel).not.toHaveBeenCalled(); + + expect(document.querySelector('#dialog-box')).toBeTruthy(); + wrapper.vm.deleteChannel(wrapper.channel); + expect(mockFunctions.deleteChannel).toHaveBeenCalled(); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDetailsPanel.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDetailsPanel.spec.js new file mode 100644 index 0000000000..520a812c50 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDetailsPanel.spec.js @@ -0,0 +1,33 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelDetailsPanel from './../views/ChannelDetailsPanel.vue'; +import _ from 'underscore'; +import { localStore, mockFunctions } from './data.js'; + + +function makeWrapper(props = {}) { + let channel = { + id: 'test', + main_tree: "abc", + ...props + } + localStore.commit('channel_list/RESET_STATE', channel) + localStore.commit('channel_list/ADD_CHANNEL', channel) + localStore.commit('channel_list/SET_ACTIVE_CHANNEL', channel.id) + return shallowMount(ChannelDetailsPanel, { + store: localStore + }) +} + +describe('channelDetailsPanel', () => { + it('panel should set background as thumbnail', () => { + let wrapper = makeWrapper({'thumbnail_url': 'test.png'}); + let panel = wrapper.find('#channel-preview-wrapper'); + let expectedStyle = "background-image: url('test.png')"; + expect(panel.attributes('style')).toContain('test.png'); + }); + it('panel should set background as default thumbnail for new channels', () => { + let wrapper = makeWrapper(); + let panel = wrapper.find('#channel-preview-wrapper'); + expect(panel.attributes('style')).toContain('kolibri_placeholder.png'); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDownloadDropdown.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDownloadDropdown.spec.js new file mode 100644 index 0000000000..63f8fcde9d --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelDownloadDropdown.spec.js @@ -0,0 +1,41 @@ +import { mount } from '@vue/test-utils'; +import ChannelDownloadDropdown from './../views/ChannelDownloadDropdown.vue'; +import _ from 'underscore'; +import { localStore, mockFunctions } from './data.js'; + + +function makeWrapper(props = {}) { + return mount(ChannelDownloadDropdown, { + store: localStore + }) +} + +const testChannel = { id: 'test', name: 'test' } +localStore.commit('channel_list/ADD_CHANNEL', testChannel) + + +describe('channelDownloadDropdown', () => { + let wrapper; + beforeEach(() => { + localStore.commit('channel_list/SET_ACTIVE_CHANNEL', testChannel.id); + mockFunctions.downloadChannelDetails.mockReset(); + wrapper = makeWrapper(); + }); + + it('clicking a download link should trigger a download', () => { + wrapper.find('li a').trigger('click'); + expect(mockFunctions.downloadChannelDetails).toHaveBeenCalled(); + }); + it('requesting a format should trigger corresponding format download', () => { + function test(format) { + wrapper.vm.downloadDetails(format); + let args = mockFunctions.downloadChannelDetails.mock.calls[0][1]; + expect(args.format).toEqual(format); + mockFunctions.downloadChannelDetails.mockReset(); + } + test('pdf'); + test('csv'); + test('detailedPdf'); + test('ppt'); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelEditor.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelEditor.spec.js new file mode 100644 index 0000000000..9e3ed3f014 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelEditor.spec.js @@ -0,0 +1,122 @@ +import { mount } from '@vue/test-utils'; +import ChannelEditor from './../views/ChannelEditor.vue'; +import { localStore, mockFunctions } from './data.js'; +require('handlebars/helpers'); // Needed for image uploader + +let testChannel = { + 'id': 'test', + 'name': 'channel', + 'description': "description", + 'language': 'en', +} + +function makeWrapper(props = {}) { + let channel = { + ...testChannel, + ...props + }; + localStore.commit('channel_list/RESET_STATE'); + localStore.commit('channel_list/ADD_CHANNEL', channel); + localStore.commit('channel_list/SET_ACTIVE_CHANNEL', channel.id); + return mount(ChannelEditor, { + store: localStore + }); +} + +describe('channelEditor', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + + describe('on load', () => { + it('changed is false', () => { + expect(wrapper.vm.changed).toBe(false); + }); + it('thumbnail is shown', () => { + let thumbnail = "thumbnail.png"; + let thumbnailWrapper = makeWrapper({'thumbnail_url': thumbnail}); + expect(thumbnailWrapper.find('img').attributes('src')).toContain(thumbnail); + }); + it('default thumbnail is shown if channel has no thumbnail', () => { + expect(wrapper.find('img').attributes('src')).toContain('kolibri_placeholder.png') + }); + it('.channel-name is set to channel.name', () => { + expect(wrapper.find('.channel-name').element.value).toEqual(testChannel.name); + }); + it('.channel-description is set to channel.description', () => { + expect(wrapper.find('.channel-description').element.value).toEqual(testChannel.description); + }); + it('#select-language is set to channel.language_id', () => { + expect(wrapper.find('#select-language').element.value).toEqual(testChannel.language); + }); + }); + + describe('changes registered', () => { + beforeEach(() => { + localStore.commit('channel_list/SET_CHANGED', false); + }); + it('setChannelThumbnail sets channel.thumbnail', () => { + let thumbnail = 'newthumbnail.png' + wrapper.vm.setChannelThumbnail(thumbnail, {'new encoding': thumbnail}, thumbnail, thumbnail) + expect(wrapper.vm.changed).toBe(true); + expect(wrapper.vm.channel.thumbnail).toEqual(thumbnail); + expect(wrapper.vm.channel.thumbnail_encoding).toEqual({'new encoding': thumbnail}); + }); + it('removeChannelThumbnail removes channel.thumbnail', () => { + let thumbnailWrapper = makeWrapper({'thumbnail': 'thumbnail.png', 'thumbnail_encoding': {'test': 'test'}}) + thumbnailWrapper.vm.removeChannelThumbnail(); + expect(wrapper.vm.changed).toBe(true); + expect(wrapper.vm.channel.thumbnail).toEqual(""); + expect(wrapper.vm.channel.thumbnail_encoding).toEqual({}); + }); + it('typing in .channel-name sets channel.name', () => { + let newName = 'new channel name'; + let nameInput = wrapper.find('.channel-name'); + nameInput.element.value = newName; + nameInput.trigger('input'); + nameInput.trigger('blur'); + expect(wrapper.vm.changed).toBe(true); + expect(wrapper.vm.channel.name).toEqual(newName); + }); + it('setting .channel-description sets channel.description', () => { + let newDescription = "new channel description"; + let descriptionInput = wrapper.find('.channel-description'); + descriptionInput.element.value = newDescription; + descriptionInput.trigger('input'); + descriptionInput.trigger('blur'); + expect(wrapper.vm.changed).toBe(true); + expect(wrapper.vm.channel.description).toEqual(newDescription); + }); + it('setting #select-language sets channel.language_id', () => { + let newLanguage = "en"; + let languageDropdown = wrapper.find('#select-language'); + languageDropdown.element.value = newLanguage; + languageDropdown.trigger('input'); + languageDropdown.trigger('blur'); + expect(wrapper.vm.changed).toBe(true); + expect(wrapper.vm.channel.language).toEqual(newLanguage); + }); + }) + + it('clicking CANCEL cancels edits', () => { + let nameInput = wrapper.find('.channel-name'); + nameInput.element.value = 'new channel name'; + nameInput.trigger('input'); + nameInput.trigger('blur'); + + // Cancel changes + wrapper.find(".cancel-edits").trigger('click'); + expect(wrapper.vm.changed).toBe(false); + expect(wrapper.vm.channel.name).toEqual('channel'); + expect(wrapper.emitted().cancelEdit).toBeTruthy(); + }); + + it('clicking SAVE saves edits', () => { + wrapper.find('form').trigger('submit'); + expect(mockFunctions.saveChannel).toHaveBeenCalled(); + wrapper.vm.$nextTick(() => { + expect(wrapper.emitted('submitChanges')).toBeTruthy(); + }); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelInvitationItem.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelInvitationItem.spec.js new file mode 100644 index 0000000000..a445ef1cf8 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelInvitationItem.spec.js @@ -0,0 +1,56 @@ +import { mount } from '@vue/test-utils'; +import ChannelInvitationItem from './../views/ChannelInvitationItem.vue'; +import _ from 'underscore'; +import { localStore, mockFunctions } from './data.js'; + + +function makeWrapper(share_mode) { + let invitation = { + "id": "inviteid", + "share_mode": share_mode || "edit", + "sender": { + "first_name": "First", + "last_name": "Last" + } + }; + localStore.commit('channel_list/SET_INVITATION_LIST', [invitation]); + return mount(ChannelInvitationItem, { + store: localStore, + propsData: { + invitationID: invitation.id + } + }) +} + +describe('channelInvitationItem', () => { + let wrapper; + beforeEach(() => { + + wrapper = makeWrapper(); + mockFunctions.declineInvitation.mockReset(); + mockFunctions.acceptInvitation.mockReset(); + }); + + it('clicking ACCEPT button should accept invitation', () => { + let acceptButton = wrapper.find('.accept-invitation'); + acceptButton.trigger('click'); + expect(mockFunctions.declineInvitation).not.toHaveBeenCalled(); + expect(mockFunctions.acceptInvitation).toHaveBeenCalled(); + }); + it('clicking DECLINE button should decline invitation', () => { + let declineButton = wrapper.find('.decline-invitation'); + declineButton.trigger('click'); + expect(mockFunctions.acceptInvitation).not.toHaveBeenCalled(); + expect(mockFunctions.declineInvitation).not.toHaveBeenCalled(); + + expect(document.querySelector('#dialog-box')).toBeTruthy(); + wrapper.vm.declineInvitation(wrapper.vm.invitation); + expect(mockFunctions.declineInvitation).toHaveBeenCalled(); + }); + it('clicking X should dismiss invitation', () => { + expect(localStore.state.channel_list.invitations).toHaveLength(1); + wrapper.setData({accepted: true}); + wrapper.find('.remove').trigger('click'); + expect(localStore.state.channel_list.invitations).toHaveLength(0); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelInvitationList.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelInvitationList.spec.js new file mode 100644 index 0000000000..34f7b78f99 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelInvitationList.spec.js @@ -0,0 +1,28 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelInvitationList from './../views/ChannelInvitationList.vue'; +import ChannelInvitationItem from './../views/ChannelInvitationItem.vue'; +import _ from 'underscore'; +import { Invitations, localStore, mockFunctions } from './data'; + + +function makeWrapper() { + return shallowMount(ChannelInvitationList, { + store: localStore + }) +} + +describe('channelInvitationList', () => { + + it('loadChannelInvitationList should be called', () => { + let listWrapper = makeWrapper(); + expect(mockFunctions.loadChannelInvitationList).toHaveBeenCalled(); + }) + + it('list should load all invitations', () => { + let listWrapper = makeWrapper(); + listWrapper.vm.$nextTick().then(() => { + let actualLength = listWrapper.findAll(ChannelInvitationItem).length; + expect(actualLength).toEqual(Invitations.length); + }); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelItem.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelItem.spec.js new file mode 100644 index 0000000000..bf7438ee05 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelItem.spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelItem from './../views/ChannelItem.vue'; +import _ from 'underscore'; +import { localStore } from './data.js'; +import { ListTypes } from './../constants'; + +const testChannel = { + id: "test channel", + name: "test title", + modified: new Date() +}; + +function makeWrapper(props = {}) { + localStore.commit('channel_list/SET_CHANNEL_LIST', {channels: [testChannel], listType: ListTypes.EDITABLE}); + return shallowMount(ChannelItem, { + store: localStore, + propsData: { + channelID: testChannel.id + } + }) +} + +describe('channelItem', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + + it('should load channel information', () => { + expect(wrapper.text()).toEqual(expect.stringContaining(testChannel.name)) + }); + it('should set the store.activeChannel', () => { + let channel = wrapper.find('.channel-container-wrapper'); + channel.trigger('click'); + expect(wrapper.vm.activeChannel.id).toEqual(testChannel.id) + }); + it('CTRL + click should open new window', () => { + global.open = jest.fn(); + let event = { metaKey: true }; + wrapper.vm.openChannel(event); + expect(global.open).toBeCalled(); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelList.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelList.spec.js new file mode 100644 index 0000000000..a40b42f7c2 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelList.spec.js @@ -0,0 +1,115 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelList from './../views/ChannelList.vue'; +import ChannelItem from './../views/ChannelItem.vue'; +import _ from 'underscore'; +import { Channels, localStore, mockFunctions } from './data'; +import { ListTypes } from './../constants'; + + +function makeWrapper(listType) { + return shallowMount(ChannelList, { + store: localStore, + propsData: { + listType: listType || ListTypes.EDITABLE + }, + }); +} + + +describe('channelList', () => { + let editListWrapper; + let starredListWrapper; + let publicListWrapper; + let viewonlyListWrapper; + + beforeEach(() => { + editListWrapper = makeWrapper(ListTypes.EDITABLE); + starredListWrapper = makeWrapper(ListTypes.STARRED); + publicListWrapper = makeWrapper(ListTypes.PUBLIC); + viewonlyListWrapper = makeWrapper(ListTypes.VIEW_ONLY); + }); + + describe('populating lists', () => { + function testAll(test) { + test(editListWrapper); + test(starredListWrapper); + test(publicListWrapper); + test(viewonlyListWrapper); + } + + it('loadChannelList should be called', () => { + expect(mockFunctions.loadChannelList).toHaveBeenCalled(); + }) + + it('channels are shown in their respective lists', () => { + function test(wrapper) { + let channels = wrapper.findAll(ChannelItem); + let expectedLength = _.where(Channels, {[wrapper.props('listType')]: true}).length; + expect(channels).toHaveLength(expectedLength); + } + testAll(test); + }) + + it('new channels are added to the edit list', () => { + let newchannel = {"id": "new", "name": "test"}; + localStore.commit('channel_list/SUBMIT_CHANNEL', newchannel); + function test(wrapper) { + let match = _.find(wrapper.vm.listChannels, {id: "new"}); + if(wrapper.props('listType') === ListTypes.EDITABLE) { + expect(match).toBeTruthy(); + } else { + expect(match).toBeUndefined(); + } + } + testAll(test); + }) + + it('channel edits are reflected across lists', () => { + let edittedChannel = {"id": "channel5", "name": "test"}; + localStore.commit('channel_list/SUBMIT_CHANNEL', edittedChannel); + function test(wrapper) { + let match = _.find(wrapper.vm.listChannels, {id: "channel5"}); + + // If a channel is part of a list, make sure the name change has been registered + if(match) { + expect(match.name).toBe("test"); + } + } + testAll(test); + }) + + it('starred channels are added to the starred list', () => { + let channel = localStore.state.channel_list.channels[0]; + channel.STARRED = true; + function test(wrapper) { + let match = _.find(wrapper.vm.listChannels, {id: channel.id}); + + // Make sure starred channels are added to the starred list + // Otherwise, make sure the STARRED property has been updated + if(wrapper.props("listType") === ListTypes.STARRED) { + expect(match).toBeTruthy(); + } else if(match) { + expect(match.STARRED).toBe(true); + } + } + testAll(test); + }) + + it('unstarred channels are removed from the starred list', () => { + let channel = _.find(localStore.state.channel_list.channels, {STARRED: true}); + channel.STARRED = false; + function test(wrapper) { + let match = _.find(wrapper.vm.listChannels, {id: channel.id}); + + // Make sure unstarred channels are removed from the starred list + // Otherwise, make sure the STARRED property has been updated + if(wrapper.props("listType") === ListTypes.STARRED) { + expect(match).toBeUndefined(); + } else if(match) { + expect(match.STARRED).toBe(false); + } + } + testAll(test); + }) + }) +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelListPage.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelListPage.spec.js new file mode 100644 index 0000000000..31d43ad727 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelListPage.spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelListPage from './../views/ChannelListPage.vue'; +import ChannelDetailsPanel from './../views/ChannelDetailsPanel.vue'; +import { ListTypes } from './../constants'; +import { localStore } from './data' +import _ from 'underscore'; + +function makeWrapper(props = {}) { + return shallowMount(ChannelListPage, { + store: localStore + }) +} + +describe('channelListPage', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + it('on LOAD all list types have a tab', () => { + let expectedTabLength = _.values(ListTypes).length; + expect(wrapper.findAll("#manage-channel-nav li")).toHaveLength(expectedTabLength); + }) + it('on CLICK tab must navigate to corresponding list', () => { + let results = wrapper.findAll("#manage-channel-nav li"); + _.each(_.range(0, results.length), (index) => { + let tab = results.at(index); + tab.trigger('click'); + expect(tab.is('.active')).toBe(true); + expect(wrapper.vm.$tr(wrapper.vm.activeList)).toEqual(tab.text()); + }) + }) + it('details panel should toggle when active channel is set/unset', () => { + let channel = {'id': 'id', 'channel': 'channel'}; + wrapper.vm.$store.commit('channel_list/ADD_CHANNEL', channel); + expect(wrapper.find(ChannelDetailsPanel).exists()).toBe(false); + wrapper.vm.$store.commit('channel_list/SET_ACTIVE_CHANNEL', channel.id); + expect(wrapper.find(ChannelDetailsPanel).exists()).toBe(true); + wrapper.vm.$store.commit('channel_list/SET_ACTIVE_CHANNEL', null); + expect(wrapper.find(ChannelDetailsPanel).exists()).toBe(false); + wrapper.vm.$store.commit('channel_list/SET_ACTIVE_CHANNEL', ""); + expect(wrapper.find(ChannelDetailsPanel).exists()).toBe(true); + }) +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelListStore.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelListStore.spec.js new file mode 100644 index 0000000000..c642809448 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelListStore.spec.js @@ -0,0 +1,178 @@ +import { mount } from '@vue/test-utils'; +import _ from 'underscore'; +import store from './../vuex/store'; +import { ListTypes } from './../constants'; +import { Invitations } from './data'; +const Backbone = require('backbone'); + +/* + TODO: there are some issues trying to mock jquery.ajax as it + throws a `TypeError: Cannot read property 'prototype' of undefined` + due to how it interacts with Backbone. Added TODOs where we'll need + to test +*/ + +describe('channelListStore', () => { + let channel; + beforeEach(() => { + channel = {'id': 'test', 'name': 'channel', 'main_tree': 'node'}; + store.commit('channel_list/RESET_STATE'); + }) + afterEach(() => { + jest.clearAllMocks(); + }) + + describe('channel actions', () => { + describe('loadChannelList', () => { + it('should get list of channels', () => { + _.each(_.values(ListTypes), (type) => { + store.dispatch('channel_list/loadChannelList', type); + // TODO: make sure ChannelListUrls[type] endpoint is called + }); + }); + describe('adding channels to the store', () => { + let channels; + let payload1; + let payload2; + + beforeEach(() => { + store.commit('channel_list/RESET_STATE'); + channels = store.state.channel_list.channels; + payload1 = { + listType: ListTypes.EDITABLE, + channels: [{'id': 'channel 1'}, {'id': 'channel 2'}] + }; + payload2 = { + listType: ListTypes.VIEW_ONLY, + channels: [{'id': 'channel 1'}, {'id': 'channel 3'}] + }; + }) + + it('should merge the channels to one list', () => { + store.commit('channel_list/SET_CHANNEL_LIST', payload1); + expect(channels).toHaveLength(2); + store.commit('channel_list/SET_CHANNEL_LIST', payload2); + expect(channels).toHaveLength(3); + }) + + it('should mark relevant list types as true', () => { + store.commit('channel_list/SET_CHANNEL_LIST', payload1); + store.commit('channel_list/SET_CHANNEL_LIST', payload2); + expect(_.find(channels, {[ListTypes.EDITABLE]: true, id: "channel 1"})).toBeTruthy(); + expect(_.find(channels, {[ListTypes.EDITABLE]: true, id: "channel 2"})).toBeTruthy(); + expect(_.find(channels, {[ListTypes.EDITABLE]: false, id: "channel 3" })).toBeTruthy(); + expect(_.find(channels, {[ListTypes.VIEW_ONLY]: true, id: "channel 1" })).toBeTruthy(); + expect(_.find(channels, {[ListTypes.VIEW_ONLY]: false, id: "channel 2" })).toBeTruthy(); + expect(_.find(channels, {[ListTypes.VIEW_ONLY]: true, id: "channel 3" })).toBeTruthy(); + }) + }) + }) + + describe('star actions', () => { + it('addStar should post a star request', () => { + // store.dispatch('channel_list/addStar', channel) + // TODO: add_bookmark endpoint should be called + }); + it('removeStar should post a star request', () => { + // store.dispatch('channel_list/removeStar', channel) + // TODO: remove_bookmark endpoint should be called + }); + }) + describe('channel edit actions', () => { + beforeEach(() => { + store.commit('channel_list/RESET_STATE'); + store.commit('channel_list/ADD_CHANNEL', channel); + store.commit('channel_list/SET_ACTIVE_CHANNEL', channel.id); + }) + + it('saveChannel should save changes', () => { + store.commit('channel_list/SET_CHANNEL_NAME', 'new name'); + store.dispatch('channel_list/saveChannel').then(() => { + // TODO: Need to mock endpoint to get rid of UnhandledPromiseRejectionWarning + // Needs to be patch as post will overwrite editors + expect(Backbone.sync.mock.calls[0][0]).toEqual('patch'); + expect(Backbone.sync.mock.calls[0][1].attributes.name).toEqual('new name'); + expect(store.state.channel_list.changed).toBe(false); + expect(store.state.channel_list.activeChannel.id).toBe("test"); + }); + }); + + it('deleteChannel should mark channel.deleted as true', () => { + store.commit('channel_list/SET_CHANNELSET_LIST', [{id:'channelset', channels:['test']}]); + store.dispatch('channel_list/deleteChannel', channel.id).then(() => { + expect(Backbone.sync.mock.calls[0][1].attributes.deleted).toBe(true); + expect(_.find(store.state.channel_list.channels, {'id': 'test'})).toBeFalsy(); + expect(store.state.channel_list.channelSets[0].channels).toHaveLength(0); + }); + }); + }); + + it('loadNodeDetails should generate channel details', () => { + // store.dispatch('channel_list/loadNodeDetails', channel.main_tree); + // TODO: get_topic_details endpoint should be called + }); + + it('should start pdf download', () => { + // store.dispatch('channel_list/downloadChannelDetails', {id: 'test', format: 'detailedPDF'}); + // TODO: make sure get_channel_details_pdf_endpoint endpoint is called + // store.dispatch('channel_list/downloadChannelDetails', {id: 'test', format: 'pdf'}); + // TODO: make sure get_channel_details_pdf_endpoint endpoint is called + // store.dispatch('channel_list/downloadChannelDetails', {id: 'test', format: 'csv'}); + // TODO: make sure get_channel_details_csv_endpoint endpoint is called + // store.dispatch('channel_list/downloadChannelDetails', {id: 'test', format: 'ppt'}); + // TODO: make sure get_channel_details_ppt_endpoint endpoint is called + }); + }); + + describe('channel set actions', () => { + beforeEach(() => { + + }) + it('loadChannelSetList should get list of channel sets', () => { + // store.dispatch('channel_list/loadChannelSetList'); + // TODO: get_user_channel_sets endpoint should be called + }); + it('deleteChannelSet should delete the channel set', () => { + let channelSet = {'id': 'test'}; + store.commit('channel_list/SET_CHANNELSET_LIST', [channelSet]); + store.dispatch('channel_list/deleteChannelSet', channelSet.id).then(() => { + expect(Backbone.sync.mock.calls[0][0]).toEqual('delete'); + expect(Backbone.sync.mock.calls[0][1].attributes.id).toEqual(channelSet.id); + expect(_.find(store.state.channel_list.channelSets, {id: channelSet.id})).toBeFalsy(); + }); + }); + }); + + describe('invitation actions', () => { + beforeEach(() => { + store.commit('channel_list/RESET_STATE'); + }) + it('loadChannelInvitationList should get list of invitations', () => { + // store.dispatch('channel_list/loadChannelInvitationList'); + // TODO: get_user_pending_channels endpoint should be called + }); + it('acceptInvitation should accept the invitation', () => { + // _.each(Invitations, (invitation) => { + // store.dispatch('channel_list/acceptInvitation', invitation).then(() => { + // TODO: accept_channel_invite endpoint should be called + // TODO: Make sure proper edit/view access is given + // TODO: Make sure channels are added to correct lists + // jest.clearAllMocks(); + // }); + // }); + }); + + it('declineInvitation should decline the invitation', () => { + _.each(Invitations, (invitation) => { + store.commit('channel_list/SET_INVITATION_LIST', [invitation]); + store.dispatch('channel_list/declineInvitation', invitation.id).then(() => { + // TODO: Need to mock endpoint to get rid of UnhandledPromiseRejectionWarning + expect(Backbone.sync.mock.calls[0][0]).toEqual('delete'); + expect(Backbone.sync.mock.calls[0][1].attributes.id).toEqual(invitation.id); + jest.clearAllMocks(); + }); + }) + }); + }); + +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelMetadataSection.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelMetadataSection.spec.js new file mode 100644 index 0000000000..43c5d8c3e9 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelMetadataSection.spec.js @@ -0,0 +1,51 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelMetadataSection from './../views/ChannelMetadataSection.vue'; +import _ from 'underscore'; +import { localStore, mockFunctions } from './data.js'; +import { ListTypes } from './../constants'; +import State from 'edit_channel/state'; + +State.current_user = {'id': 'testuser'} + +const testChannel = { + 'id': 'test', + 'name': 'channel', + 'created': new Date(), +} +function makeWrapper(props = {}) { + localStore.commit('channel_list/RESET_STATE'); + let channel = { + ...testChannel, + ...props + } + localStore.commit('channel_list/ADD_CHANNEL', channel); + localStore.commit('channel_list/SET_ACTIVE_CHANNEL', channel.id); + return shallowMount(ChannelMetadataSection, { + store: localStore + }) +} + +describe('channelMetadataSection', () => { + it('channels with edit access should show EDIT DETAILS button', () => { + let wrapper = makeWrapper({editors: ['testuser']}); + expect(wrapper.find('#edit-details').exists()).toBe(true); + }); + it('channels with view only access should hide EDIT DETAILS button', () => { + let wrapper = makeWrapper({editors: []}); + expect(wrapper.find('#edit-details').exists()).toBe(false); + }); + it('ricecooker channels should hide EDIT DETAILS button', () => { + let wrapper = makeWrapper({editors: ['testuser'], ricecooker_version: 'rice'}); + expect(wrapper.find('#edit-details').exists()).toBe(false); + }); + it('OPEN CHANNEL button should link to edit mode for editable channels', () => { + let wrapper = makeWrapper({editors: ['testuser']}); + let openButton = wrapper.find('#open-channel'); + expect(openButton.attributes('href')).toEqual('channel'); + }); + it('OPEN CHANNEL button should link to view only mode for view only channels', () => { + let wrapper = makeWrapper({editors: []}); + let openButton = wrapper.find('#open-channel'); + expect(openButton.attributes('href')).toEqual('channel_view_only'); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelSetItem.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelSetItem.spec.js new file mode 100644 index 0000000000..36d59680b2 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelSetItem.spec.js @@ -0,0 +1,53 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelSetItem from './../views/ChannelSetItem.vue'; +import { localStore, mockFunctions } from './data.js'; + +function makeWrapper(props = {}) { + let channelSet = { + id: 'channel-set-id', + name: "test title", + channels: ['test'], + secret_token: { + display_token: 'test-test' + } + } + + localStore.commit('channel_list/ADD_CHANNEL', {'id': 'test'}) + localStore.commit('channel_list/SET_CHANNELSET_LIST', [channelSet]) + + return shallowMount(ChannelSetItem, { + store: localStore, + propsData: { + channelSetID: channelSet.id + } + }) +} + +describe('channelSetItem', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + + it('should show correct number of channels', () => { + let metadata = wrapper.find('.channel-metadata') + expect(metadata.text()).toEqual(expect.stringContaining("1 Channel")); + }); + it('on DELETE click, dialog should be shown', () => { + wrapper.find('.delete-channelset').trigger('click') + expect(mockFunctions.deleteChannelSet).not.toHaveBeenCalled(); + expect(document.querySelector('#dialog-box')).toBeTruthy(); + }); + it('on DELETE, channel set should be deleted', () => { + wrapper.vm.deleteChannelSet(wrapper.vm.channelSet); + expect(mockFunctions.deleteChannelSet).toHaveBeenCalled(); + }); + it('on channel delete, channel should be removed from channel', () => { + localStore.commit('channel_list/REMOVE_CHANNEL', 'test'); + expect(wrapper.vm.channelSet.channels).not.toContain('test'); + }); + it('on click should open channel set modal', () => { + wrapper.find('.channel-container-wrapper').trigger('click'); + // TODO: check for channel_set/views/ChannelSetModal + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelSetList.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelSetList.spec.js new file mode 100644 index 0000000000..dd422ebd9a --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelSetList.spec.js @@ -0,0 +1,40 @@ +import { shallowMount } from '@vue/test-utils'; +import ChannelSetList from './../views/ChannelSetList.vue'; +import ChannelSetItem from './../views/ChannelSetItem.vue'; +import { ChannelSets, localStore, mockFunctions } from './data'; +import _ from 'underscore'; +require('handlebars/helpers'); // Needed for collection details modal + +function makeWrapper() { + return shallowMount(ChannelSetList, { + store: localStore + }) +} + +describe('channelSetList', () => { + let listWrapper; + + beforeEach(() => { + listWrapper = makeWrapper(); + }) + + it('loadChannelSetList should be called', () => { + expect(mockFunctions.loadChannelSetList).toHaveBeenCalled(); + }) + + it('list should load all channel sets', () => { + listWrapper.vm.$nextTick().then(() => { + let actualLength = listWrapper.findAll(ChannelSetItem).length; + expect(actualLength).toEqual(ChannelSets.length); + }); + }); + + it('CREATE should open channel set modal', () => { + listWrapper.find('#new-set').trigger('click'); + // TODO: check for channel_set/views/ChannelSetModal + }); + it('About Collections link should open information modal', () => { + listWrapper.find('#about-sets').trigger('click'); + // TODO: check for information/views/ChannelSetInformationModalView + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelStar.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelStar.spec.js new file mode 100644 index 0000000000..03a740c79a --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/channelStar.spec.js @@ -0,0 +1,37 @@ +import { mount } from '@vue/test-utils'; +import ChannelStar from './../views/ChannelStar.vue'; +import _ from 'underscore'; +import { localStore, mockFunctions } from './data'; +import { ListTypes } from './../constants'; + + +function makeWrapper(starred) { + let channel = { + id: "test channel", + name: "test title", + modified: new Date(), + STARRED: starred + }; + localStore.commit('channel_list/SET_CHANNEL_LIST', {channels: [channel], listType: ListTypes.EDITABLE}); + return mount(ChannelStar, { + store: localStore, + propsData: { + channelID: channel.id + } + }) +} + +describe('channelStar', () => { + + it('clicking the star should toggle the star', () => { + function test(starred) { + let wrapper = makeWrapper(starred); + let star = wrapper.find('a'); + star.trigger('click'); + } + test(true); + expect(mockFunctions.removeStar).toHaveBeenCalled(); + test(false); + expect(mockFunctions.addStar).toHaveBeenCalled(); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/data.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/data.js new file mode 100644 index 0000000000..25c1f8f0e3 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/data.js @@ -0,0 +1,150 @@ +import { ListTypes } from '../constants'; +import Vue from 'vue'; +var mutations = require('edit_channel/channel_list/vuex/mutations'); +var getters = require('edit_channel/channel_list/vuex/getters'); +var actions = require('edit_channel/channel_list/vuex/actions'); +const Vuex = require('vuex'); +Vue.use(Vuex); + +export const Channels = [ + { + "id": "channel1", + "name": "Editable Channel", + [ListTypes.STARRED]: false, + [ListTypes.EDITABLE]: true, + [ListTypes.PUBLIC]: false, + [ListTypes.VIEW_ONLY]: false + }, + { + "id": "channel2", + "name": "View-Only Channel", + [ListTypes.STARRED]: false, + [ListTypes.EDITABLE]: false, + [ListTypes.PUBLIC]: false, + [ListTypes.VIEW_ONLY]: true + }, + { + "id": "channel3", + "name": "Public Channel", + [ListTypes.STARRED]: false, + [ListTypes.EDITABLE]: false, + [ListTypes.PUBLIC]: true, + [ListTypes.VIEW_ONLY]: false + }, + { + "id": "channel4", + "name": "Starred + Public Channel", + [ListTypes.STARRED]: true, + [ListTypes.EDITABLE]: false, + [ListTypes.PUBLIC]: true, + [ListTypes.VIEW_ONLY]: false + }, + { + "id": "channel5", + "name": "Starred + Editable Channel", + [ListTypes.STARRED]: true, + [ListTypes.EDITABLE]: true, + [ListTypes.PUBLIC]: false, + [ListTypes.VIEW_ONLY]: false + }, +] + +export const Invitations = [ + { + "id": "invitation1", + "share_mode": "edit", + "sender": { + "first_name": "First", + "last_name": "Last" + } + }, + { + "id": "invitation2", + "share_mode": "view", + "sender": { + "first_name": "First", + "last_name": "Last" + } + } +] + +export const ChannelSets = [ + { + id: 'channelSet1', + name: "test title", + channels: ['test'], + secret_token: { + display_token: 'test-test' + } + }, + { + id: 'channelSet2', + name: "test title", + channels: ['test'], + secret_token: { + display_token: 'test-test' + } + } +] + +export const mockFunctions = { + downloadChannelDetails: jest.fn(), + addStar: jest.fn(), + removeStar: jest.fn(), + loadChannelInvitationList: jest.fn(), + loadChannelList: jest.fn(), + acceptInvitation: jest.fn(), + declineInvitation: jest.fn(), + deleteChannel: jest.fn(), + loadNodeDetails: jest.fn(), + deleteChannelSet: jest.fn(), + loadChannelSetList: jest.fn(), + saveChannel: jest.fn() +} + +export const localStore = new Vuex.Store({ + modules: { + "channel_list": { + namespaced: true, + state: { + channels: [], + activeChannel: null, + changed: false, + channelChanges: {}, + channelSets: [], + invitations: [] + }, + getters: getters, + mutations: { + ...mutations, + SET_CHANNEL_LIST: (state, payload) => { + state.channels = payload.channels; + } + }, + actions: { + ...actions, + loadChannelInvitationList: (context) => { + context.commit('SET_INVITATION_LIST', Invitations); + mockFunctions.loadChannelInvitationList(); + }, + loadChannelList: (context) => { + context.commit('SET_CHANNEL_LIST', {channels: Channels}); + mockFunctions.loadChannelList(); + }, + loadChannelSetList: (context) => { + context.commit('SET_CHANNELSET_LIST', ChannelSets); + mockFunctions.loadChannelSetList(); + }, + addStar: mockFunctions.addStar, + removeStar: mockFunctions.removeStar, + downloadChannelDetails: mockFunctions.downloadChannelDetails, + acceptInvitation: mockFunctions.acceptInvitation, + declineInvitation: mockFunctions.declineInvitation, + deleteChannel: mockFunctions.deleteChannel, + loadNodeDetails: mockFunctions.loadNodeDetails, + deleteChannelSet: mockFunctions.deleteChannelSet, + saveChannel: mockFunctions.saveChannel + } + } + } +}) diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/lookInsideView.spec.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/lookInsideView.spec.js new file mode 100644 index 0000000000..1fb7bca18a --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/tests/lookInsideView.spec.js @@ -0,0 +1,22 @@ +import { shallowMount } from '@vue/test-utils'; +import LookInsideView from './../views/LookInsideView.vue'; +import { localStore, mockFunctions } from './data.js'; + + +function makeWrapper(props = {}) { + return shallowMount(LookInsideView, { + store: localStore, + propsData: { + nodeID: "abc", + channel: {} + } + }) +} + +describe('lookInsideView', () => { + it('should load details when rendered', () => { + let wrapper = makeWrapper(); + // TODO: Need to mock endpoint to get rid of UnhandledPromiseRejectionWarning + expect(mockFunctions.loadNodeDetails).toHaveBeenCalled() + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/utils.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/utils.js new file mode 100644 index 0000000000..f66a1befa7 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/utils.js @@ -0,0 +1,34 @@ + +import Models from 'edit_channel/models'; +import { ListTypes } from './constants'; +import State from 'edit_channel/state'; +import _ from 'underscore'; + +const ListValues = _.values(ListTypes) +export function prepChannel(channel) { + // Set all channel list attributes so vue will listen to them + _.each(ListValues, (type) => { + channel[type] = false; + }); +} + +export function getDefaultChannel() { + return { + name: "", + description: "", + editors: [State.current_user.id], + pending_editors: [], + language: State.preferences && State.preferences.language, + content_defaults: State.preferences, + thumbnail: "", + thumbnail_encoding: {} + } +} + +export function getBackboneChannel(channel) { + return new Models.ChannelModel(channel); +} + +export function getChannelSetModel(channelSet) { + return new Models.ChannelSetModel(channelSet); +} diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDeleteSection.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDeleteSection.vue new file mode 100644 index 0000000000..9632412788 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDeleteSection.vue @@ -0,0 +1,67 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDetailsPanel.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDetailsPanel.vue new file mode 100644 index 0000000000..d11cf07236 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDetailsPanel.vue @@ -0,0 +1,180 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDownloadDropdown.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDownloadDropdown.vue new file mode 100644 index 0000000000..cb24c6ee95 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelDownloadDropdown.vue @@ -0,0 +1,91 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelEditor.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelEditor.vue new file mode 100644 index 0000000000..b4f6f04e29 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelEditor.vue @@ -0,0 +1,357 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelInvitationItem.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelInvitationItem.vue new file mode 100644 index 0000000000..f4dfac5084 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelInvitationItem.vue @@ -0,0 +1,208 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelInvitationList.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelInvitationList.vue new file mode 100644 index 0000000000..ca04ed4e45 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelInvitationList.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelItem.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelItem.vue new file mode 100644 index 0000000000..ecc0f241d0 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelItem.vue @@ -0,0 +1,165 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelList.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelList.vue new file mode 100644 index 0000000000..217eeb0c57 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelList.vue @@ -0,0 +1,98 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelListPage.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelListPage.vue new file mode 100644 index 0000000000..515421f63e --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelListPage.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelMetadataSection.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelMetadataSection.vue new file mode 100644 index 0000000000..ce58921c6c --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelMetadataSection.vue @@ -0,0 +1,135 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelSetItem.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelSetItem.vue new file mode 100644 index 0000000000..625a239010 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelSetItem.vue @@ -0,0 +1,118 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelSetList.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelSetList.vue new file mode 100644 index 0000000000..086b8b2e27 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelSetList.vue @@ -0,0 +1,109 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelStar.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelStar.vue new file mode 100644 index 0000000000..44a555b8ef --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/ChannelStar.vue @@ -0,0 +1,42 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/LookInsideView.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/LookInsideView.vue new file mode 100644 index 0000000000..b058399f65 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/views/LookInsideView.vue @@ -0,0 +1,86 @@ + + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/actions.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/actions.js new file mode 100644 index 0000000000..d0e0a790d9 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/actions.js @@ -0,0 +1,205 @@ +import Vue from 'vue'; +import _ from 'underscore'; +import State from 'edit_channel/state'; + +import Models from 'edit_channel/models'; +import { ChannelSetModalView } from 'edit_channel/channel_set/views'; +import { ListTypes, ChannelListUrls, ChannelInvitationMapping } from './../constants'; +import { prepChannel } from './../utils'; +import fileDownload from 'jquery-file-download'; + + +/* CHANNEL LIST ACTIONS */ +export function loadChannelList(context, listType) { + return new Promise((resolve, reject) => { + $.ajax({ + method: "GET", + url: ChannelListUrls[listType], + error: reject, + success: (channels) => { + context.commit('SET_CHANNEL_LIST', { + listType: listType, + channels: channels + }); + resolve(channels); + } + }); + }); +} + +export function addStar(context, channelID) { + let channel = context.getters.getChannel(channelID); + channel.STARRING = true; + $.ajax({ + method: "POST", + data: JSON.stringify({ + "channel_id": channelID, + "user_id": State.current_user.id + }), + url: window.Urls.add_bookmark(), + success: () => { + channel.STARRED = true; + channel.STARRING = false; + } + }); +} + +export function removeStar(context, channelID) { + let channel = context.getters.getChannel(channelID); + channel.STARRING = true; + $.ajax({ + method: "POST", + data: JSON.stringify({ + "channel_id": channelID, + "user_id": State.current_user.id + }), + url: window.Urls.remove_bookmark(), + success: () => { + channel.STARRED = false; + channel.STARRING = false; + } + }); +} + + +/* CHANNEL EDITOR ACTIONS */ +export function saveChannel(context) { + /* TODO: REMOVE BACKBONE */ + return new Promise((resolve, reject) => { + new Models.ChannelModel().save(context.state.channelChanges, { + patch: true, + error: reject, + success: (channel) => { + channel = channel.toJSON(); + context.commit('SUBMIT_CHANNEL', channel); + context.commit('SET_ACTIVE_CHANNEL', channel); // Used for new channelss + resolve(channel); + } + }); + }); +} + +export function deleteChannel(context, channelID) { + let channel = context.getters.getChannel(channelID); + + /* TODO: REMOVE BACKBONE */ + return new Promise((resolve, reject) => { + new Models.ChannelModel(channel).save({"deleted": true}, { + error: reject, + success: () => { + context.commit('REMOVE_CHANNEL', channel.id); + context.commit('SET_ACTIVE_CHANNEL', null); + resolve(); + } + }); + }); +} + +export function loadNodeDetails(context, nodeID) { + return new Promise(function (resolve, reject) { + $.ajax({ + method: "GET", + url: window.Urls.get_topic_details(nodeID), + error: reject, + success: (result) => { + let node = new Models.ContentNodeModel({'metadata': JSON.parse(result)}); + node.id = nodeID; + resolve(node); + } + }); + }); +} + +export function downloadChannelDetails(context, payload) { + return new Promise(function (resolve, reject) { + let url = ""; + switch(payload.format) { + case "detailedPdf": + url = window.Urls.get_channel_details_pdf_endpoint(payload.id); + break; + case "csv": + url = window.Urls.get_channel_details_csv_endpoint(payload.id); + break; + case "ppt": + url = window.Urls.get_channel_details_ppt_endpoint(payload.id); + break; + default: + url = window.Urls.get_channel_details_pdf_endpoint(payload.id) + "?condensed=true"; + } + $.fileDownload(url, { + successCallback: resolve, + failCallback: (responseHtml, url) => { + reject(responseHtml); + } + }); + }); +} + + +/* CHANNEL SET ACTIONS */ +export function loadChannelSetList(context) { + return new Promise((resolve, reject) => { + $.ajax({ + method: "GET", + url: window.Urls.get_user_channel_sets(), + error: reject, + success: (channelSets) => { + context.commit('SET_CHANNELSET_LIST', channelSets); + resolve(channelSets); + } + }); + }); +} + +export function deleteChannelSet(context, channelSetID) { + /* TODO: REMOVE BACKBONE */ + new Models.ChannelSetModel({id: channelSetID}).destroy({ + success: function() { + context.commit('REMOVE_CHANNELSET', channelSetID); + } + }); +} + + + +/* INVITATION ACTIONS */ +export function loadChannelInvitationList(context) { + return new Promise((resolve, reject) => { + $.ajax({ + method: "GET", + url: window.Urls.get_user_pending_channels(), + error: reject, + success: (invitations) => { + context.commit('SET_INVITATION_LIST', invitations); + resolve(invitations); + } + }); + }); +} + +export function acceptInvitation(context, invitationID) { + let invitation = context.getters.getInvitation(invitationID); + return new Promise((resolve, reject) => { + $.ajax({ + method: "POST", + url: window.Urls.accept_channel_invite(), + data: { "invitation_id": invitationID }, + error: reject, + success: (channel) => { + prepChannel(channel); + let listType = ChannelInvitationMapping[invitation.share_mode]; + channel[listType] = true; + context.commit('ADD_CHANNEL', channel); + resolve(channel); + } + }); + }); +} + +export function declineInvitation(context, invitationID) { + /* TODO: REMOVE BACKBONE */ + return new Promise((resolve, reject) => { + let invite = new Models.InvitationModel({id: invitationID}); + invite.destroy({ success: resolve, error: reject }) + }); +} diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/getters.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/getters.js new file mode 100644 index 0000000000..c8168de1d8 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/getters.js @@ -0,0 +1,36 @@ + +import _ from 'underscore'; + +export function channels(state) { + return state.channels; +} + +export function channelSets(state) { + return state.channelSets; +} + +export function invitations(state) { + return state.invitations; +} + +export function changed(state) { + return state.changed; +} + +export function getChannel(state) { + return function(channelID) { + return _.findWhere(state.channels, {id: channelID}); + }; +} + +export function getInvitation(state) { + return function(invitationID) { + return _.findWhere(state.invitations, {id: invitationID}); + }; +} + +export function getChannelSet(state) { + return function(channelSetID) { + return _.findWhere(state.channelSets, {id: channelSetID}); + }; +} diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/mutations.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/mutations.js new file mode 100644 index 0000000000..0e21fca922 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/mutations.js @@ -0,0 +1,135 @@ +import _ from 'underscore'; +import { ListTypes } from './../constants'; +import { prepChannel, getDefaultChannel } from './../utils'; + + +export function RESET_STATE(state) { + state = { + channels: [], + activeChannel: null, + changed: false, + channelChanges: {}, + channelSets: [], + invitations: [] + } +} + + +/* CHANNEL LIST MUTATIONS */ +export function SET_ACTIVE_CHANNEL(state, channelID) { + state.activeChannel = (channelID === "")? getDefaultChannel() : + _.findWhere(state.channels, {id: channelID}); + state.channelChanges = _.clone(state.activeChannel); +} + +export function SET_CHANNEL_LIST(state, payload) { + let listValues = _.values(ListTypes); + _.each(payload.channels, (channel)=> { + let match = _.findWhere(state.channels, {id: channel.id}) + if(match) { // If it exists, set the existing channel's listType to true + match[payload.listType] = true; + } else { // Otherwise, add to the list of channels + prepChannel(channel); + channel[payload.listType] = true; + state.channels.push(channel); + } + }); +} + +export function ADD_CHANNEL(state, channel) { + state.channels.unshift(channel); +} + + + +/* CHANNEL EDITOR MUTATIONS */ +export function SUBMIT_CHANNEL(state, channel) { + // If this is an existing channel, update the fields + // Otherwise, add new channels to the list + let match = _.findWhere(state.channels, {id: channel.id}); + if (match) { + _.each(_.pairs(channel), (val) => { + match[val[0]] = val[1]; + }); + } else { + prepChannel(channel); + channel.EDITABLE = true; + state.channels.unshift(channel); + } + state.changed = false; +} + +export function CANCEL_CHANNEL_CHANGES(state) { + state.changed = false; + state.channelChanges = _.clone(state.activeChannel); +} + +export function REMOVE_CHANNEL(state, channelID) { + state.channels = _.reject(state.channels, (c) => { + return c.id === channelID; + }); + + // Remove channel from channel sets too + _.each(state.channelSets, (channelset) => { + channelset.channels = _.reject(channelset.channels, (cid) => { + return cid === channelID; + }); + }); + state.changed = false; +} + +export function SET_CHANNEL_NAME(state, name) { + state.channelChanges.name = name; + state.changed = true; +} + +export function SET_CHANNEL_DESCRIPTION(state, description) { + state.channelChanges.description = description.trim(); + state.changed = true; +} + +export function SET_CHANNEL_THUMBNAIL(state, payload) { + state.channelChanges.thumbnail = payload.thumbnail; + state.channelChanges.thumbnail_encoding = payload.encoding; + state.changed = true; +} + +export function SET_CHANNEL_LANGUAGE(state, language) { + state.channelChanges.language = language; + state.changed = true; +} + +export function SET_CHANGED(state, changed) { + state.changed = changed; +} + + + +/* CHANNEL SET MUTATIONS */ +export function SET_CHANNELSET_LIST(state, channelSets) { + state.channelSets = channelSets; +} + +export function ADD_CHANNELSET(state, channelSet) { + /* TODO: REMOVE BACKBONE */ + state.channelSets.push(channelSet.toJSON()); +} + +export function REMOVE_CHANNELSET(state, channelSetID) { + state.channelSets = _.reject(state.channelSets, (set)=> { + return set.id === channelSetID; + }); +} + + + +/* INVITATION MUTATIONS */ +export function SET_INVITATION_LIST(state, invitations) { + state.invitations = invitations; +} + +export function REMOVE_INVITATION(state, invitationID) { + state.invitations = _.reject(state.invitations, (invitation)=> { + return invitation.id === invitationID; + }); +} diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/store.js b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/store.js new file mode 100644 index 0000000000..a8e5edab8f --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_list/vuex/store.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +var mutations = require('./mutations'); +var actions = require('./actions'); +var getters = require('./getters'); + +const Vuex = require('vuex'); +Vue.use(Vuex); + +module.exports = new Vuex.Store({ + modules: { + "channel_list": { + namespaced: true, + state: { + channels: [], + activeChannel: null, + changed: false, + channelChanges: {}, + channelSets: [], + invitations: [] + }, + getters: getters, + mutations: mutations, + actions: actions + } + } +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_set/views.js b/contentcuration/contentcuration/static/js/edit_channel/channel_set/views.js index f82c9833a1..0ba5d2f82a 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/channel_set/views.js +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_set/views.js @@ -1,5 +1,8 @@ +// TODO: REMOVE BACKBONE + import Vue from 'vue'; import ChannelSetModalComponent from './views/ChannelSetModal.vue'; +import _ from 'underscore'; var Backbone = require('backbone'); var BaseViews = require("../views"); diff --git a/contentcuration/contentcuration/static/js/edit_channel/channel_set/views/ChannelSetList.vue b/contentcuration/contentcuration/static/js/edit_channel/channel_set/views/ChannelSetList.vue index 7b49ac9937..e46cc91b81 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/channel_set/views/ChannelSetList.vue +++ b/contentcuration/contentcuration/static/js/edit_channel/channel_set/views/ChannelSetList.vue @@ -30,7 +30,7 @@
-
+
{{ $tr('loading') }}
diff --git a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/channel_editor.handlebars b/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/channel_editor.handlebars deleted file mode 100644 index 310a53c9c2..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/channel_editor.handlebars +++ /dev/null @@ -1,71 +0,0 @@ -
-
- {{#if edit}} -
- {{else}} - {{channel.name}} logo - {{/if}} -
-
-
- {{#if edit}} - language - - {{else}} - {{#if language}}language {{language.native_name}}{{/if}} - {{/if}} -
- {{#if edit}} -

{{formatMessage (intlGet 'messages.channel_name')}}

- - {{else}} -

{{channel.name}}

-

- {{formatMessage (intlGet 'messages.created')}} {{formatDate channel.created day="numeric" month="short" year="numeric"}} -  |  - {{#if channel.published}} - {{formatMessage (intlGet 'messages.last_published')}} {{formatDate channel.last_published day="numeric" month="short" year="numeric"}} - {{else}}{{formatMessage (intlGet 'messages.unpublished')}} - {{/if}} -

-
- {{/if}} - {{#if edit}} -

{{formatMessage (intlGet 'messages.channel_description')}}

- - {{else}} -

{{channel.description}}

- {{/if}} -
- {{#if is_new}} - - {{formatMessage (intlGet 'messages.cancel')}} - {{else}}{{#if edit}} - - {{formatMessage (intlGet 'messages.cancel')}} - {{else}} - {{formatMessage (intlGet 'messages.open')}} - {{#unless channel.ricecooker_version}}{{#if can_edit}}{{formatMessage (intlGet 'messages.edit_details')}}{{/if}}{{/unless}} -
-
- - -
-
- {{/if}}{{/if}} -
-
\ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_editor.handlebars b/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_editor.handlebars deleted file mode 100644 index ec17f6cec0..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_editor.handlebars +++ /dev/null @@ -1,25 +0,0 @@ -{{#unless is_new}} - {{#if channel.is_bookmarked}}star{{else}}star_border{{/if}} -{{/unless}} -
-
- -
-

- {{#unless is_new}} -
-
- {{formatMessage (intlGet 'messages.loading_details')}} -
-
- - {{#if can_edit}} -
-
-

{{formatMessage (intlGet 'messages.delete_title')}}

-

{{formatMessage (intlGet 'messages.delete_prompt')}}

- {{formatMessage (intlGet 'messages.delete_channel')}} -
- {{/if}} - {{/unless}} -
diff --git a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_modal.handlebars b/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_modal.handlebars deleted file mode 100644 index af91763d3b..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_modal.handlebars +++ /dev/null @@ -1,12 +0,0 @@ - \ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_view.handlebars b/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_view.handlebars index 70a6f7c74c..9b9999f6cf 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_view.handlebars +++ b/contentcuration/contentcuration/static/js/edit_channel/details/hbtemplates/details_view.handlebars @@ -114,7 +114,7 @@ {{#if details.sample_pathway}}
{{#each details.sample_pathway}} - + {{{get_icon kind_id}}}
{{title}}
@@ -125,7 +125,7 @@
{{#each details.sample_nodes}}
- +
{{title}}
diff --git a/contentcuration/contentcuration/static/js/edit_channel/details/views.js b/contentcuration/contentcuration/static/js/edit_channel/details/views.js index b77a6c1b4c..d84fe5e69f 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/details/views.js +++ b/contentcuration/contentcuration/static/js/edit_channel/details/views.js @@ -22,20 +22,10 @@ var MESSAGES = { "topic_aggregator": "Material in this topic was originally hosted at", "topic_provider": "The material in this topic was provided by", "topic_empty_details": "This topic is empty", - "saved": "SAVED!", - "header": "CHANNEL DETAILS", - "save_changes": "SAVE CHANGES", - "unable_to_save": "Error Saving Channel", - "channel_name": "Channel Name", - "channel_name_error": "Channel name cannot be blank.", - "channel_name_placeholder": "Enter channel name...", - "channel_description": "Channel Description", - "description_placeholder": "Enter channel description...", "no_license": "No license selected", "author_description": "Person or organization who created the content", "aggregator_description": "Website or org hosting the content collection but not necessarily the creator or copyright holder", "provider_description": "Organization that commissioned or is distributing the content", - "loading_details": "Loading details...", "license": "{count, plural,\n =1 {License}\n other {Licenses}}", "copyright_holder": "{count, plural,\n =1 {Copyright Holder}\n other {Copyright Holders}}", "auth_info": "Authoring Information", @@ -48,29 +38,18 @@ var MESSAGES = { "content_breakdown": "Content Summary", "languages": "Languages", "tags": "Content Tags", - "open": "OPEN CHANNEL", "resource_count": "{count, plural,\n =1 {# Resource}\n other {# Resources}}", "visibility_count": "{count, plural,\n =1 {# resource is}\n other {# resources are}} visible to {user}", "kind_count": "{count, plural,\n =1 {# {kind}}\n other {# {kind_plural}}}", "role_description": "Coach content is visible to coaches only in Kolibri", "sample_pathway": "Sample Pathway", - "channel_language": "Language", "channel_id": "Channel ID", "channel_tokens": "{count, plural,\n =1 {Channel Token}\n other {Channel Tokens}}", - "unpublished": "Unpublished", - "last_published": "Last Published", "copy": "Copy", "copy_text": "Copy the following into Kolibri to import this channel", - "delete_channel": "DELETE CHANNEL", - "deleting_channel": "Deleting Channel...", - "delete_warning": "All content under this channel will be permanently deleted.\nAre you sure you want to delete this channel?", - "delete_title": "Delete this Channel", - "delete_prompt": "Once you delete a channel, the channel will be permanently deleted.", "total_resource_count": "{data, plural,\n =1 {Total Resource}\n other {Total Resources}}", - "create": "CREATE", "invalid_channel": "Cannot save invalid channel", "original_channels": "Includes Content From", - "created": "Created", "whats_inside": "What's Inside", "source": "Source", "topic": "topic", @@ -87,23 +66,8 @@ var MESSAGES = { "instructor_resources": "For Educators", "recommended": "(Recommended)", "preview": "Preview", - "star_channel": "Star Channel", - "unstar_channel": "Remove Star", - "edit_details": "Edit Details", "more": "Show More", "less": "Show Less", - "download_pdf": "Download Detailed PDF", - "download_condensed_pdf": "Download PDF", - "download_csv": "Download CSV", - "download_ppt": "Download PPT", - "download_as": "Download Channel Report", - "download_started": "Download Started", - "download_started_text": "Generating a {data} for {data2}. Download will start automatically.", - "download_failed": "Download Failed", - "download_failed_text": "Failed to download a {data} for {data2}", - "format_pdf": "pdf", - "format_ppt": "ppt", - "format_csv": "csv", "original_content": "Original Content", "details_tooltip": "{kind} ({percent}%)" } @@ -111,311 +75,6 @@ var MESSAGES = { const CHANNEL_SIZE_DIVISOR = 100000000; var SCALE_TEXT = ["very_small", "very_small", "small", "small", "average", "average", "average", "large", "large", "very_large", "very_large"]; -var ChannelDetailsView = BaseViews.BaseListEditableItemView.extend({ - template: require("./hbtemplates/details_editor.handlebars"), - channel_template: require("./hbtemplates/channel_editor.handlebars"), - id: "channel_details_view_panel", - tagName: "div", - name: NAMESPACE, - $trs: MESSAGES, - initialize: function(options) { - _.bindAll(this, 'set_background', 'change'); - this.onnew = options.onnew; - this.onclose = options.onclose; - this.ondelete = options.ondelete; - this.onstar = options.onstar; - this.onunstar = options.onunstar; - this.allow_edit = options.allow_edit; - this.changed = false; - if(!this.onnew) { - var main_tree_id = (typeof this.model.get('main_tree') == "string")? this.model.get('main_tree') : this.model.get('main_tree').id; - this.main_tree = new Models.ContentNodeModel({id: main_tree_id}); - } - this.listenTo(this.model, "sync", this.set_background); - }, - set_background: function() { - // Set background of channel panel to thumbnail - $("#channel_preview_wrapper").css("background-image", "url('" + this.model.get("thumbnail_url").replace("\\", "/") + "')") - }, - events: { - "click .copy-id-btn" : "copy_id", - "click .delete_channel": "delete_channel", - "click .cancel": "close", - "click .star_icon": "toggle_star" - }, - render: function() { - this.$el.html(this.template({ - channel: this.model.toJSON(), - can_edit: this.allow_edit, - is_new: !!this.onnew - }, { - data: this.get_intl_data() - })); - this.set_background(); - - this.editor = new ChannelEditorView({ - onnew: this.onnew, - onclose: this.onclose, - onchange: this.change, - allow_edit: this.allow_edit, - el: this.$("#channel_details_area"), - model: this.model - }); - this.$('[data-toggle="tooltip"]').tooltip(); - - // Load main tree details - if(!this.onnew) { - var self = this; - var main_tree = this.main_tree || this.model.get("main_tree"); - _.defer(function() { - main_tree.fetch_details().then(function(data) { - self.settings_view = new DetailsView({ - el: self.$("#look-inside"), - allow_edit: true, - model: data, - channel_id: self.model.id, - is_channel: true, - channel: self.model.toJSON() - }); - $(".details_view").css("display", "block"); - }) - }); - } - }, - submit_changes: function() { - this.editor.submit_changes(); - }, - change: function(changed) { - // Mark as changed to prompt user to save if closing the panel - this.changed = changed; - }, - copy_id:function(event){ - event.stopPropagation(); - event.preventDefault(); - var button = $(event.target); - var self = this; - $(button.data("text")).focus(); - $(button.data("text")).select(); - try { - document.execCommand("copy"); - button.text("check"); - } catch(e) { - button.text("clear"); - } - setTimeout(function(){ - button.text("content_paste"); - }, 2500); - }, - delete_channel: function() { - var self = this; - dialog.dialog(this.get_translation("warning"), this.get_translation("delete_warning", this.model.get("name")), { - [this.get_translation("cancel")]:function(){}, - [this.get_translation("delete_channel")]: function(){ - self.save({"deleted":true}, self.get_translation("deleting_channel")).then(function() { - self.ondelete(self.model); - }); - }, - }, null); - }, - toggle_star: function(event) { - if($(event.target).html() === "star") { - this.onunstar(null, $(event.target)); - $(event.target).html("star_border"); - } else { - this.onstar(null, $(event.target)); - $(event.target).html("star"); - } - } -}); - -var ChannelEditorView = BaseViews.BaseListEditableItemView.extend({ - template: require("./hbtemplates/channel_editor.handlebars"), - tagName: "div", - name: NAMESPACE, - $trs: MESSAGES, - initialize: function(options) { - _.bindAll(this, "set_thumbnail", "reset_thumbnail", "remove_thumbnail", "init_focus", "create_initial", "submit_changes"); - this.onnew = options.onnew; - this.onclose = options.onclose; - this.allow_edit = options.allow_edit; - this.onchange = options.onchange; - this.edit = !!this.onnew; - this.render(); - }, - events: { - "click #submit": "submit_changes", - "change .input_listener": "register_changes", - "keyup .input_listener": "register_changes", - "focus .input-tab-control": "loop_focus", - "click .copy-id-btn" : "copy_id", - "click #cancel_new": "close", - "click #edit_details": "edit_details", - 'click .toggle_description' : 'toggle_description', - "click #cancel_edit": "cancel_edit", - "click #download_pdf": "download_pdf", - "click #download_csv": "download_csv", - "click #download_ppt": "download_ppt", - "click #download_condensed_pdf": "download_condensed_pdf", - }, - render: function() { - this.original_thumbnail = this.model.get("thumbnail"); - this.original_thumbnail_encoding = this.model.get("thumbnail_encoding"); - this.$el.html(this.template({ - channel: this.model.toJSON(), - languages: Constants.Languages, - picture : (this.model.get("thumbnail_encoding") && this.model.get("thumbnail_encoding").base64) || this.model.get("thumbnail_url"), - language: Constants.Languages.find(language => language.id === this.model.get("language")), - can_edit: this.allow_edit, - is_new: !!this.onnew, - edit: this.edit - }, { - data: this.get_intl_data() - })); - $("#select_language").val(this.model.get("language") || 0); - this.$('[data-toggle="tooltip"]').tooltip(); - - if(!this.edit) { - this.description = new descriptionHelper.Description(this.model.get("description"), this.$("#channel_description"), 100); - } - - this.create_initial(); - }, - create_initial: function() { - // Create thumbnail and set tab focus - if(this.allow_edit) { - this.image_upload = new Images.ThumbnailUploadView({ - model: this.model, - el: this.$("#channel_thumbnail"), - preset_id: "channel_thumbnail", - upload_url: window.Urls.thumbnail_upload(), - acceptedFiles: "image/jpeg,image/jpeg,image/png", - image_url: this.model.get("thumbnail_url"), - default_url: "/static/img/kolibri_placeholder.png", - onsuccess: this.set_thumbnail, - onerror: this.reset_thumbnail, - oncancel:this.enable_submit, - onstart: this.disable_submit, - onremove: this.remove_thumbnail, - allow_edit: true, - is_channel: true - }); - this.init_focus(); - } - }, - init_focus: function(){ - this.set_indices(); - this.set_initial_focus(); - window.scrollTo(0, 0); - }, - close: function() { - this.onclose(); - }, - cancel_edit: function() { - this.edit = false; - this.model.set("thumbnail", this.original_thumbnail); - this.model.set("thumbnail_encoding", this.original_thumbnail_encoding); - this.onchange(false); - this.render(); - }, - submit_changes:function(){ - var language = $("#select_language").val(); - var self = this; - $("#submit").html(this.get_translation("saving")) - .attr("disabled", "disabled") - .addClass("disabled"); - this.save({ - "name": $("#input_title").val().trim(), - "description": $("#input_description").val(), - "thumbnail": this.model.get("thumbnail"), - "thumbnail_encoding": this.model.get("thumbnail_encoding"), - "language": (language===0)? null : language - }).then(function(data){ - self.onchange(false); - if (self.onnew) { - self.onnew(data); - } else { - self.edit = false; - self.render(); - } - - }).catch( function(error) { - dialog.alert(self.get_translation("unable_to_save"), error.responseText); - }); - }, - register_changes:function(){ - $("#submit").html(this.get_translation((this.onnew)? "create" : "save")); - - var isvalid = $("#input_title").val().trim() !== ""; - $("#channel_error").css("display", (isvalid)? "none" : "inline-block"); - if(isvalid){ - $("#submit").attr("disabled", false).removeClass("disabled").removeAttr("title"); - } else { - $("#submit").attr("disabled", "disabled").addClass("disabled").attr("title", this.get_translation("invalid_channel")); - } - this.onchange(true); - }, - reset_thumbnail:function(){ - this.render(); - this.register_changes(); - }, - remove_thumbnail:function(){ - this.model.set("thumbnail", "/static/img/kolibri_placeholder.png"); - this.register_changes(); - }, - set_thumbnail:function(thumbnail, encoding, formatted_name, path){ - this.model.set("thumbnail", formatted_name); - this.model.set("thumbnail_encoding", encoding) - this.register_changes(); - }, - disable_submit:function(){ - this.$("#submit").attr("disabled", "disabled"); - this.$("#submit").addClass("disabled"); - }, - edit_details: function() { - this.edit = true; - this.render(); - }, - download_pdf: function() { - var self = this; - var format = this.get_translation("format_pdf"); - dialog.alert(this.get_translation("download_started"), this.get_translation("download_started_text", format, this.model.get("name"))); - $.fileDownload(window.Urls.get_channel_details_pdf_endpoint(this.model.id), { - failCallback: function(responseHtml, url) { - dialog.alert(self.get_translation("download_failed"), self.get_translation("download_failed_text", format, self.model.get("name"))); - } - }); - }, - download_condensed_pdf: function() { - var self = this; - var format = this.get_translation("format_pdf"); - dialog.alert(this.get_translation("download_started"), this.get_translation("download_started_text", format, this.model.get("name"))); - $.fileDownload(window.Urls.get_channel_details_pdf_endpoint(this.model.id) + "?condensed=true", { - failCallback: function(responseHtml, url) { - dialog.alert(self.get_translation("download_failed"), self.get_translation("download_failed_text", format, self.model.get("name"))); - } - }); - }, - download_csv: function() { - var self = this; - var format = this.get_translation("format_csv"); - dialog.alert(this.get_translation("download_started"), this.get_translation("download_started_text", format, this.model.get("name"))); - $.fileDownload(window.Urls.get_channel_details_csv_endpoint(this.model.id), { - failCallback: function(responseHtml, url) { - dialog.alert(self.get_translation("download_failed"), self.get_translation("download_failed_text", format, self.model.get("name"))); - } - }); - }, - download_ppt: function() { - var self = this; - var format = this.get_translation("format_ppt"); - dialog.alert(this.get_translation("download_started"), this.get_translation("download_started_text", format, this.model.get("name"))); - $.fileDownload(window.Urls.get_channel_details_ppt_endpoint(this.model.id), { - failCallback: function(responseHtml, url) { - dialog.alert(self.get_translation("download_failed"), self.get_translation("download_failed_text", format, self.model.get("name"))); - } - }); - } -}); var DetailsView = BaseViews.BaseListEditableItemView.extend({ template: require("./hbtemplates/details_view.handlebars"), @@ -424,29 +83,25 @@ var DetailsView = BaseViews.BaseListEditableItemView.extend({ $trs: MESSAGES, initialize: function(options) { _.bindAll(this, "render_visuals"); - this.allow_edit = options.allow_edit; - this.channel_id = options.channel_id; - this.is_channel = options.is_channel; this.channel = options.channel; State.current_channel_editor_cid = this.cid; this.render(); }, events: { "click .btn-tab": "set_tab", - "click .toggle-list": "set_toggle_text" + "click .toggle-list": "set_toggle_text", + "click .copy-id-btn" : "copy_id" }, render: function() { var self = this; var original_channels = _.map(this.model.get("metadata").original_channels, function(item) { - return (item.id === self.channel_id) ? {"id": item.id, "name": self.get_translation("original_content"), "count": item.count} : item; + return (item.id === self.channel.id) ? {"id": item.id, "name": self.get_translation("original_content"), "count": item.count} : item; }); this.$el.html(this.template({ details: this.model.get("metadata"), resource_count: this.model.get("metadata").resource_count, - channel_id: this.channel_id, - allow_edit: this.allow_edit, original_channels:original_channels, - is_channel: this.is_channel, + is_channel: (this.channel.main_tree.id || this.channel.main_tree) === this.model.id, license_count: this.model.get("metadata").licenses.length, copyright_holder_count: this.model.get("metadata").copyright_holders.length, token_count: (this.channel && this.channel.secret_tokens)? this.channel.secret_tokens.length : 0, @@ -549,10 +204,26 @@ var DetailsView = BaseViews.BaseListEditableItemView.extend({ var current_text = $(e.target).text(); $(e.target).text($(e.target).data("update")); $(e.target).data("update", current_text); + }, + copy_id:function(event){ + event.stopPropagation(); + event.preventDefault(); + var button = $(event.target); + var self = this; + $(button.data("text")).focus(); + $(button.data("text")).select(); + try { + document.execCommand("copy"); + button.text("check"); + } catch(e) { + button.text("clear"); + } + setTimeout(function(){ + button.text("content_paste"); + }, 2500); } }); module.exports = { - ChannelDetailsView: ChannelDetailsView, DetailsView:DetailsView } diff --git a/contentcuration/contentcuration/static/js/edit_channel/information/views.js b/contentcuration/contentcuration/static/js/edit_channel/information/views.js index 465132910b..d31d8ae6cd 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/information/views.js +++ b/contentcuration/contentcuration/static/js/edit_channel/information/views.js @@ -108,7 +108,7 @@ var PrerequisiteModalView = BaseInfoModalView.extend({ modal_id: "#prereq_modal", }); -var ChannelSetModalView = BaseInfoModalView.extend({ +var ChannelSetInformationModalView = BaseInfoModalView.extend({ template: require("./hbtemplates/channel_set_modal.handlebars"), modal_id: "#channel_set_modal", }); @@ -168,5 +168,5 @@ module.exports = { PrerequisiteModalView: PrerequisiteModalView, PublishedModalView: PublishedModalView, RolesModalView: RolesModalView, - ChannelSetModalView: ChannelSetModalView + ChannelSetInformationModalView: ChannelSetInformationModalView } diff --git a/contentcuration/contentcuration/static/js/edit_channel/models.js b/contentcuration/contentcuration/static/js/edit_channel/models.js index 64101948d3..a2cac4a3a6 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/models.js +++ b/contentcuration/contentcuration/static/js/edit_channel/models.js @@ -208,92 +208,6 @@ var UserModel = BaseModel.extend({ }, get_full_name: function () { return this.get('first_name') + " " + this.get('last_name'); - }, - get_channels: function () { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "GET", - url: window.Urls.get_user_edit_channels(), - error: reject, - success: function (data) { - var collection = new ChannelCollection(data); - collection.each(function (item) { item.set("is_bookmarked", _.contains(self.get("bookmarks"), item.id)); }); - resolve(collection); - } - }); - }); - }, - get_view_only_channels: function () { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "GET", - url: window.Urls.get_user_view_channels(), - error: reject, - success: function (data) { - var collection = new ChannelCollection(data); - collection.each(function (item) { item.set("is_bookmarked", _.contains(self.get("bookmarks"), item.id)); }); - resolve(collection); - } - }); - }); - }, - get_bookmarked_channels: function () { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "GET", - url: window.Urls.get_user_bookmarked_channels(), - error: reject, - success: function (data) { - var collection = new ChannelCollection(data); - collection.each(function (item) { item.set("is_bookmarked", true); }); - resolve(collection); - } - }); - }); - }, - get_public_channels: function () { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "GET", - url: window.Urls.get_user_public_channels(), - error: reject, - success: function (data) { - var collection = new ChannelCollection(data); - collection.each(function (item) { item.set("is_bookmarked", _.contains(self.get("bookmarks"), item.id)); }); - resolve(collection); - } - }); - }); - }, - get_pending_invites: function () { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "GET", - url: window.Urls.get_user_pending_channels(), - error: reject, - success: function (data) { - resolve(new InvitationCollection(data)); - } - }); - }); - }, - get_user_channel_collections: function() { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "GET", - url: window.Urls.get_user_channel_sets(), - error: reject, - success: function (data) { - resolve(new ChannelSetCollection(data)); - } - }); - }); } }); @@ -318,26 +232,6 @@ var InvitationModel = BaseModel.extend({ }, get_full_name: function () { return this.get('first_name') + " " + this.get('last_name'); - }, - accept_invitation: function () { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "POST", - url: window.Urls.accept_channel_invite(), - data: JSON.stringify({ "invitation_id": self.id }), - error: reject, - success: function (data) { - resolve(new ChannelModel(JSON.parse(data))); - } - }); - }); - }, - decline_invitation: function () { - var self = this; - return new Promise(function (resolve, reject) { - self.destroy({ success: resolve, error: reject }); - }); } }); @@ -921,36 +815,6 @@ var ChannelModel = BaseModel.extend({ }); }); }, - add_bookmark: function (user_id) { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "POST", - data: JSON.stringify({ - "channel_id": self.id, - "user_id": user_id - }), - url: window.Urls.add_bookmark(), - success: resolve, - error: function (error) { reject(error.responseText); } - }); - }); - }, - remove_bookmark: function (user_id) { - var self = this; - return new Promise(function (resolve, reject) { - $.ajax({ - method: "POST", - data: JSON.stringify({ - "channel_id": self.id, - "user_id": user_id - }), - url: window.Urls.remove_bookmark(), - success: resolve, - error: function (error) { reject(error.responseText); } - }); - }); - }, get_channel_counts: function () { var self = this; return new Promise(function (resolve, reject) { diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_create.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_create.handlebars deleted file mode 100644 index ba5f246858..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_create.handlebars +++ /dev/null @@ -1,45 +0,0 @@ -
-
- -
-
- -
-
- -
{{formatMessage (intlGet 'messages.loading')}}
- -
-
{{formatMessage (intlGet 'messages.loading')}}
-
{{formatMessage (intlGet 'messages.loading')}}
-
{{formatMessage (intlGet 'messages.loading')}}
- -
-
-
-
-
-
-
-
-

- clear -

-
-
{{formatMessage (intlGet 'messages.channel_detail_prompt')}}
-
-
-
-
\ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_item.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_item.handlebars deleted file mode 100644 index 5073dd39b1..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_item.handlebars +++ /dev/null @@ -1,39 +0,0 @@ -
arrow_forward
-
-
- {{#if picture}} - {{channel.name}} logo - {{/if}} -
- -

- {{#if channel.is_bookmarked}} - star - {{else}} - star_border - {{/if}} -

{{channel.name}}

-

-
-

{{channel.description}}

-
-
- {{formatMessage (intlGet 'messages.last_updated') updated=(formatRelative modified)}} -
-
diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_item_pending.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_item_pending.handlebars deleted file mode 100644 index ade7c4d9b9..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_item_pending.handlebars +++ /dev/null @@ -1,26 +0,0 @@ -{{#if status}} - {{#if status.accepted}} - - {{else}} - - {{/if}} -{{else}} - -{{/if}} \ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list.handlebars deleted file mode 100644 index 569dc3690c..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list.handlebars +++ /dev/null @@ -1 +0,0 @@ -
  • {{formatMessage (intlGet 'messages.loading')}}
\ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list_current.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list_current.handlebars deleted file mode 100644 index 160165024e..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list_current.handlebars +++ /dev/null @@ -1,3 +0,0 @@ -
    -
  • {{formatMessage (intlGet 'messages.loading')}}
  • -
\ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list_pending.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list_pending.handlebars deleted file mode 100644 index 6a713c3e40..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_list_pending.handlebars +++ /dev/null @@ -1,3 +0,0 @@ -
    -
  • {{formatMessage (intlGet 'messages.pending_loading')}}
  • -
\ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_set_item.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_set_item.handlebars deleted file mode 100644 index fcdf57dcb7..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_set_item.handlebars +++ /dev/null @@ -1,21 +0,0 @@ -
-
-
- storage -
-
- -

{{channelset.name}}

-

{{channelset.description}}

-
-
-
diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_set_list.handlebars b/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_set_list.handlebars deleted file mode 100644 index f1e8d9606e..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/hbtemplates/channel_set_list.handlebars +++ /dev/null @@ -1,9 +0,0 @@ - -
  • {{formatMessage (intlGet 'messages.loading')}}
\ No newline at end of file diff --git a/contentcuration/contentcuration/static/js/edit_channel/new_channel/views.js b/contentcuration/contentcuration/static/js/edit_channel/new_channel/views.js deleted file mode 100644 index f875cdb93b..0000000000 --- a/contentcuration/contentcuration/static/js/edit_channel/new_channel/views.js +++ /dev/null @@ -1,689 +0,0 @@ -var Backbone = require("backbone"); -var _ = require("underscore"); -require("channel_create.less"); -var Dropzone = require("dropzone"); -require("dropzone/dist/dropzone.css"); -var Models = require("edit_channel/models"); -var BaseViews = require("edit_channel/views"); -var ImageViews = require("edit_channel/image/views"); -var DetailView = require('edit_channel/details/views'); -var ChannelSetViews = require("edit_channel/channel_set/views"); -var Info = require("edit_channel/information/views"); -var get_cookie = require("utils/get_cookie"); -var stringHelper = require("edit_channel/utils/string_helper") -var dialog = require("edit_channel/utils/dialog"); -const State = require("edit_channel/state"); -const Constants = require("edit_channel/constants/index"); - -var NAMESPACE = "newChannel"; -var MESSAGES = { - "channel": "Channel", - "header": "My Channels", - "starred": "Starred", - "public": "Public", - "add_channel_title": "Create a new channel", - "pending_loading": "Checking for invitations...", - "copy_id": "Copy ID to clipboard", - "copy_token": "Copy Token", - "copy_prompt": "Copy token to import channel into Kolibri", - "unpublished": "(Unpublished)", - "view_only": "View Only", - "invitation_error": "Invitation Error", - "declining_invitation": "Declining Invitation", - "declining_invitation_message": "Are you sure you want to decline this invitation?", - "decline": "DECLINE", - "accept": "ACCEPT", - "accept_prompt": "has invited you to", - "accept_success": "Accepted invitation to", - "decline_success": "Declined invitation to", - "edit": "edit", - "view": "view", - "edit_channel": "Edit Details", - "view_channel": "View Details", - "star_channel": "Star Channel", - "unstar_channel": "Remove Star", - "viewonly": "View-Only", - "last_updated": "Updated {updated}", - "starred_channel": "Star Added!", - "unstarred_channel": "Star Removed", - "create": "Create", - "channel_title_prompt": "('CTRL' or 'CMD' + click to open in new tab)", - "channel_detail_prompt": "Select a channel to view details", - "channel_details": "About this Channel", - "open_channel": "Open Channel", - "save": "SAVE", - "dont_save": "Discard Changes", - "keep_open": "Keep Editing", - "channel_sets": "Collections", - "about_channel_sets": "About Collections", - "channel_set_description": "You can package together multiple Studio channels to create a collection. Use a collection token to make multiple channels available for import at once in Kolibri!", - "channel_set": "Collection", - "add_channel_set_title": "Create a new collection of channels", - "channel_count": "{count, plural,\n =1 {# Channel}\n other {# Channels}}", - "delete_channel_set": "Delete Collection", - "delete_channel_set_text": "Are you sure you want to PERMANTENTLY delete this channel collection?" -} - -var ChannelListPage = BaseViews.BaseView.extend({ - template: require("./hbtemplates/channel_create.handlebars"), - list_selector: "#channel_list", - name: NAMESPACE, - $trs: MESSAGES, - initialize: function(options) { - _.bindAll(this, 'new_channel', 'set_all_models', 'toggle_panel'); - this.open = false; - this.render(); - }, - render: function() { - this.$el.html(this.template(null, { - data: this.get_intl_data() - })); - this.pending_channel_list = new PendingChannelList({container: this, el: this.$("#pending_list")}); - var self = this; - State.current_user.get_channels().then(function(channels){ - self.current_channel_list = new CurrentChannelList({ - container: self, - el: self.$("#channel_list"), - collection: channels - }); - }); - State.current_user.get_bookmarked_channels().then(function(channels){ - self.starred_channel_list = new StarredChannelList({ - container: self, - el: self.$("#starred_list"), - collection: channels - }); - }); - State.current_user.get_public_channels().then(function(channels){ - self.public_channel_list = new PublicChannelList({ - container: self, - el: self.$("#public_list"), - collection: channels - }); - }); - State.current_user.get_view_only_channels().then(function(channels){ - self.viewonly_channel_list = new ViewOnlyChannelList({ - container: self, - el: self.$("#viewonly_list"), - collection: channels - }); - }); - State.current_user.get_user_channel_collections().then(function(sets){ - self.channel_set_list = new ChannelSetList({ - container: self, - el: self.$("#channel_set_list"), - collection: sets - }); - }); - }, - events: { - 'click .new_channel_button' : 'new_channel', - "click #close_details": "close_details", - }, - new_channel: function(){ - if (this.current_channel_list.new_channel){ - this.current_channel_list.new_channel(); - } - }, - add_channel: function(channel, category){ - switch(category){ - case "edit": - this.current_channel_list.add_channel(channel); - this.$('#manage-channel-nav a[href="#channels"]').tab('show'); - break; - case "view": - this.viewonly_channel_list.add_channel(channel); - this.$('#manage-channel-nav a[href="#viewonly"]').tab('show'); - break; - case "star": - this.starred_channel_list.add_channel(channel); - break; - } - this.set_all_models(channel); - }, - delete_channel: function(channel){ - this.starred_channel_list.delete_channel(channel); - this.current_channel_list.delete_channel(channel); - this.public_channel_list.delete_channel(channel); - this.viewonly_channel_list.delete_channel(channel); - this.channel_set_list && this.channel_set_list.delete_channel(channel); - this.toggle_panel(); - }, - remove_star: function(channel){ - this.starred_channel_list.remove_channel(channel); - this.set_all_models(channel); - }, - set_active_channel(channel) { - this.$el.removeClass("active_channel"); - // This function gets called by open_channel, but since the API calls to populate the channels list can be slow, - // these lists may be undefined when this gets called. For now, just check that the list is undefined to prevent - // this from throwing errors. - // TODO: The real fix is to make the channel list code more performant so that this doesn't happen. - if (this.starred_channel_list) { - this.starred_channel_list.set_active_channel(channel); - } - if (this.current_channel_list) { - this.current_channel_list.set_active_channel(channel); - } - if (this.public_channel_list) { - this.public_channel_list.set_active_channel(channel); - } - if (this.viewonly_channel_list) { - this.viewonly_channel_list.set_active_channel(channel); - } - }, - set_all_models: function(channel){ - this.starred_channel_list.set_model(channel); - this.current_channel_list.set_model(channel); - this.public_channel_list.set_model(channel); - this.viewonly_channel_list.set_model(channel); - }, - close_details: function() { - this.toggle_panel() - }, - toggle_panel: function(view, channel_list_item) { - // Toggle channel details panel - if(!this.current_view || !this.current_view.changed || !this.open) { - this.set_details(view, channel_list_item); - } else { - var self = this; - dialog.dialog(this.get_translation("unsaved_changes"), this.get_translation("unsaved_changes_text"), { - [self.get_translation("dont_save")]: function(){ - self.set_details(view, channel_list_item); - }, - [self.get_translation("keep_open")]:function(){}, - [self.get_translation("save")]:function(){ - self.current_view.submit_changes(); - self.set_details(view, channel_list_item); - }, - }, null); - } - }, - set_details: function(view, channel_list_item) { - // Render channel details - $(".active_channel").removeClass("active_channel"); - if(view) { - this.current_view && this.current_view.remove(); - view.render(); // Calling separately to make background stay the same if user selects "KEEP EDITING" option - if(!this.open) { - this.open = true; - this.$("#channel_preview_wrapper").animate({ width: 650 }, 500); - this.$("#channel_list_wrapper").addClass("show-panel"); - } - this.current_view = view; - $("#channel_details_panel").html(view.el); - channel_list_item && this.set_active_channel(channel_list_item); - } else if(this.open) { - this.open = false; - this.$("#channel_preview_wrapper").animate({ width: 0 }, 500); - this.$("#channel_list_wrapper").removeClass("show-panel"); - } - } -}); - -var ChannelList = BaseViews.BaseEditableListView.extend({ - template: require("./hbtemplates/channel_list.handlebars"), - list_selector: ".channel_list", - default_item: ".default-item", - name: NAMESPACE, - $trs: MESSAGES, - initialize: function(options) { - this.bind_edit_functions(); - _.bindAll(this, "add_channel", "delete_channel", "save_new_channel"); - this.container = options.container; - this.collection = options.collection; - this.render(); - }, - render: function() { - this.$el.html(this.template(null, { - data: this.get_intl_data() - })); - this.load_content(); - }, - create_new_view:function(data){ - var newView = new ChannelListItem({ - model: data, - containing_list_view: this, - container: this.container - }); - this.views.push(newView); - return newView; - }, - save_new_channel: function(channel) { - var view = this.add_channel(channel); - view.open_channel(); - }, - add_channel: function(channel){ - if(!this.collection.findWhere({id: channel.id})) { - this.collection.add(channel); - var newView = this.create_new_view(channel); - newView.$el.css('display', 'none'); - newView.$el.fadeIn(300); - this.$(this.list_selector).prepend(newView.el); - this.$(".default-item").css('display', 'none'); - return newView; - } - }, - remove_channel: function(channel) { - this.collection.remove(channel); - this.render(); - }, - set_model: function(channel){ - _.each(this.views, function(view){ - if(view.model.id === channel.id) { - view.model.set(channel.toJSON()); - view.render(); - } - }); - }, - set_active_channel: function(channel) { - // Show channel as being opened across lists - _.each(this.views, function(view){ - if(view.model.id === channel.id) { - view.$el.addClass("active_channel"); - } - }); - }, - delete_channel: function(channel){ - this.collection.remove(channel); - this.render(); - } -}); - -var CurrentChannelList = ChannelList.extend({ - new_channel: function(){ - var preferences = (typeof window.user_preferences === "string")? JSON.parse(window.user_preferences) : window.user_preferences; - - var data = { - editors: [State.current_user.id], - pending_editors: [], - language: window.user_preferences.language, - content_defaults: preferences - }; - var detail_view = new DetailView.ChannelDetailsView({ - model: new Models.ChannelModel(data), - allow_edit: true, - onnew: this.save_new_channel, - onclose: this.container.toggle_panel - }); - this.container.toggle_panel(detail_view); - } -}); - -var StarredChannelList = ChannelList.extend({}); - -var PublicChannelList = ChannelList.extend({}); - -var ViewOnlyChannelList = ChannelList.extend({}); - -var ChannelListItem = BaseViews.BaseListEditableItemView.extend({ - name: NAMESPACE, - $trs: MESSAGES, - tagName: "li", - id: function(){ - return (this.model)? this.model.get("id") : "new"; - }, - className:"channel_container", - template: require("./hbtemplates/channel_item.handlebars"), - initialize: function(options) { - this.bind_edit_functions(); - _.bindAll(this, 'delete_channel', 'star_channel', 'unstar_channel', 'set_star_icon', 'set_model'); - this.listenTo(this.model, "sync", this.set_model); - this.containing_list_view = options.containing_list_view; - this.container = options.container; - this.can_edit = this.model.get("editors").indexOf(State.current_user.id) >= 0; - this.render(); - }, - set_is_new:function(isNew){ - this.isNew = isNew; - }, - set_model: function(data) { - this.container.set_all_models(this.model); - }, - render: function() { - this.$el.html(this.template({ - can_edit: this.can_edit, - channel: this.model.toJSON(), - total_file_size: this.model.get("size"), - resource_count: this.model.get("count"), - channel_link : this.model.get("id"), - picture : (this.model.get("thumbnail_encoding") && this.model.get("thumbnail_encoding").base64) || this.model.get("thumbnail_url"), - modified: this.model.get("modified") || new Date(), - languages: Constants.Languages, - language: Constants.Languages.find(language => language.id === this.model.get("language")), - new: this.isNew - }, { - data: this.get_intl_data() - })); - this.$('[data-toggle="tooltip"]').tooltip(); - }, - events: { - 'click .star_channel': 'star_channel', - 'click .unstar_channel': 'unstar_channel', - 'mouseover .channel_option_icon':'remove_highlight', - 'mouseover .copy-id-btn':'remove_highlight', - 'click .copy-id-btn' : 'copy_id', - 'click .open_channel': 'open_channel', - 'mouseover .open_channel': 'add_highlight', - 'mouseleave .open_channel': 'remove_highlight' - }, - remove_highlight:function(event){ - event.stopPropagation(); - event.preventDefault(); - this.$el.removeClass('highlight'); - }, - add_highlight:function(event){ - this.$el.addClass('highlight'); - }, - open_channel:function(event){ - if(!event || this.$el.hasClass('highlight')){ - if (event && (event.metaKey || event.ctrlKey)) { - var open_url = '/channels/' + this.model.get("id") + ((this.can_edit)? '/edit' : '/view'); - window.open(open_url, '_blank'); - } else if(!this.$el.hasClass('active_channel')) { - var detail_view = new DetailView.ChannelDetailsView({ - model: this.model, - allow_edit: this.can_edit, - ondelete: this.delete_channel, - onstar: this.star_channel, - onunstar: this.unstar_channel - }); - this.container.toggle_panel(detail_view, this.model); - } - } - }, - copy_id:function(event){ - event.stopPropagation(); - event.preventDefault(); - var self = this; - this.$(".copy-id-text").focus(); - this.$(".copy-id-text").select(); - try { - document.execCommand("copy"); - self.$(".copy-id-btn").text("check"); - } catch(e) { - self.$(".copy-id-btn").text("clear"); - } - setTimeout(function(event){ - self.$(".copy-id-btn").text("content_paste"); - }, 2500); - }, - star_channel: function(event, star_icon){ - var self = this; - this.model.add_bookmark(State.current_user.id).then(function() { - self.model.set("is_bookmarked", true); - self.render(); - self.set_star_icon(self.get_translation("unstar_channel"), star_icon); - self.container.add_channel(self.model, "star"); - }); - }, - unstar_channel: function(event, star_icon){ - var self = this; - this.model.remove_bookmark(State.current_user.id).then(function() { - self.model.set("is_bookmarked", false); - self.render(); - self.set_star_icon(self.get_translation("star_channel"), star_icon); - self.container.remove_star(self.model); - }); - }, - set_star_icon: function(new_message, star_icon){ - star_icon = star_icon || this.$(".star_option"); - star_icon.tooltip("hide"); - - // Keep channel details in sync with channel list - $("#channel_details_view_panel .star_icon").html(this.$(".star_option").html()); - $("#channel_details_view_panel .star_icon").attr("data-original-title", new_message); - this.$(".star_option").attr("data-original-title", new_message); - }, - delete_channel: function(model){ - this.container.delete_channel(this.model); - } -}); - -var PendingChannelList = ChannelList.extend({ - template: require("./hbtemplates/channel_list_pending.handlebars"), - list_selector: "#channel_list_pending", - initialize: function(options) { - this.bind_edit_functions(); - this.container = options.container; - this.collection = new Models.InvitationCollection(); - this.render(); - }, - render: function() { - this.$el.html(this.template(null, { - data: this.get_intl_data() - })); - var self = this; - State.current_user.get_pending_invites().then(function(invitations){ - self.collection.reset(invitations.toJSON()); - self.load_content(self.collection, " "); - }); - }, - create_new_view:function(data){ - var newView = new ChannelListPendingItem({ - model: data, - containing_list_view: this, - }); - this.views.push(newView); - return newView; - }, - invitation_submitted: function(invitation, channel){ - this.collection.remove(invitation); - if(channel){ - this.container.add_channel(channel, invitation.get("share_mode")); - } - } -}); - -var ChannelListPendingItem = BaseViews.BaseListEditableItemView.extend({ - name: NAMESPACE, - $trs: MESSAGES, - tagName: "li", - id: function(){ - return (this.model)? this.model.get("id") : "new"; - }, - className:"pending_container", - template: require("./hbtemplates/channel_item_pending.handlebars"), - initialize: function(options) { - this.bind_edit_functions(); - _.bindAll(this, 'accept','decline', 'submit_invitation'); - this.listenTo(this.model, "sync", this.render); - this.containing_list_view = options.containing_list_view; - this.status = null; - this.render(); - }, - render: function() { - this.$el.html(this.template({ - invitation: this.model.toJSON(), - status: this.status - }, { - data: this.get_intl_data() - })); - }, - events: { - 'click .accept_invite':'accept', - 'click .decline_invite':'decline' - }, - accept: function(){ - var self = this; - this.model.accept_invitation().then(function(channel){ - self.submit_invitation(true, channel); - }).catch(function(error){ - console.error(error); - dialog.alert(self.get_translation("invitation_error"), error); - }); - }, - decline: function(){ - var self = this; - dialog.dialog(self.get_translation("declining_invitation"), self.get_translation("declining_invitation_message"), { - [self.get_translation("cancel")]:function(){}, - [self.get_translation("decline")]: function(){ - self.model.decline_invitation().then(function(){ - self.submit_invitation(false, null); - }); - }, - }, function(){ }); - }, - submit_invitation: function(accepted, channel){ - // Show invitation was accepted - this.status = {"accepted" : accepted}; - this.render(); - this.containing_list_view.invitation_submitted(this.model, channel) - } -}); - -var ChannelSetList = BaseViews.BaseEditableListView.extend({ - template: require("./hbtemplates/channel_set_list.handlebars"), - list_selector: ".channel_list", - default_item: ".default-item", - name: NAMESPACE, - $trs: MESSAGES, - initialize: function(options) { - this.bind_edit_functions(); - _.bindAll(this, "save_new_channel_set"); - this.container = options.container; - this.collection = options.collection; - this.render(); - this.listenTo(this.collection, "add", this.render); - this.listenTo(this.collection, "remove", this.render); - }, - events: { - 'click .new_set_button': 'new_channel_set', - 'click .about_sets_button': 'open_about_sets' - }, - new_channel_set: function() { - var channel_set_view = new ChannelSetViews.ChannelSetModalView({ - modal: true, - onsave: this.save_new_channel_set, - isNew: true, - model: new Models.ChannelSetModel() - }); - }, - save_new_channel_set: function(channel_set) { - this.collection.add(channel_set); - }, - render: function() { - this.$el.html(this.template(null, { - data: this.get_intl_data() - })); - this.load_content(this.collection, this.get_translation("channel_set_description")); - }, - create_new_view:function(data){ - var newView = new ChannelSetListItem({ - model: data, - containing_list_view: this, - container: this.container - }); - this.views.push(newView); - return newView; - }, - open_about_sets: function() { - var channel_set_info_modal = new Info.ChannelSetModalView({}); - }, - delete_channel: function(channel) { - // Find channel sets that have the deleted channel and reload their views - var channelSetCollection = new Models.ChannelSetCollection( - this.collection.filter(function(channelset) { - return _.contains(channelset.get('channels'), channel.id); - }) - ); - - var self = this; - channelSetCollection.fetch({ - success: function(collection) { - collection.each(function(model) { - var view = _.find(self.views, function(view) { - return view.model.id === model.id; - }); - view.reload(model); - }); - } - }); - } -}); - -var ChannelSetListItem = BaseViews.BaseListEditableItemView.extend({ - name: NAMESPACE, - $trs: MESSAGES, - tagName: "li", - id: function(){ - return (this.model)? this.model.get("id") : "new"; - }, - className:"channel_container", - template: require("./hbtemplates/channel_set_item.handlebars"), - initialize: function(options) { - this.bind_edit_functions(); - _.bindAll(this, "delete_channel_set", "reload"); - this.containing_list_view = options.containing_list_view; - this.container = options.container; - this.render(); - }, - render: function() { - this.$el.html(this.template({ - channelset: this.model.toJSON(), - }, { - data: this.get_intl_data() - })); - this.$('[data-toggle="tooltip"]').tooltip(); - }, - events: { - 'mouseover .channel_option_icon':'remove_highlight', - 'mouseover .copy-id-btn':'remove_highlight', - 'click .copy-id-btn' : 'copy_id', - 'click .open_channel_set': 'open_channel_set', - 'mouseover .open_channel_set': 'add_highlight', - 'mouseleave .open_channel_set': 'remove_highlight', - 'mouseover .delete_channel_set': 'remove_highlight', - 'click .delete_channel_set': 'delete_channel_set' - }, - remove_highlight:function(event){ - event.stopPropagation(); - event.preventDefault(); - this.$el.removeClass('highlight'); - }, - add_highlight:function(event){ - this.$el.addClass('highlight'); - }, - open_channel_set:function(event){ - var channel_set_view = new ChannelSetViews.ChannelSetModalView({ - modal: true, - onsave: this.reload, - isNew: false, - model: this.model - }); - }, - copy_id:function(event){ - event.stopPropagation(); - event.preventDefault(); - var self = this; - this.$(".copy-id-text").focus(); - this.$(".copy-id-text").select(); - try { - document.execCommand("copy"); - self.$(".copy-id-btn").text("check"); - } catch(e) { - self.$(".copy-id-btn").text("clear"); - } - setTimeout(function(event){ - self.$(".copy-id-btn").text("content_paste"); - }, 2500); - }, - delete_channel_set: function(event){ - event.stopImmediatePropagation(); - var self = this; - dialog.dialog(self.get_translation("delete_channel_set"), self.get_translation("delete_channel_set_text"), { - [self.get_translation("cancel")]:function(){}, - [self.get_translation("delete_channel_set")]: function(){ - self.model.destroy({ - success: function() { - self.containing_list_view.collection.remove(self.model); - } - }); - }, - }, function(){ }); - } -}); - -module.exports = { - ChannelListPage : ChannelListPage -} diff --git a/contentcuration/contentcuration/static/js/edit_channel/router.js b/contentcuration/contentcuration/static/js/edit_channel/router.js index e6e4417717..54bd5a9ed1 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/router.js +++ b/contentcuration/contentcuration/static/js/edit_channel/router.js @@ -2,6 +2,9 @@ var _ = require("underscore"); var Backbone = require("backbone"); var State = require("./state"); +import Vue from 'vue'; +import ChannelListPage from 'edit_channel/channel_list/views/ChannelListPage.vue'; + //var saveDispatcher = _.clone(Backbone.Events); var URL_CHAR_LIMIT = 7; @@ -19,9 +22,13 @@ var ChannelEditRouter = Backbone.Router.extend({ }, navigate_channel_home: function() { - var ChannelManageView = require("edit_channel/new_channel/views"); - var channel_manager_view = new ChannelManageView.ChannelListPage ({ - el: $("#channel-container"), + var store = require("edit_channel/channel_list/vuex/store"); + State.setChannelListState(); + + new Vue({ + el: '#channel-container', + store, + ...ChannelListPage }); }, diff --git a/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/CopyToken.vue b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/CopyToken.vue new file mode 100644 index 0000000000..e135c83de2 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/CopyToken.vue @@ -0,0 +1,92 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/Star.vue b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/Star.vue new file mode 100644 index 0000000000..20cc768a9b --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/Star.vue @@ -0,0 +1,57 @@ + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/ToggleText.vue b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/ToggleText.vue new file mode 100644 index 0000000000..14ab6fe76b --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/ToggleText.vue @@ -0,0 +1,102 @@ + + + + + + + diff --git a/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/copyToken.spec.js b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/copyToken.spec.js new file mode 100644 index 0000000000..d0a4d5577b --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/copyToken.spec.js @@ -0,0 +1,24 @@ +import { mount } from '@vue/test-utils'; +import CopyToken from '../CopyToken.vue'; + +function makeWrapper() { + return mount(CopyToken, { + propsData: { + token: "testtoken" + } + }) +} + +describe('copyToken', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }) + it('text should be populated on load', () => { + let token = wrapper.find({ref: 'tokenText'}); + expect(token.element.value).toEqual('testtoken'); + expect(wrapper.vm.copyStatus === "IDLE"); + }); + // TODO: Need to figure out a way to test if text was properly + // copied (document.execCommand not supported) +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/star.spec.js b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/star.spec.js new file mode 100644 index 0000000000..ba5c23f903 --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/star.spec.js @@ -0,0 +1,33 @@ +import { mount } from '@vue/test-utils'; +import Star from '../Star.vue'; + +function makeWrapper(starred) { + return mount(Star, { + propsData: { + starred: starred + } + }) +} + +describe('star', () => { + let starredWrapper; + let unstarredWrapper; + beforeEach(() => { + starredWrapper = makeWrapper(true); + unstarredWrapper = makeWrapper(false); + }) + it('should reflect correct star on load', () => { + expect(starredWrapper.find('a').is('.starred')).toBe(true); + expect(unstarredWrapper.find('a').is('.starred')).toBe(false); + }); + it('starred components should emit an unstarred event when clicked', () => { + starredWrapper.find('a').trigger('click'); + expect(starredWrapper.emitted('unstarred').length).toBe(1); + expect(starredWrapper.emitted('starred')).toBeFalsy(); + }); + it('unstarred components should emit a starred event when clicked', () => { + unstarredWrapper.find('a').trigger('click'); + expect(unstarredWrapper.emitted('starred').length).toBe(1); + expect(unstarredWrapper.emitted('unstarred')).toBeFalsy(); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/toggleText.spec.js b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/toggleText.spec.js new file mode 100644 index 0000000000..b3f771616a --- /dev/null +++ b/contentcuration/contentcuration/static/js/edit_channel/sharedComponents/__tests__/toggleText.spec.js @@ -0,0 +1,30 @@ +import { mount } from '@vue/test-utils'; +import ToggleText from '../ToggleText.vue'; + +function makeWrapper(splitAt) { + return mount(ToggleText, { + propsData: { + text: "test test test test", + splitAt: splitAt + } + }) +} + +describe('toggleText', () => { + it('text should be split around the split index', () => { + let splitWrapper = makeWrapper(5); + expect(splitWrapper.vm.overflowText).toBe(' test test test'); + splitWrapper = makeWrapper(17) + expect(splitWrapper.vm.overflowText).toBeFalsy(); + }); + it('clicking the toggle button should collapse/expand text', () => { + let splitWrapper = makeWrapper(5); + let toggler = splitWrapper.find('.toggler'); + let overflow = splitWrapper.find('.overflow'); + expect(overflow.is('.expanded')).toBe(false); + toggler.trigger('click'); + expect(overflow.is('.expanded')).toBe(true); + toggler.trigger('click'); + expect(overflow.is('.expanded')).toBe(false); + }); +}); diff --git a/contentcuration/contentcuration/static/js/edit_channel/state.js b/contentcuration/contentcuration/static/js/edit_channel/state.js index 561429872f..a95913ae3d 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/state.js +++ b/contentcuration/contentcuration/static/js/edit_channel/state.js @@ -24,6 +24,9 @@ const State = { current_user: new Models.UserModel(window.user), nodeCollection: new Models.ContentNodeCollection(), currentLanguage: Constants.Languages.find(l => l.id && l.id.toLowerCase() === (window.languageCode || 'en') ), + setChannelListState() { + this.preferences = (typeof window.user_preferences === "string")? JSON.parse(window.user_preferences) : window.user_preferences; + }, openChannel(data) { this.staging = data.is_staging || false; Store.commit('SET_CONTENT_TAGS', this.current_channel.get('tags')) diff --git a/contentcuration/contentcuration/static/js/edit_channel/utils/dialog.js b/contentcuration/contentcuration/static/js/edit_channel/utils/dialog.js index b5d970afb6..77c49b4ee9 100644 --- a/contentcuration/contentcuration/static/js/edit_channel/utils/dialog.js +++ b/contentcuration/contentcuration/static/js/edit_channel/utils/dialog.js @@ -1,3 +1,5 @@ +// TODO: REMOVE BACKBONE + var Models = require("edit_channel/models"); const State = require("edit_channel/state"); var template = require("edit_channel/utils/hbtemplates/dialog.handlebars"); diff --git a/contentcuration/contentcuration/static/less/channel_create.less b/contentcuration/contentcuration/static/less/channel_create.less deleted file mode 100644 index 7aaeea86a5..0000000000 --- a/contentcuration/contentcuration/static/less/channel_create.less +++ /dev/null @@ -1,416 +0,0 @@ -@import "global-variables.less"; - -@btn-filter-color:#C5C5C5; -@btn-find-color:#6FAFE3; - -@channel-clipboard-btn-color: #F2F2F2; -@channel-btn-border-color:#7A7A7A; -@details-bg-color: #EFE6EC; - -// nested flexbox settings to ensure that the channels list view is vertically responsive. -body { - display: flex; - flex-direction: column; - - #channel-container { - display: flex; - height: 0; - flex-direction: column; - flex-grow: 1; - - #top-navigation { - flex: 0; - } - - #channel_area_wrapper { - display: flex; - max-height: 100%; - flex: 1; - - #channel_list_wrapper { - display: grid; - justify-content: normal; - align-content: flex-start; - - #channel-list-area { - margin: auto; - } - } - - #channel_preview_wrapper { - display: flex; - flex-direction: column; - flex: 1; - - #channel_details_overlay { - display: flex; - flex-direction: column; - flex: 1; - - #channel_details_panel { - display: flex; - flex-direction: column; - flex: 1; - - #channel_details_view_panel { - display: flex; - flex-direction: column; - flex: 1; - - #channel_details { - flex-direction: column; - flex-grow: 1; - width: 100%; - } - } - } - } - } - } - } -} -// end of flexbox settings - -.default-item{ - color:gray; - padding:5px 10px; -} - -#channel_list_wrapper{ - padding:10px; - overflow-y:auto; - width: 100vw; - transition: width 500ms; - &.show-panel { - width: calc(~"100vw - 650px"); - } - #manage-channel-nav.nav-tabs{ - border-bottom: none; - margin-bottom: 10px; - .channel_tab{ - font-size: 14pt; - color: @body-font-color; - margin-right: 10px; - &:hover { border-color: @body-background; } - } - .active .channel_tab{ - background-color: @body-background; - font-weight: bold; - border: none; - border-bottom: 3px solid @blue-500; - } - } - #channel-list-area{ - @channel-container-bg-color: white; - @channel-continer-border-color: #C5C5C5; - @channel-container-height: 250px; - @channel-container-width: 800px; - @channel-profile-width: 150px; - @channel-font-size: 18pt; - @channel-description-font-size: 11pt; - @channel-btn-color:#CCCCCC; - - width: @channel-container-width + 50; - - .channel_list_wrapper { - padding-top: 20px; - .add_channel_wrapper { - margin-bottom: 15px; - padding-right: 50px; - height: 40px; - .new_channel_button, .new_set_button { - cursor: pointer; - font-size: 18px; - text-decoration: none; - &:hover { color:white; } - } - .about_sets_button { - vertical-align: sub; - font-size: 12pt; - padding: 0px; - i { - font-size: 16pt; - vertical-align: sub; - } - } - } - } - - .new-channel { - display: inline-block; - position: relative; - left: 13px; - top: 6px; - } - #pending_list{ - margin-bottom: 30px; - .pending_container{ - font-size: 12pt; - .pending_text{ padding: 5px;} - .btn-pending { - background-color: white; - font-weight:bold; - width: 100%; - border: 2px solid white; - &.accept_invite { color: @blue-500; &:hover{ border-color: @blue-500; }} - &.decline_invite { color: #D14C4C; &:hover{ border-color: #D14C4C; }} - i { - font-size: 14pt; - font-weight: bold; - } - } - .alert{ - margin-bottom: 15px; - padding: 5px 15px; - button{ right: 0px; top: 5px; } - } - &:last-child{ margin-bottom: 50px; } - } - } - .channel_list { - padding-bottom: 50px; - .default-item{ - font-size: 12pt; - margin: 20px 10px 10px 10px; - color: @annotation-gray; - text-align: center; - } - .is_selected { - display: none; - padding-top: 9%; - i { - font-size: 45pt; - color: @topnav-bg-color; - font-weight: bold; - } - } - - .channel-container-wrapper { - background-color: @panel-container-color; - width: @channel-container-width; - min-height: @channel-container-height; - height:auto; - box-shadow: @box-shadow; - margin: 0px; - margin-bottom: 30px; - padding: 10px; - border:4px solid @panel-container-color; - cursor:pointer; - - &.channelset-wrapper { - min-height: 0px; - } - - .profile{ - height:100%; - width: @channel-profile-width; - margin: 5px 15px 5px 5px; - .image_dropzone{ width: auto; } - img{ - width:130px; - height:130px; - object-fit: cover; - } - } - .channel_set_icon { - font-size: 65pt; - color: @gray-700; - } - .delete_channel_set { - color: @gray-700; - } - .manage_area{ - width:100%; - .open_channel { - .action-button; - margin-right:10px; - } - } - - .channel_metadata{ - color: @annotation-gray; - padding: 0px; - font-size: 11pt; - .resource-icon { - padding: 0 3px 0 10px; - } - .copy-id-btn{ - padding:3px; - font-size: 16pt; - vertical-align: sub; - &:hover { color:@blue-500; } - } - .copy-id-text{ - display: inline-block; - padding:2px; - background-color: @gray-300; - font-size:11pt; - border:none; - font-weight: bold; - width: 125px; - color: @gray-700; - } - .channel_language{ - max-width: 135px; - display: inline-block; - margin-bottom: -3px; - } - } - #channel_name_error{ - color:@red-error-color; - font-weight:bold; - margin:0; - padding:0; - visibility: hidden; - } - h4{ - font-size: @channel-font-size; - font-weight: bold; - color: @body-font-color; - margin-bottom: 0px; - padding-bottom: 5px; - .channel_name{ - color: @body-font-color; - height:@channel-font-size + 10px; - } - .channel_option_icon { - cursor:pointer; - display:inline-block; - font-size: 20pt; - padding:10px; - color: @gray-700; - position: relative; - top: -45px; - &:not(.disabled):hover{ color: @blue-500; } - &.unstar_channel{ - color: @blue-500; - &:hover{ color: @blue-200; } - } - } - } - .description{ - min-height: @channel-container-height - @channel-font-size - 115px; - overflow-y:auto; - margin-bottom:5px; - p{ - font-size: @channel-description-font-size; - } - } - - #new_channel_name, textarea{ - height: 35px; - width: @channel-container-width - @channel-profile-width - 50px; - color: @body-font-color; - background: transparent; - border: none; - border-bottom: 1px solid #BDBDBD; - margin-top: 10px; - } - - #new_channel_name:focus, textarea:focus { - outline: none; - border-bottom: 3px solid #BDBDBD; - .input-form; - } - .delete_channel{ - color: @delete-color; - font-weight: bold; - margin-top:10px; - font-size:9pt; - } - - .channel_options{ - width: @channel-container-width - 40px; - padding-left:@channel-profile-width; - top: 20px; - .save_channel { - .action-button; - } - } - input[type=text] { - font-size: 20px; - } - textarea{ - font-size: 16px; - resize: none; - height: @channel-container-height * 0.3; - margin-top: 25px; - } - - .tag_area{ - li{ - background-color: @channel-btn-color; - border: 1px solid @channel-btn-border-color; - color:black; - padding: 0px 5px; - font-size:9pt; - } - } - .updated_time { - font-style: italic; - font-size: 10pt; - color: gray; - } - .delete_channel_set:hover { - color: @red-error-color; - } - } - .active_channel{ - .is_selected { - display: block; - } - .channel-container-wrapper { - border-color: @topnav-bg-color; - } - } - .highlight { - .is_selected i{ - color: @blue-500 !important; - } - .channel-container-wrapper { - border:4px solid @blue-500 !important; - .channel_name{ - color:@blue-500 !important; - } - } - - } - } - } -} - -@channel-preview-width: 650px; -#channel_preview_wrapper { - overflow: hidden; - width: 0px; - margin-left: 2px; - background-color: white; - background-size: cover; - background-repeat: no-repeat; - background-position: center center; - #channel_details_overlay { - height: inherit; - background-color: rgba(240,240,240, 0.8); - } - - #close_wrapper { - margin: 12px 0px; - #close_details { margin-right: 20px; } - } - - #channel_details_panel { - margin: 0px auto; - float: none; - display: table-cell; - vertical-align: top; - height: 83vh; - width: @channel-preview-width - 40px; - .default-item { - padding-top: 45%; - font-size: 15pt; - font-weight: bold; - text-align: center; - color: @gray-300; - } - } -} diff --git a/contentcuration/contentcuration/static/less/channel_list.less b/contentcuration/contentcuration/static/less/channel_list.less new file mode 100644 index 0000000000..7c576fbe9a --- /dev/null +++ b/contentcuration/contentcuration/static/less/channel_list.less @@ -0,0 +1,204 @@ +@import "global-variables.less"; + +@channel-preview-width: 650px; +@channel-item-width: 70vw; +@channel-item-max-width: 800px; +@channel-item-min-width: 400px; +@channel-container-height: 250px; +@channel-profile-width: 150px; +@channel-thumbnail-size: 150px; + +// nested flexbox settings to ensure that the channels list view is vertically responsive. +body { + display: flex; + flex-direction: column; + + #channel-container { + display: flex; + height: 0; + flex-direction: column; + flex-grow: 1; + + #top-navigation { + flex: 0; + } + + #channel-area-wrapper { + display: flex; + max-height: 100%; + flex: 1; + + #channel-list-wrapper { + display: grid; + justify-content: normal; + align-content: flex-start; + + #channel-list-area { + margin: auto; + } + } + + #channel-preview-wrapper { + display: flex; + flex-direction: column; + flex: 1; + + #channel-details-overlay { + display: flex; + flex-direction: column; + flex: 1; + + #channel-details-panel { + display: flex; + flex-direction: column; + flex: 1; + + #channel-details-view-panel { + display: flex; + flex-direction: column; + flex: 1; + + #channel-details { + flex-direction: column; + flex-grow: 1; + width: 100%; + } + } + } + } + } + } + } +} +// end of flexbox settings + +.channel-list-width { + width: @channel-item-width; + max-width: @channel-item-max-width; + min-width: @channel-item-min-width; + margin: 50px auto; +} + +.channel-list { + .channel-list-width; + .new-button { + text-align: right; + margin-bottom: 30px; + a, button { + font-size: 14pt; + span { + font-size: 18pt; + } + } + } + .default-item { + color: @gray-500; + font-size: 16pt; + font-style: italic; + font-weight: bold; + text-align: center; + } + .channel-container-wrapper { + .channel-list-width; + .thumbnail-title-columns; + background-color: @panel-container-color; + box-shadow: @box-shadow; + margin-top: 0px; + margin-bottom: 20px; + padding: 10px; + border:4px solid @panel-container-color; + cursor:pointer; + position: relative; + + .profile { + margin: 5px 15px 5px 5px; + text-align: left; + img{ + width:@channel-thumbnail-size; + height:@channel-thumbnail-size; + object-fit: cover; + } + span { + font-size: 65pt; + color: @gray-700; + } + } + .channel-options-wrapper { + display: grid; + grid-auto-flow: column; + justify-content: space-between; + .channel-metadata { + color: @annotation-gray; + font-size: 11pt; + div { + display: inline-block; + &:not(:last-child)::after { + content: ' • '; + } + + .channel-language { + .truncate; + max-width: 135px; + } + .copy-id-btn{ + padding:3px; + font-size: 16pt; + vertical-align: sub; + &:hover { color:@blue-500; } + } + input { + display: inline-block; + padding: 2px; + background-color: @gray-300; + font-size: 11pt; + border:none; + font-weight: bold; + width: 120px; + text-align: center; + color: @gray-700; + } + } + } + .option { + font-size: 20pt; + padding:10px; + color: @gray-700; + margin: -15px 0px; + &.star-option::before { + .material-icons; + content: "star_border"; + } + &.star-option:hover{ + color: @blue-500; + } + &.starred{ + color: @blue-500; + &::before { + content: "star"; + } + &:hover{ + color: @blue-200; + } + } + &.delete-channelset:hover { + color: @red-error-color; + } + } + } + + + h4 { + .truncate; + font-size: 18pt; + font-weight: bold; + color: @body-font-color; + padding-bottom: 5px; + } + .description { + .wordwrap; + font-size: 11pt; + margin-bottom:5px; + } + } + + } diff --git a/contentcuration/contentcuration/static/less/details.less b/contentcuration/contentcuration/static/less/details.less index 038a396812..42b5f93815 100644 --- a/contentcuration/contentcuration/static/less/details.less +++ b/contentcuration/contentcuration/static/less/details.less @@ -1,193 +1,5 @@ @import "global-variables.less"; -#channel_details_overlay #close_details, -#channel_details_view_panel .star_icon { - cursor: pointer; - color: @gray-800; - font-weight: bold; -} - -#channel_details_view_panel { - height: inherit; - .star_icon { - font-size: 30px; - position: absolute; - margin-top: -40px; - } -} -#channel_details{ - display: block; - overflow: hidden; - overflow-y: auto; - padding: 0px; - min-height: 350px; - background-color: rgba(256,256,256,0.8); - height: inherit; - padding-top: 15px; - border: 2px solid @gray-500; - position: relative; - - #channel_details_area { - width: 100%; - margin-left: 10px; - .column { - display: table-column; - &.profile { - padding-top: 30px; - } - .description { - min-height: 25px; - word-wrap: break-word; - } - } - .cancel { padding-left: 0px; } - #submit { margin-right: 10px; } - } - - #channel_error{ display:none; } - .image_dropzone{ width: 120px; } - .profile { padding: 0px; } - .new_channel_pic{ - .image_dropzone{ width: auto; } - } - .channel-download-wrapper { - margin-top: 30px; - margin-bottom: -25px; - padding: 0px; - font-size: 10pt; - text-align: right; - } - .details-download-dropdown { - width: max-content; - } - img{ - width:130px; - height:130px; - object-fit: cover; - } - .channel_pic { - border: 2px solid @gray-500; - min-height: 130px; - } - - h4, .title { - font-size: 10pt; - color: @gray-800; - margin: 2px 0px; - font-weight: bold; - } - h3 { - margin-bottom: 5px; - margin-top: 0px; - font-weight: bold; - } - - .channel_text { - color: @gray-700; - } - .identifier_label { - padding: 0; - padding-top: 5px; - } - .language_wrapper { - min-height: 25px; - b { font-size: 15pt; } - i { - color: @blue-200; - vertical-align: top; - font-size: 20pt; - margin-right: 5px; - } - } - - input[type="text"] { - margin-bottom: 20px; - } - - input[type="text"], textarea { - padding: 2px 0; - font-size: @larger-body-text; - width:100%; - .input-form; - &.copy-id-text { - display: inline-block; - padding: 2px; - background-color: @gray-200; - border: none; - font-weight: bold; - color: @gray-500; - margin-bottom: 5px; - width: 225px; - font-size: 10pt; - } - } - .copy-id-btn{ - padding:3px; - font-size: 16pt; - vertical-align: sub; - cursor: pointer; - color: @gray-500; - &:hover { color:@blue-500; } - } - textarea{ - resize:none; - height:auto; - margin-bottom: 15px; - } - - .details_view { - display: none; - } - #look-inside { border-bottom: 2px solid @blue-200; } - #delete-section { - padding: 5px 30px 35px 30px; - p { - margin-top: 5px; - margin-bottom: 20px; - } - .delete_channel { - color: @delete-color; - font-weight: bold; - } - } - - .required{ - margin-left:5px; - color:@red-error-color; - span{ - font-style:italic; - font-size: 8pt; - } - } - - .tab-content { - border: none; - border-top: 2px solid @blue-200; - } - #open-button { - padding: 5px 25px; - } - .download_option { - padding: 5px 10px; - cursor: pointer; - &:hover { - background-color: @gray-200; - } - } - .toggle_description { - color: @gray-700 !important; - &:hover { color: @blue-500 !important; } - } - .empty_default { - padding: 100px 0px; - margin-top: 0px; - &.loading { border-top: 2px solid @blue-200; } - } - hr { - border-color: @gray-300; - } -} - .details_view { background-color: white; .help_icon { @@ -451,6 +263,7 @@ border: 2px solid @gray-400; width: 100px; height: 100px; + object-fit: cover; border-radius: 5px; background-color: white; } @@ -559,6 +372,7 @@ font-size: 16pt; vertical-align: sub; color: @blue-200; + cursor: pointer; &:hover { color:@blue-500; } } .copy-id-text{ @@ -579,17 +393,3 @@ } } } - -#channel_details_bottom { - height: 40px; - padding: 0px; - padding-top: 5px; - width: 100%; - a { - border-width: 2px !important; - width: inherit; - padding: 5px; - font-size: 14pt; - margin-top: 3px; - } -} diff --git a/contentcuration/contentcuration/static/less/global-variables.less b/contentcuration/contentcuration/static/less/global-variables.less index a1bd5f05f7..9c741c6283 100644 --- a/contentcuration/contentcuration/static/less/global-variables.less +++ b/contentcuration/contentcuration/static/less/global-variables.less @@ -58,7 +58,7 @@ @yellow-bg-color: #F5DC14; .action-button { background: @blue-500; - border: 1px solid @blue-500 !important; + border: 1px solid @blue-500; color: white; padding: 5px 16px; border-radius: 2px; @@ -66,7 +66,7 @@ text-decoration: none; &:hover, &:focus { background: white; - color: @blue-500 !important; + color: @blue-500; font-weight:bold; text-decoration: none; } @@ -279,7 +279,8 @@ ul li.placeholder:before { .uppercase { text-transform: uppercase; } .empty_default{ - margin-top:20%; + margin-top:10%; + margin-bottom:10%; text-align: center; color:@gray-400; font-size:14pt; @@ -343,3 +344,10 @@ ul li.placeholder:before { content: 'rotate_right'; } } + +.thumbnail-title-columns { + display: grid; + grid-auto-flow: column; + justify-content: flex-start; + grid-template-columns: 0fr 1fr; +} diff --git a/contentcuration/contentcuration/views/base.py b/contentcuration/contentcuration/views/base.py index 1d44b7e049..13d7341648 100644 --- a/contentcuration/contentcuration/views/base.py +++ b/contentcuration/contentcuration/views/base.py @@ -370,18 +370,15 @@ def accessible_channels(request, channel_id): return Response(serializer.data) +@api_view(['POST']) def accept_channel_invite(request): - if request.method != 'POST': - return HttpResponseBadRequest("Only POST requests are allowed on this endpoint.") - - data = json.loads(request.body) - invitation = Invitation.objects.get(pk=data['invitation_id']) + invitation = Invitation.objects.get(pk=request.data.get('invitation_id')) channel = invitation.channel channel.is_view_only = invitation.share_mode == VIEW_ACCESS channel_serializer = AltChannelListSerializer(channel) add_editor_to_channel(invitation) - return HttpResponse(JSONRenderer().render(channel_serializer.data)) + return Response(channel_serializer.data) def activate_channel_endpoint(request): diff --git a/package.json b/package.json index c0f6099d9b..ac38149b79 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "homepage": "https://github.com/learningequality/studio#readme", "dependencies": { + "@vue/test-utils": "^1.0.0-beta.29", "backbone": "^1.2.1", "backbone-undo": "^0.2.5", "backbone.paginator": "^2.0.8", diff --git a/yarn.lock b/yarn.lock index 33cc8a79dd..6a9179c812 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2635,6 +2635,7 @@ dateformat@^3.0.3: de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -3956,8 +3957,9 @@ hash.js@^1.0.0, hash.js@^1.0.3: minimalistic-assert "^1.0.1" he@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== hmac-drbg@^1.0.0: version "1.0.1" @@ -8513,8 +8515,9 @@ vue-style-loader@^4.1.0: loader-utils "^1.0.2" vue-template-compiler@^2.5.16: - version "2.5.17" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.17.tgz#52a4a078c327deb937482a509ae85c06f346c3cb" + version "2.6.7" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.7.tgz#7f6c14eacf3c912d28d33b029cde706d9756e00c" + integrity sha512-ZjxJLr6Lw2gj6aQGKwBWTxVNNd28/qggIdwvr5ushrUHUvqgbHD0xusOVP2yRxT4pX3wRIJ2LfxjgFT41dEtoQ== dependencies: de-indent "^1.0.2" he "^1.1.0" @@ -8524,8 +8527,9 @@ vue-template-es2015-compiler@^1.6.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" vue@^2.5.16: - version "2.5.17" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.17.tgz#0f8789ad718be68ca1872629832ed533589c6ada" + version "2.6.7" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.7.tgz#254f188e7621d2d19ee28d0c0442c6d21b53ae2d" + integrity sha512-g7ADfQ82QU+j6F/bVDioVQf2ccIMYLuR4E8ev+RsDBlmwRkhGO3HhgF4PF9vpwjdPpxyb1zzLur2nQ2oIMAMEg== vuex@^3.0.1: version "3.0.1"