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
88 changes: 53 additions & 35 deletions src/components/pages/ProductionSchedule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,11 @@ const modals = ref({
const pendingParentChange = ref(null)
const scheduleRef = useTemplateRef('schedule')

// Bumped when the production or the episode changes and when the page is
// left: an expansion still running then stops, rather than loading the
// stores of the new scope for the rows of the one left.
let scheduleGeneration = 0

// Computed
// --------------------------------------------------------------------------
const currentEpisode = computed(() => store.getters.currentEpisode)
Expand Down Expand Up @@ -1032,7 +1037,12 @@ const loadData = async () => {
}
}

const isStaleSchedule = generation => generation !== scheduleGeneration

const reset = () => {
scheduleGeneration++
// the rows of the new production or episode load collapsed
expandAll.value = false
// Debounce: cross-prod navigation triggers two close currentEpisode changes (transient 'main' then 'all').
if (resetTimeout.value) clearTimeout(resetTimeout.value)
resetTimeout.value = setTimeout(() => {
Expand Down Expand Up @@ -1161,10 +1171,29 @@ const loadScopedAssets = () => {
}

// Whether an asset falls in the current scope: for the main pack, only the
// assets attached to no episode (source_id null); otherwise the assetMap is
// already scoped and every asset it holds belongs.
// assets attached to no episode; otherwise the assetMap is already scoped
// and every asset it holds belongs. The store may hand over the list of the
// Assets page instead of a list without tasks: that list names the episode
// episode_id rather than source_id, and holds the assets other productions
// share with this one, which never belong.
const assetInScope = asset => {
return !isMainPack.value || !asset.source_id
return (
!asset.shared &&
(!isMainPack.value || !(asset.episode_id || asset.source_id))
)
}

// Loads the entities, scoped to the current episode for TV shows, that the
// rows of a task type stand for.
const loadTaskTypeEntities = forEntity => {
const loaders = {
Asset: loadScopedAssets,
Shot: () => store.dispatch('loadShots'),
Sequence: () => store.dispatch('loadSequences'),
Episode: () => store.dispatch('loadEpisodes'),
Edit: () => store.dispatch('loadEdits')
}
return loaders[forEntity]?.()
}

const expandTaskTypeElement = (
Expand Down Expand Up @@ -1228,6 +1257,7 @@ const expandTaskTypeDrillDown = async (
taskTypeElement.expanded = expanded || !taskTypeElement.expanded

if (taskTypeElement.expanded) {
const generation = scheduleGeneration
// unversioned task list shared with the side panel to avoid a reload
let rawTasks = null
try {
Expand Down Expand Up @@ -1259,6 +1289,7 @@ const expandTaskTypeDrillDown = async (
episodeId: currentEpisodeId.value
}
const entityRows = await loadEntityRows(parameters)
if (isStaleSchedule(generation)) return

let children = convertScheduleItems(taskTypeElement, entityRows)
const childrenById = new Map(
Expand All @@ -1267,22 +1298,14 @@ const expandTaskTypeDrillDown = async (

// load entities (scoped to the current episode for TV shows) that
// back the row grouping, the entity name and the episode filter
if (taskTypeElement.for_entity === 'Asset') {
await loadScopedAssets()
} else if (taskTypeElement.for_entity === 'Shot') {
await store.dispatch('loadShots')
} else if (taskTypeElement.for_entity === 'Sequence') {
await store.dispatch('loadSequences')
} else if (taskTypeElement.for_entity === 'Episode') {
await store.dispatch('loadEpisodes')
} else if (taskTypeElement.for_entity === 'Edit') {
await store.dispatch('loadEdits')
}
await loadTaskTypeEntities(taskTypeElement.for_entity)
if (isStaleSchedule(generation)) return

let tasks = await store.dispatch(
'loadTasks',
buildTaskFilters(taskTypeElement)
)
if (isStaleSchedule(generation)) return
rawTasks = tasks

// Update tasks for versioned schedules
Expand Down Expand Up @@ -1333,6 +1356,7 @@ const expandTaskTypeDrillDown = async (
)
daysOffRangeKey.value = daysOffKey
}
if (isStaleSchedule(generation)) return

// Read the entity maps fresh from the store cache. They are plain,
// non-reactive Maps replaced on each episode-scoped load, so a cached
Expand Down Expand Up @@ -1804,6 +1828,7 @@ const selectTaskTypeElement = async (
return
}

const generation = scheduleGeneration
selectedTaskType.value = taskType

if (resetAssignments) {
Expand All @@ -1820,14 +1845,15 @@ const selectTaskTypeElement = async (
'loadTasks',
buildTaskFilters(selectedTaskType.value)
))
if (isStaleSchedule(generation)) return
if (!preloadedTasks) {
await loadTaskTypeEntities(taskType.for_entity)
if (isStaleSchedule(generation)) return
}
const taskEntityIds = new Set(tasks.map(task => task.entity_id))

// load entity types
if (taskType.for_entity === 'Asset') {
if (!preloadedTasks) {
await loadScopedAssets()
}

assignments.value.entityTypes = productionAssetTypes.value
.filter(assetType => {
// filtering following custom asset types workflow
Expand Down Expand Up @@ -1861,10 +1887,6 @@ const selectTaskTypeElement = async (
}
})
} else if (taskType.for_entity === 'Shot') {
if (!preloadedTasks) {
await store.dispatch('loadShots')
}

const shotsBySequence = shotStore.cache.shots
.filter(shot => taskEntityIds.has(shot.id))
.reduce((acc, shot) => {
Expand All @@ -1891,10 +1913,6 @@ const selectTaskTypeElement = async (
}
)
} else if (taskType.for_entity === 'Sequence') {
if (!preloadedTasks) {
await store.dispatch('loadSequences')
}

// sequences are the assignable entities, grouped by episode
const sequencesByEpisode = [...sequenceStore.cache.sequenceMap.values()]
.filter(
Expand Down Expand Up @@ -1929,10 +1947,6 @@ const selectTaskTypeElement = async (
}
)
} else if (taskType.for_entity === 'Episode') {
if (!preloadedTasks) {
await store.dispatch('loadEpisodes')
}

// episodes are the assignable entities, under a single production group
const episodes = [...episodeStore.cache.episodeMap.values()]
.filter(
Expand All @@ -1957,10 +1971,6 @@ const selectTaskTypeElement = async (
}
]
} else if (taskType.for_entity === 'Edit') {
if (!preloadedTasks) {
await store.dispatch('loadEdits')
}

// edits are the assignable entities, grouped by episode
const editsByEpisode = [...editStore.cache.editMap.values()]
.filter(
Expand Down Expand Up @@ -2374,13 +2384,16 @@ const saveTask = async () => {
const onScheduleExpandAll = async () => {
if (loading.value.expandSchedule) return

const generation = scheduleGeneration
loading.value.expandSchedule = true
if (!expandAll.value) {
await expandAllScheduleItems()
} else {
collapseAllScheduleItems()
}
expandAll.value = !expandAll.value
// Stopped by a production or episode change: the rows shown now are
// collapsed.
if (!isStaleSchedule(generation)) expandAll.value = !expandAll.value
loading.value.expandSchedule = false
}

Expand Down Expand Up @@ -2573,11 +2586,13 @@ const applyToProduction = async () => {
}

const expandAllScheduleItems = async () => {
const generation = scheduleGeneration
// scopedScheduleItems keeps the main pack to its in-scope Asset rows:
// drilling an out-of-scope row would forward episode_id=main to an
// endpoint that rejects it. Same array reference in every other mode.
// run sequentially to avoid overloading the server
for (const element of scopedScheduleItems.value) {
if (isStaleSchedule(generation)) return
if (!element.expanded) {
await expandTaskTypeElement(
element,
Expand All @@ -2598,12 +2613,14 @@ const collapseAllScheduleItems = () => {
}

const exportSchedule = async (withAllRows = true) => {
const generation = scheduleGeneration
loading.value.exportSchedule = true

try {
if (withAllRows) {
await expandAllScheduleItems()
}
if (isStaleSchedule(generation)) return

const data = scheduleRef.value?.exportData()

Expand Down Expand Up @@ -2889,6 +2906,7 @@ onMounted(() => {
})

onBeforeUnmount(() => {
scheduleGeneration++
if (resetTimeout.value) clearTimeout(resetTimeout.value)
})

Expand Down
8 changes: 7 additions & 1 deletion src/store/modules/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,14 @@ const actions = {
const marker = isPartial ? '#partial' : all ? '#shared' : ''
const scope = all ? 'all' : (episode?.id ?? '')
const loadingKey = `${production.id}/${scope}${marker}`
// The full list of the same scope, loaded or on its way, holds every
// asset a partial load would fetch: a partial load takes it rather than
// replace the list a page displays with assets without their tasks.
const isFullListOfScope =
isPartial && state.assetsLoadingKey === `${production.id}/${scope}`

if (state.isAssetsLoading) {
if (state.assetsLoadingKey === loadingKey) {
if (state.assetsLoadingKey === loadingKey || isFullListOfScope) {
return cache.assetsLoadingPromise || cache.assets
}
// Another scope is in flight (the user switched episode mid-load): wait
Expand All @@ -472,6 +477,7 @@ const actions = {
() => dispatch('loadAssets', { all, withShared, withTasks })
)
}
if (isFullListOfScope) return cache.assets

if (all || episode?.id === 'all') {
episode = null // Do not filter by episode
Expand Down
28 changes: 14 additions & 14 deletions tests/unit.setup.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { config } from "@vue/test-utils"
import moment from 'moment'

moment.locale('en')
config.global.mocks = {
$t: tKey => tKey // just return translation key
}
// Node specs never mount: they skip loading Vue Test Utils.
if (typeof document !== 'undefined') {
const { config } = await import('@vue/test-utils')
config.global.mocks = {
$t: tKey => tKey // just return translation key
}

// v-focus only exists on the real app built by main.js, which the specs
// never load: mirror it here so mounted components resolve it instead of
// logging a warning.
config.global.directives = {
focus: {
mounted(el, binding) {
el.focus(binding.value)
// v-focus only exists on the real app built by main.js, which the specs
// never load: mirror it here so mounted components resolve it instead of
// logging a warning.
config.global.directives = {
focus: {
mounted(el, binding) {
el.focus(binding.value)
}
}
}
}
Expand Down
Loading
Loading