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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
props: {
nodeIds: {
type: Array,
default: () => [],
default: null,
},
},
data() {
return {
hasChanges: false,
hasNodeChanges: false,
isSaving: false,
lastSaved: null,
lastSavedText: '',
Expand All @@ -43,6 +43,13 @@
computed: {
...mapGetters('file', ['getContentNodeFiles']),
...mapGetters('assessmentItem', ['getAssessmentItems']),
...mapGetters(['areAllChangesSaved']),
hasChanges() {
if (this.nodeIds) {
return this.hasNodeChanges;
}
return !this.areAllChangesSaved;
},
},
watch: {
hasChanges(hasChanges) {
Expand All @@ -62,21 +69,27 @@
},
},
mounted() {
this.interval = setInterval(() => {
const files = flatten(this.nodeIds.map(this.getContentNodeFiles));
const assessmentItems = flatten(this.nodeIds.map(this.getAssessmentItems));
this.checkSavingProgress({
contentNodeIds: this.nodeIds,
fileIds: files.map(f => f.id).filter(Boolean),
assessmentIds: assessmentItems
.map(ai => [ai.contentnode, ai.assessment_id])
.filter(Boolean),
}).then(hasChanges => (this.hasChanges = hasChanges));
}, CHECK_SAVE_INTERVAL);
if (this.nodeIds) {
this.interval = setInterval(() => {
const files = flatten(this.nodeIds.map(this.getContentNodeFiles));
const assessmentItems = flatten(this.nodeIds.map(this.getAssessmentItems));
this.checkSavingProgress({
contentNodeIds: this.nodeIds,
fileIds: files.map(f => f.id).filter(Boolean),
assessmentIds: assessmentItems
.map(ai => [ai.contentnode, ai.assessment_id])
.filter(Boolean),
}).then(hasChanges => (this.hasNodeChanges = hasChanges));
}, CHECK_SAVE_INTERVAL);
}
},
beforeDestroy() {
clearInterval(this.interval);
clearInterval(this.updateLastSavedInterval);
if (this.interval) {
clearInterval(this.interval);
}
if (this.updateLastSavedInterval) {
clearInterval(this.updateLastSavedInterval);
}
},
methods: {
...mapActions('contentNode', ['checkSavingProgress']),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
</router-link>
</VToolbarItems>
<VSpacer />
<SavingIndicator v-if="!offline" />
<OfflineText indicator />
<ProgressModal />
<div
Expand Down Expand Up @@ -305,11 +306,12 @@

<script>

import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import Clipboard from '../../components/Clipboard';
import SyncResourcesModal from '../sync/SyncResourcesModal';
import ProgressModal from '../progress/ProgressModal';
import PublishModal from '../../components/publish/PublishModal';
import SavingIndicator from '../../components/edit/SavingIndicator';
import { DraggableRegions, DraggableUniverses, RouteNames } from '../../constants';
import MainNavigationDrawer from 'shared/views/MainNavigationDrawer';
import IconButton from 'shared/views/IconButton';
Expand Down Expand Up @@ -340,6 +342,7 @@
ContentNodeIcon,
DraggablePlaceholder,
MessageDialog,
SavingIndicator,
},
mixins: [titleMixin],
props: {
Expand All @@ -360,6 +363,9 @@
};
},
computed: {
...mapState({
offline: state => !state.connection.online,
}),
...mapGetters('contentNode', ['getContentNode']),
...mapGetters('currentChannel', ['currentChannel', 'canEdit', 'canManage', 'rootId']),
rootNode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const USER_2 = { id: 1 };

describe('startApp', () => {
let store;
let cleanup;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -24,6 +25,10 @@ describe('startApp', () => {
afterEach(async () => {
global.user = undefined;
await Session.table.clear();
if (cleanup) {
cleanup();
}
cleanup = undefined;
});

describe('for a guest', () => {
Expand All @@ -38,7 +43,7 @@ describe('startApp', () => {
});

it("the client database shouldn't be reset", async () => {
await startApp({ router, store });
cleanup = await startApp({ router, store });
expect(resetDB).not.toHaveBeenCalled();
});
});
Expand All @@ -52,7 +57,7 @@ describe('startApp', () => {
});

it('the client database should be reset', async () => {
await startApp({ router, store });
cleanup = await startApp({ router, store });
expect(resetDB).toHaveBeenCalledTimes(1);
});
});
Expand All @@ -70,7 +75,7 @@ describe('startApp', () => {
});

it("the client database shouldn't be reset", async () => {
await startApp({ router, store });
cleanup = await startApp({ router, store });
expect(resetDB).not.toHaveBeenCalled();
});
});
Expand All @@ -84,7 +89,7 @@ describe('startApp', () => {
});

it("the client database shouldn't be reset", async () => {
await startApp({ router, store });
cleanup = await startApp({ router, store });
expect(resetDB).not.toHaveBeenCalled();
});
});
Expand All @@ -98,7 +103,7 @@ describe('startApp', () => {
});

