diff --git a/js/src/userviewmodel.js b/js/src/userviewmodel.js
new file mode 100644
index 0000000..6a7f1c2
--- /dev/null
+++ b/js/src/userviewmodel.js
@@ -0,0 +1,79 @@
+/*
+ * μlogger
+ *
+ * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
+ *
+ * This is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ */
+
+import { auth, lang } from './initializer.js';
+import ViewModel from './viewmodel.js';
+import uSelect from './select.js';
+import uUser from './user.js';
+import uUtils from './utils.js';
+
+/**
+ * @class UserViewModel
+ */
+export default class UserViewModel extends ViewModel {
+
+ /**
+ * @param {uState} state
+ */
+ constructor(state) {
+ super({
+ /** @type {uUser[]} */
+ userList: [],
+ /** @type {string} */
+ currentUserId: '0'
+ });
+ /** @type HTMLSelectElement */
+ const listEl = document.querySelector('#user');
+ this.select = new uSelect(listEl, lang.strings['suser'], `- ${lang.strings['allusers']} -`);
+ this.state = state;
+ this.onChanged('userList', (list) => { this.select.setOptions(list); });
+ this.onChanged('currentUserId', (listValue) => {
+ this.state.showAllUsers = listValue === uSelect.allValue;
+ this.state.currentUser = this.model.userList.find((_user) => _user.listValue === listValue) || null;
+ });
+ state.onChanged('showLatest', (showLatest) => {
+ if (showLatest) {
+ this.select.showAllOption();
+ } else {
+ this.select.hideAllOption();
+ }
+ });
+ this.init();
+ }
+
+ init() {
+ this.bindAll();
+ uUser.fetchList()
+ .then((_users) => {
+ this.model.userList = _users;
+ if (_users.length) {
+ let userId = _users[0].listValue;
+ if (auth.isAuthenticated) {
+ const user = this.model.userList.find((_user) => _user.listValue === auth.user.listValue);
+ if (user) {
+ userId = user.listValue;
+ }
+ }
+ this.model.currentUserId = userId;
+ }
+ })
+ .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); });
+ }
+
+}
diff --git a/js/test/userviewmodel.test.js b/js/test/userviewmodel.test.js
new file mode 100644
index 0000000..f37db95
--- /dev/null
+++ b/js/test/userviewmodel.test.js
@@ -0,0 +1,213 @@
+/*
+ * μlogger
+ *
+ * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
+ *
+ * This is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ */
+
+import { auth, config, lang } from '../src/initializer.js';
+import UserViewModel from '../src/userviewmodel.js';
+import ViewModel from '../src/viewmodel.js';
+import uObserve from '../src/observe.js';
+import uSelect from '../src/select.js';
+import uState from '../src/state.js';
+import uUser from '../src/user.js';
+
+describe('UserViewModel tests', () => {
+
+ let state;
+ let user1;
+ let user2;
+ let users;
+ /** @type {HTMLSelectElement} */
+ let userEl;
+
+ beforeEach(() => {
+ const fixture = `
+
+
+
+
+
`;
+ document.body.insertAdjacentHTML('afterbegin', fixture);
+ userEl = document.querySelector('#user');
+ config.initialize();
+ lang.init(config);
+ lang.strings['suser'] = 'select user';
+ lang.strings['allusers'] = 'all users';
+ state = new uState();
+ user1 = new uUser(1, 'user1');
+ user2 = new uUser(2, 'user2');
+ users = [ user1, user2 ];
+ });
+
+ afterEach(() => {
+ document.body.removeChild(document.querySelector('#fixture'));
+ auth.user = null;
+ });
+
+ it('should create instance with state as parameter and load user list and select first user on list', (done) => {
+ // given
+ spyOn(uUser, 'fetchList').and.returnValue(Promise.resolve(users));
+ // when
+ const vm = new UserViewModel(state);
+ // then
+ setTimeout(() => {
+ expect(vm).toBeInstanceOf(ViewModel);
+ expect(vm.select.element).toBeInstanceOf(HTMLSelectElement);
+ expect(vm.state).toBe(state);
+ expect(vm.model.userList.length).toBe(users.length);
+ expect(userEl.value).toBe(user1.listValue);
+ expect(userEl.options.length).toBe(users.length + 1);
+ expect(userEl.options[1].selected).toBe(true);
+ expect(userEl.options[1].value).toBe(user1.listValue);
+ done();
+ }, 100);
+ });
+
+ it('should create instance with state as parameter and load user list and select authorized user on list', (done) => {
+ // given
+ spyOn(uUser, 'fetchList').and.returnValue(Promise.resolve(users));
+ // when
+ auth.user = user2;
+ const vm = new UserViewModel(state);
+ // then
+ setTimeout(() => {
+ expect(vm).toBeInstanceOf(ViewModel);
+ expect(vm.select.element).toBeInstanceOf(HTMLSelectElement);
+ expect(vm.state).toBe(state);
+ expect(vm.model.userList.length).toBe(users.length);
+ expect(userEl.value).toBe(user2.listValue);
+ expect(userEl.options.length).toBe(users.length + 1);
+ expect(userEl.options[2].selected).toBe(true);
+ expect(userEl.options[2].value).toBe(user2.listValue);
+ done();
+ }, 100);
+ });
+
+ it('should change current user on user list option selected', (done) => {
+ // given
+ spyOn(UserViewModel.prototype, 'init');
+ const vm = new UserViewModel(state);
+ uObserve.setSilently(state, 'currentUser', user1);
+ uObserve.setSilently(vm.model, 'userList', users);
+ uObserve.setSilently(vm.model, 'currentUserId', user1.listValue);
+ const options = '';
+ userEl.insertAdjacentHTML('beforeend', options);
+ const optLength = userEl.options.length;
+ vm.bindAll();
+ // when
+ userEl.value = user2.listValue;
+ userEl.dispatchEvent(new Event('change'));
+ // then
+ setTimeout(() => {
+ expect(state.currentUser).toBe(user2);
+ expect(userEl.options.length).toBe(optLength);
+ expect(userEl.value).toBe(user2.listValue);
+ expect(userEl.options[2].selected).toBe(true);
+ expect(userEl.options[2].value).toBe(user2.listValue);
+ done();
+ }, 100);
+ });
+
+ it('should set showAllUsers state on "all users" option selected', (done) => {
+ // given
+ spyOn(UserViewModel.prototype, 'init');
+ const vm = new UserViewModel(state);
+ uObserve.setSilently(state, 'currentUser', user1);
+ uObserve.setSilently(state, 'showAllUsers', false);
+ uObserve.setSilently(vm.model, 'userList', users);
+ uObserve.setSilently(vm.model, 'currentUserId', user1.listValue);
+ const options = ``;
+ userEl.insertAdjacentHTML('beforeend', options);
+ const optLength = userEl.options.length;
+ vm.bindAll();
+ // when
+ userEl.value = uSelect.allValue;
+ userEl.dispatchEvent(new Event('change'));
+ // then
+ setTimeout(() => {
+ expect(state.showAllUsers).toBe(true);
+ expect(state.currentUser).toBe(null);
+ expect(userEl.value).toBe(uSelect.allValue);
+ expect(userEl.options.length).toBe(optLength);
+ expect(userEl.options[1].selected).toBe(true);
+ expect(userEl.options[1].value).toBe(uSelect.allValue);
+ done();
+ }, 100);
+ });
+
+ it('should add "all users" option when "showLatest" state is set', (done) => {
+ // given
+ spyOn(UserViewModel.prototype, 'init');
+ const vm = new UserViewModel(state);
+ uObserve.setSilently(state, 'currentUser', user1);
+ uObserve.setSilently(state, 'showAllUsers', false);
+ uObserve.setSilently(vm.model, 'userList', users);
+ uObserve.setSilently(vm.model, 'currentUserId', user1.listValue);
+ const options = '';
+ userEl.insertAdjacentHTML('beforeend', options);
+ const optLength = userEl.options.length;
+ const listLength = vm.model.userList.length;
+ vm.bindAll();
+ // when
+ state.showLatest = true;
+ // then
+ setTimeout(() => {
+ expect(state.showAllUsers).toBe(false);
+ expect(state.currentUser).toBe(user1);
+ expect(vm.select.hasAllOption).toBe(true);
+ expect(userEl.value).toBe(user1.listValue);
+ expect(userEl.options.length).toBe(optLength + 1);
+ expect(vm.model.userList.length).toBe(listLength);
+ expect(userEl.options[1].selected).toBe(false);
+ expect(userEl.options[1].value).toBe(uSelect.allValue);
+ expect(userEl.options[2].selected).toBe(true);
+ expect(userEl.options[2].value).toBe(user1.listValue);
+ done();
+ }, 100);
+ });
+
+ it('should remove "all users" option when "showLatest" state is unset', (done) => {
+ // given
+ spyOn(UserViewModel.prototype, 'init');
+ const vm = new UserViewModel(state);
+ uObserve.setSilently(state, 'currentUser', user1);
+ uObserve.setSilently(state, 'showAllUsers', false);
+ uObserve.setSilently(state, 'showLatest', true);
+ uObserve.setSilently(vm.model, 'userList', users);
+ uObserve.setSilently(vm.model, 'currentUserId', user1.listValue);
+ const options = ``;
+ userEl.insertAdjacentHTML('beforeend', options);
+ const optLength = userEl.options.length;
+ const listLength = vm.model.userList.length;
+ vm.bindAll();
+ // when
+ state.showLatest = false;
+ // then
+ setTimeout(() => {
+ expect(state.showAllUsers).toBe(false);
+ expect(state.currentUser).toBe(user1);
+ expect(vm.select.hasAllOption).toBe(false);
+ expect(userEl.value).toBe(user1.listValue);
+ expect(userEl.options.length).toBe(optLength - 1);
+ expect(vm.model.userList.length).toBe(listLength);
+ expect(userEl.options[1].selected).toBe(true);
+ expect(userEl.options[1].value).toBe(user1.listValue);
+ done();
+ }, 100);
+ });
+
+});