Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vueified channel list page #1264

Merged
merged 44 commits into from
Mar 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2711579
Got channel list to load and set active channels
jayoshih Feb 13, 2019
7399419
Added starring functions
jayoshih Feb 13, 2019
cde4d59
Added channel set rendering
jayoshih Feb 14, 2019
1f503a1
Added invitations
jayoshih Feb 14, 2019
21231e0
Added opening channel set modal
jayoshih Feb 14, 2019
8b155c3
Use rest framework reponse on channel accept
jayoshih Feb 14, 2019
b6db5f0
Started adding channel details
jayoshih Feb 16, 2019
5c1d933
Got details panel to work
jayoshih Feb 22, 2019
ac3ef06
Added channel detail exporting
jayoshih Feb 25, 2019
e0d4fcb
Clearned up store
jayoshih Feb 26, 2019
0669014
Removed unused functions
jayoshih Feb 26, 2019
c6770f2
Updated styling
jayoshih Feb 26, 2019
58f3b3e
Separated out modules
jayoshih Feb 26, 2019
612cad4
Separated out shared components
jayoshih Feb 26, 2019
c46351e
Removed legacy files
jayoshih Feb 26, 2019
b406374
Added tab mixin to unify how tabs are handled across components
jayoshih Feb 26, 2019
237ba7a
Merge branch 'develop' of https://github.com/learningequality/studio …
jayoshih Feb 26, 2019
cfb93c0
Fixed is_channel logic on details panel
jayoshih Feb 26, 2019
76a04aa
Removed commented out code, updated changelog
jayoshih Feb 26, 2019
18b03b3
Added tests for views
jayoshih Mar 2, 2019
2bca790
Finished tests
jayoshih Mar 4, 2019
9228d95
Fixed store test
jayoshih Mar 4, 2019
fb83a08
Added translations to vue
jayoshih Mar 5, 2019
2376b17
Added vue mock
jayoshih Mar 5, 2019
80a0e91
Fixed channel sort order
jayoshih Mar 5, 2019
bc0c671
Adding key for linting rules.
DXCanas Mar 12, 2019
87575c1
Modifying channel editor to be a form.
DXCanas Mar 13, 2019
16ee4fe
ChannelEditor css cleanup.
DXCanas Mar 13, 2019
c95a38f
Having styles match Channel Details a little better.
DXCanas Mar 13, 2019
89cfe84
Adding webpack aliases to eslint rules.
DXCanas Mar 14, 2019
0167a12
Merge pull request #33 from DXCanas/vue-channels-list
Mar 15, 2019
beb946d
Merge branch 'develop' into vue-channel-list
Mar 19, 2019
9b87f3d
Merge branch 'develop' into vue-channel-list
Mar 19, 2019
7b6d78f
Merge branch 'develop' of https://github.com/learningequality/studio …
jayoshih Mar 19, 2019
9a41506
Merge branch 'vue-channel-list' of https://github.com/jayoshih/conten…
jayoshih Mar 19, 2019
d997097
Made smaller fixes
jayoshih Mar 19, 2019
e2c26ec
Added prop validator
jayoshih Mar 19, 2019
7a4b7c5
Updated test
jayoshih Mar 19, 2019
571652e
Fixed test
jayoshih Mar 19, 2019
44811d9
Moved preferences to shared vue state
jayoshih Mar 19, 2019
397c6ab
Switched to object spread
jayoshih Mar 19, 2019
b55654f
Removed backbone wrapper
jayoshih Mar 19, 2019
79aa6f4
Pass ids instead of objects into components and actions
jayoshih Mar 20, 2019
04fea11
Fixed tests
jayoshih Mar 21, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions __mocks__/vue.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

This pattern has cropped up on Kolibri too - seems like it is probably worth my while thinking about how to make $trs work for mixins.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would be great!

}
},
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();
});
}
};
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Loading