it('the client database should be reset', async () => {
await startApp({ router, store });
cleanup = await startApp({ router, store });
expect(resetDB).toHaveBeenCalledTimes(1);
});
});
Expand Down
32 changes: 32 additions & 0 deletions contentcuration/contentcuration/frontend/shared/app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'regenerator-runtime/runtime';
import { liveQuery } from 'dexie';
import * as Sentry from '@sentry/vue';
import Vue from 'vue';
import VueRouter from 'vue-router';
Expand Down Expand Up @@ -115,6 +116,7 @@ import { i18nSetup } from 'shared/i18n';
import './styles/vuetify.css';
import 'shared/styles/main.less';
import Base from 'shared/Base.vue';
import urls from 'shared/urls';
import ActionLink from 'shared/views/ActionLink';
import Menu from 'shared/views/Menu';
import { initializeDB, resetDB } from 'shared/data';
Expand Down Expand Up @@ -315,8 +317,27 @@ export default async function startApp({ store, router, index }) {
) {
await resetDB();
}

let subscription;

if (currentUser.id !== undefined && currentUser.id !== null) {
// The user is logged on, so persist that to the session table in indexeddb
await store.dispatch('saveSession', currentUser, { root: true });
// Also watch in case the user logs out, then we should redirect to the login page
const observable = liveQuery(() => {
return Session.table.toCollection().first(Boolean);
});

subscription = observable.subscribe({
next(result) {
if (!result && !window.location.pathname.endsWith(urls.accounts())) {
window.location = urls.accounts();
}
},
error() {
subscription.unsubscribe();
},
});
}

await Session.setChannelScope();
Expand Down Expand Up @@ -350,5 +371,16 @@ export default async function startApp({ store, router, index }) {
// to the session state.
injectVuexStore(store);

// Start listening for unsynced change events in IndexedDB
store.listenForIndexedDBChanges();

rootVue = new Vue(config);

// Return a cleanup function
return function() {
if (subscription) {
subscription.unsubscribe();
}
store.stopListeningForIndexedDBChanges();
};
}
8 changes: 0 additions & 8 deletions contentcuration/contentcuration/frontend/shared/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,10 @@ if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
window.resetDB = resetDB;
}

function applyResourceListener(change) {
const resource = INDEXEDDB_RESOURCES[change.table];
if (resource && resource.listeners && resource.listeners[change.type]) {
resource.listeners[change.type](change);
}
}

export async function initializeDB() {
try {
setupSchema();
await db.open();
db.on('changes', changes => changes.map(applyResourceListener));
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopSyncing();
Expand Down
11 changes: 0 additions & 11 deletions contentcuration/contentcuration/frontend/shared/data/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ class IndexedDBResource {
uuid = true,
indexFields = [],
syncable = false,
listeners = {},
...options
} = {}) {
this.tableName = tableName;
Expand All @@ -228,9 +227,6 @@ class IndexedDBResource {
copyProperties(this, options);
// By default these resources do not sync changes to the backend.
this.syncable = syncable;
// An object for listening to specific change events on this resource in order to
// allow for side effects from changes - should be a map of change type to handler function.
this.listeners = listeners;
}

get table() {
Expand Down Expand Up @@ -977,13 +973,6 @@ export const Session = new IndexedDBResource({
tableName: TABLE_NAMES.SESSION,
idField: CURRENT_USER,
uuid: false,
listeners: {
[CHANGE_TYPES.DELETED]: function() {
if (!window.location.pathname.endsWith(urls.accounts())) {
window.location = urls.accounts();
}
},
},
get currentChannel() {
return window.CHANNEL_EDIT_GLOBAL || {};
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { liveQuery } from 'dexie';
import syncProgressModule from './syncProgressModule';
import db from 'shared/data/db';
import { CHANGES_TABLE } from 'shared/data/constants';

const SyncProgressPlugin = store => {
store.registerModule('syncProgress', syncProgressModule);

db.on('changes', function(changes) {
const changesTableUpdated = changes.some(change => change.table === CHANGES_TABLE);
if (!changesTableUpdated) {
return;
}
store.listenForIndexedDBChanges = () => {
const observable = liveQuery(() => {
return db[CHANGES_TABLE].toCollection()
.filter(c => !c.synced)
.first(Boolean);
});

db[CHANGES_TABLE].toCollection()
.filter(c => !c.synced)
.limit(1)
.count()
.then(count => store.commit('SET_UNSAVED_CHANGES', count > 0));
});
const subscription = observable.subscribe({
next(result) {
store.commit('SET_UNSAVED_CHANGES', result);
},
error() {
subscription.unsubscribe();
},
});
store.stopListeningForIndexedDBChanges = subscription.unsubscribe;
};
};

export default SyncProgressPlugin;