diff --git a/package.json b/package.json index fb0c9ab44..b313771c6 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ }, "dependencies": { "@animxyz/vue3": "0.6.7", - "@arch-inc/fabricjs-psbrush": "0.0.20", "@fullcalendar/core": "6.1.19", "@fullcalendar/daygrid": "6.1.19", "@fullcalendar/multimonth": "6.1.19", @@ -39,6 +38,7 @@ "core-js": "3.47.0", "exceljs": "4.4.0", "fabric": "cgwire/fabric.js", + "fabricjs-psbrush": "cgwire/fabricjs-psbrush", "file-saver": "2.0.5", "lucide-vue-next": "0.562.0", "marked": "17.0.1", diff --git a/src/components/mixins/annotation.js b/src/components/mixins/annotation.js index a9b1442de..d6e16328c 100644 --- a/src/components/mixins/annotation.js +++ b/src/components/mixins/annotation.js @@ -4,7 +4,7 @@ */ import { fabric } from 'fabric' import moment from 'moment' -import { PSStroke, PSBrush } from '@arch-inc/fabricjs-psbrush' +import { PSStroke, PSBrush } from 'fabricjs-psbrush' import { v4 as uuidv4 } from 'uuid' import { markRaw } from 'vue' diff --git a/src/components/modals/NotifyClientModal.vue b/src/components/modals/NotifyClientModal.vue index f12b32218..2b3f4c3a9 100644 --- a/src/components/modals/NotifyClientModal.vue +++ b/src/components/modals/NotifyClientModal.vue @@ -16,6 +16,14 @@ v-model="form.studio_id" /> + +

{{ $t('playlists.clients_to_notify') }}

@@ -59,6 +67,7 @@ import { mapGetters } from 'vuex' import { modalMixin } from '@/components/modals/base_modal' import BaseModal from '@/components/modals/BaseModal.vue' +import ComboboxDepartment from '@/components/widgets/ComboboxDepartment.vue' import ComboboxStudio from '@/components/widgets/ComboboxStudio.vue' import ModalFooter from '@/components/modals/ModalFooter.vue' import PeopleAvatar from '../widgets/PeopleAvatar.vue' @@ -71,6 +80,7 @@ export default { components: { BaseModal, + ComboboxDepartment, ComboboxStudio, ModalFooter, PeopleAvatar, @@ -105,7 +115,8 @@ export default { data() { return { form: { - studio_id: '' + studio_id: '', + department_id: '' } } }, @@ -121,13 +132,20 @@ export default { person => !this.form.studio_id || person.studio_id === this.form.studio_id ) + .filter( + person => + !this.form.department_id || + (person.departments && + person.departments.includes(this.form.department_id)) + ) } }, methods: { runConfirmation() { this.$emit('confirm', { - studio_id: this.form.studio_id + studio_id: this.form.studio_id, + department_id: this.form.department_id }) } } diff --git a/src/components/pages/Plugin.vue b/src/components/pages/Plugin.vue new file mode 100644 index 000000000..8d901fbfd --- /dev/null +++ b/src/components/pages/Plugin.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/components/pages/playlists/PlaylistPlayer.vue b/src/components/pages/playlists/PlaylistPlayer.vue index d2d91485f..4f6247521 100644 --- a/src/components/pages/playlists/PlaylistPlayer.vue +++ b/src/components/pages/playlists/PlaylistPlayer.vue @@ -1482,14 +1482,15 @@ export default { this.errors.notifyClients = false }, - async confirmNotifyClients({ studioId }) { + async confirmNotifyClients({ studioId, departmentId }) { this.loading.notifyClients = true this.errors.notifyClients = false this.success.notifyClients = false try { await this.notifyClients({ playlist: this.playlist, - studioId + studioId, + departmentId }) this.success.notifyClients = true } catch (err) { diff --git a/src/components/previews/PreviewPlayer.vue b/src/components/previews/PreviewPlayer.vue index a77c84a27..a4817d0a7 100644 --- a/src/components/previews/PreviewPlayer.vue +++ b/src/components/previews/PreviewPlayer.vue @@ -560,7 +560,7 @@ diff --git a/src/lib/path.js b/src/lib/path.js index c37fff78b..b494d1841 100644 --- a/src/lib/path.js +++ b/src/lib/path.js @@ -129,56 +129,84 @@ export const getEntityPath = ( return route } -const getProductionRoute = (name, productionId) => { +const SECTION_NAME_MAP = { + assetTypes: 'production-asset-types', + newsFeed: 'news-feed', + plugin: 'production-plugin' +} + +const TVSHOW_NON_EPISODIC_SECTIONS = [ + 'news-feed', + 'schedule', + 'production-settings', + 'quota', + 'budget', + 'team', + 'episodes', + 'episode-stats', + 'concepts', + 'brief' +] + +const SECTIONS_WITH_SEARCH = [ + 'assets', + 'shots', + 'edits', + 'sequences', + 'episodes', + 'breakdown' +] + +/* Enforce section depending on the production type */ +const _getAdjustedRouteName = (productionType, section, pluginId = null) => { + if (pluginId) { + section = 'production-plugin' + } + if (productionType === 'shots' && section === 'assets') { + return 'shots' + } + if (productionType === 'assets' && ['shots', 'sequences'].includes(section)) { + return 'assets' + } + return section +} + +const getProductionRoute = (name, productionId, pluginId = null) => { + const params = { + production_id: productionId + } + if (pluginId) { + params.plugin_id = pluginId + } return { name, - params: { - production_id: productionId - } + params } } export const getProductionPath = ( production, section = production.homepage || 'assets', - episodeId + episodeId, + pluginId ) => { - if (section === 'assetTypes') section = 'production-asset-types' - if (section === 'newsFeed') section = 'news-feed' - let route = getProductionRoute(section, production.id) + const normalizedSection = SECTION_NAME_MAP[section] || section + const routeName = _getAdjustedRouteName( + production.production_type, + normalizedSection, + pluginId + ) + let route = getProductionRoute(routeName, production.id, pluginId) - if (production.production_type === 'shots' && route.name === 'assets') { - route.name = 'shots' - } else if ( - production.production_type === 'assets' && - ['shots', 'sequences'].includes(route.name) - ) { - route.name = 'assets' - } - - if ( + const shouldEpisodify = production.production_type === 'tvshow' && - ![ - 'news-feed', - 'schedule', - 'production-settings', - 'quota', - 'budget', - 'team', - 'episodes', - 'episode-stats', - 'concepts', - 'brief' - ].includes(section) - ) { + !TVSHOW_NON_EPISODIC_SECTIONS.includes(normalizedSection) + + if (shouldEpisodify) { route = episodifyRoute(route, episodeId || 'all') } - if ( - ['assets', 'shots', 'edits', 'sequences', 'episodes', 'breakdown'].includes( - section - ) - ) { + if (SECTIONS_WITH_SEARCH.includes(normalizedSection)) { route.query = { search: '' } } diff --git a/src/lib/string.js b/src/lib/string.js index d90384e26..5cfcdb7b3 100644 --- a/src/lib/string.js +++ b/src/lib/string.js @@ -46,6 +46,10 @@ export default { return str.charAt(0).toUpperCase() + str.slice(1) }, + snakeToCamel(str) { + return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) + }, + filenameWithoutExtension(filename) { return filename.replace(/\.[^/.]+$/, '') }, diff --git a/src/locales/en.js b/src/locales/en.js index fab34dbc9..dd954882d 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -352,7 +352,7 @@ export default { }, departments: { - all_asset_types: 'All departments', + all_departments: 'All departments', available_items: 'Available Items', create_error: 'An error occurred while saving this department. Are you sure there is no department with a similar name?', delete_text: 'Are you sure you want to remove {name} from your database?', diff --git a/src/router/routes.js b/src/router/routes.js index 97b7831f8..d17d41c01 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -51,6 +51,7 @@ const NotFound = () => import('@/components/pages/NotFound.vue') const People = () => import('@/components/pages/People.vue') const Person = () => import('@/components/pages/Person.vue') const Playlist = () => import('@/components/pages/Playlist.vue') +const Plugin = () => import('@/components/pages/Plugin.vue') const ProductionAssetTypes = () => import('@/components/pages/ProductionAssetTypes.vue') const ProductionQuota = () => import('@/components/pages/ProductionQuota.vue') @@ -396,6 +397,12 @@ export const routes = [ ] }, + { + path: '/plugins/:plugin_id', + component: Plugin, + name: 'plugin' + }, + { path: 'settings', component: Settings, @@ -490,6 +497,18 @@ export const routes = [ name: 'brief' }, + { + path: 'productions/:production_id/plugins/:plugin_id', + component: Plugin, + name: 'production-plugin' + }, + + { + path: 'productions/:production_id/episodes/:episode_id/plugins/:plugin_id', + component: Plugin, + name: 'episode-production-plugin' + }, + { path: 'productions/:production_id/quota', component: ProductionQuota, diff --git a/src/store/api/playlists.js b/src/store/api/playlists.js index a11e4bfc7..45db54ad4 100644 --- a/src/store/api/playlists.js +++ b/src/store/api/playlists.js @@ -94,8 +94,8 @@ export default { return client.ppost(path, { task_ids: taskIds }) }, - notifyClients(playlist, studioId) { - const data = { studio_id: studioId } + notifyClients(playlist, studioId, departmentId) { + const data = { studio_id: studioId, department_id: departmentId } return client.ppost( `/api/data/playlists/${playlist.id}/notify-clients`, data diff --git a/src/store/modules/playlists.js b/src/store/modules/playlists.js index 746c580df..8fc5b5a7a 100644 --- a/src/store/modules/playlists.js +++ b/src/store/modules/playlists.js @@ -249,8 +249,8 @@ const actions = { return playlistsApi.updatePreviewFileValidationStatus(previewFile, status) }, - notifyClients({ commit }, { playlist, studioId }) { - return playlistsApi.notifyClients(playlist, studioId) + notifyClients({ commit }, { playlist, studioId, departmentId }) { + return playlistsApi.notifyClients(playlist, studioId, departmentId) } } diff --git a/src/store/modules/user.js b/src/store/modules/user.js index 10e390ec7..2922ebfa6 100644 --- a/src/store/modules/user.js +++ b/src/store/modules/user.js @@ -71,6 +71,7 @@ import { LOAD_CUSTOM_ACTIONS_END, LOAD_STATUS_AUTOMATIONS_END, LOAD_ASSET_TYPES_END, + LOAD_PLUGINS_END, SET_NOTIFICATION_COUNT, LOAD_OPEN_PRODUCTIONS_END, RESET_ALL, @@ -119,7 +120,9 @@ const initialState = { todoListScrollPosition: 0, timeSpentMap: {}, - timeSpentTotal: 0 + timeSpentTotal: 0, + + plugins: [] } const state = { @@ -158,7 +161,13 @@ const getters = { todoListScrollPosition: state => state.todoListScrollPosition, timeSpentMap: state => state.timeSpentMap, - timeSpentTotal: state => state.timeSpentTotal + timeSpentTotal: state => state.timeSpentTotal, + + plugins: state => state.plugins, + studioPlugins: state => + state.plugins.filter(plugin => plugin.frontend_studio_enabled), + projectPlugins: state => + state.plugins.filter(plugin => plugin.frontend_project_enabled) } const actions = { @@ -389,6 +398,7 @@ const actions = { commit(SET_CURRENT_PRODUCTION, rootGetters.currentProduction.id) } commit(LOAD_TASK_TYPES_END, context.task_types) + commit(LOAD_PLUGINS_END, context.plugins) }) } } @@ -821,6 +831,10 @@ const mutations = { } }, + [LOAD_PLUGINS_END](state, plugins) { + state.plugins = plugins + }, + [CLEAR_AVATAR](state, userId) { if (state.user.id === userId) { state.user.has_avatar = false @@ -836,7 +850,6 @@ const mutations = { } export default { - namespace: true, state, getters, actions, diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js index ec80e1434..cba357a51 100644 --- a/src/store/mutation-types.js +++ b/src/store/mutation-types.js @@ -291,6 +291,10 @@ export const LOAD_STUDIOS_END = 'LOAD_STUDIOS_END' export const EDIT_STUDIOS_END = 'EDIT_STUDIOS_END' export const DELETE_STUDIOS_END = 'DELETE_STUDIOS_END' +// Plugins + +export const LOAD_PLUGINS_END = 'LOAD_PLUGINS_END' + // Assets export const CLEAR_ASSETS = 'CLEAR_ASSETS'