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 @@
+
+
+
Plugin
+ {{ plugin?.name }}
+
+
+
+
+
+
+
+
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'