diff --git a/lib/www/client/source/src/store/index.js b/lib/www/client/source/src/store/index.js index a01cf15..5b6b7f2 100644 --- a/lib/www/client/source/src/store/index.js +++ b/lib/www/client/source/src/store/index.js @@ -7,6 +7,7 @@ import snack from './modules/snack' import project from './modules/project' import event from './modules/event' import label from './modules/label' +import plan from './modules/plan' import line from './modules/line' import notify from './modules/notify' @@ -20,6 +21,7 @@ export default new Vuex.Store({ project, event, label, + plan, line, notify } diff --git a/lib/www/client/source/src/store/modules/plan/actions.js b/lib/www/client/source/src/store/modules/plan/actions.js new file mode 100644 index 0000000..f0e2087 --- /dev/null +++ b/lib/www/client/source/src/store/modules/plan/actions.js @@ -0,0 +1,114 @@ + +/** Fetch sequences from server + */ +async function refreshPlan ({commit, dispatch, state, rootState}) { + + if (state.loading) { + commit('abortPlanLoading'); + } + + commit('setPlanLoading'); + const pid = rootState.project.projectId; + const url = `/project/${pid}/plan`; + const init = { + signal: state.loading.signal + }; + const res = await dispatch('api', [url, init]); + + if (res) { + commit('setPlan', res); + commit('setPlanTimestamp'); + } + commit('clearPlanLoading'); +} + +/** Return a subset of sequences from state.sequences + */ +async function getPlannedSequences ({commit, dispatch, state}, [projectId, {sequence, date0, date1, sortBy, sortDesc, itemsPerPage, page, text}]) { + let filteredPlannedSequences = [...state.sequences]; + + if (sortBy) { + + sortBy.forEach( (key, idx) => { + filteredPlannedSequences.sort( (el0, el1) => { + const a = el0?.[key]; + const b = el1?.[key]; + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else if (a == b) { + return 0; + } else if (a && !b) { + return 1; + } else if (!a && b) { + return -1; + } else { + return 0; + } + }); + if (sortDesc && sortDesc[idx] === true) { + filteredPlannedSequences.reverse(); + } + }); + + } + + if (sequence) { + filteredPlannedSequences = filteredPlannedSequences.filter( sequence => sequence.sequence == sequence ); + } + + if (date0 && date1) { + filteredPlannedSequences = filteredPlannedSequences.filter( sequence => + sequence.ts0.substr(0, 10) >= date0 && sequence.ts1.substr(0, 10) <= date1 + ); + } else if (date0) { + filteredPlannedSequences = filteredPlannedSequences.filter( sequence => sequence.ts0.substr(0, 10) == date0 || sequence.ts1.substr(0, 10) ); + } + + if (text) { + const tstampFilter = (value, search, item) => { + return textFilter(value.toISOString(), search, item); + }; + + const numberFilter = (value, search, item) => { + return value == search; + }; + + const textFilter = (value, search, item) => { + return String(value).toLowerCase().includes(search.toLowerCase()); + }; + + const searchFunctions = { + sequence: numberFilter, + line: numberFilter, + remarks: textFilter, + ts0: tstampFilter, + ts1: tstampFilter + }; + + filteredPlannedSequences = filteredPlannedSequences.filter ( sequence => { + for (let key in searchFunctions) { + const fn = searchFunctions[key]; + if (fn(sequence[key], text, sequence)) { + return true; + } + } + return false; + }); + } + + const count = filteredPlannedSequences.length; + + if (itemsPerPage && itemsPerPage > 0) { + const offset = (page > 0) + ? (page-1) * itemsPerPage + : 0; + + filteredPlannedSequences = filteredPlannedSequences.slice(offset, offset+itemsPerPage); + } + + return {sequences: filteredPlannedSequences, count}; +} + +export default { refreshPlan, getPlannedSequences }; diff --git a/lib/www/client/source/src/store/modules/plan/getters.js b/lib/www/client/source/src/store/modules/plan/getters.js new file mode 100644 index 0000000..65c9dd3 --- /dev/null +++ b/lib/www/client/source/src/store/modules/plan/getters.js @@ -0,0 +1,18 @@ + +function planRemarks (state) { + return state.remarks; +} + +function plannedSequences (state) { + return state.sequences; +} + +function plannedSequenceCount (state) { + return state.sequences?.length ?? 0; +} + +function plannedSequencesLoading (state) { + return !!state.loading; +} + +export default { planRemarks, plannedSequences, plannedSequenceCount, plannedSequencesLoading }; diff --git a/lib/www/client/source/src/store/modules/plan/index.js b/lib/www/client/source/src/store/modules/plan/index.js new file mode 100644 index 0000000..dae701e --- /dev/null +++ b/lib/www/client/source/src/store/modules/plan/index.js @@ -0,0 +1,6 @@ +import state from './state' +import getters from './getters' +import actions from './actions' +import mutations from './mutations' + +export default { state, getters, actions, mutations }; diff --git a/lib/www/client/source/src/store/modules/plan/mutations.js b/lib/www/client/source/src/store/modules/plan/mutations.js new file mode 100644 index 0000000..96bf205 --- /dev/null +++ b/lib/www/client/source/src/store/modules/plan/mutations.js @@ -0,0 +1,59 @@ + + +function transform (item) { + item.ts0 = new Date(item.ts0); + item.ts1 = new Date(item.ts1); + return item; +} + +// ATTENTION: This relies on the new planner endpoint +// as per issue #281. + +function setPlan (state, plan) { + // We don't need or want the planned sequences array to be reactive + state.sequences = Object.freeze(plan.sequences.map(transform)); + state.remarks = plan.remarks; +} + +function setPlanLoading (state, abortController = new AbortController()) { + state.loading = abortController; +} + +// This assumes that we know any transactions have finished or we +// don't care about aborting. +function clearPlanLoading (state) { + state.loading = null; +} + +function setPlanTimestamp (state, timestamp = new Date()) { + // NOTE: There is no `modified_on` property in the plan + // result or in the database schema, but we should probably add + // one. + if (timestamp === true) { + const tstamp = state.plan + .map( item => item.modified_on ) + .reduce( (acc, cur) => acc > cur ? acc : cur ); + state.timestamp = tstamp ? new Date(tstamp) : new Date(); + } else { + state.timestamp = timestamp; + } +} + +function setPlanETag (state, etag) { + state.etag = etag; +} + +function abortPlanLoading (state) { + if (state.loading) { + state.loading.abort(); + } + state.loading = null; +} + +export default { + setPlan, + setPlanLoading, + clearPlanLoading, + setPlanTimestamp, + setPlanETag +}; diff --git a/lib/www/client/source/src/store/modules/plan/state.js b/lib/www/client/source/src/store/modules/plan/state.js new file mode 100644 index 0000000..998bbb0 --- /dev/null +++ b/lib/www/client/source/src/store/modules/plan/state.js @@ -0,0 +1,9 @@ +const state = () => ({ + sequences: Object.freeze([]), + remarks: null, + loading: null, + timestamp: null, + etag: null, +}); + +export default state;