From ab77aeb775b29220d2ec77bcf48b0d377b7ea25f Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Tue, 21 Nov 2023 10:45:37 +0100 Subject: [PATCH 01/41] [concepts] init new page --- src/components/pages/Concepts.vue | 515 ++++++++++++++++++++++++++++++ src/locales/en.js | 11 + 2 files changed, 526 insertions(+) create mode 100644 src/components/pages/Concepts.vue diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue new file mode 100644 index 0000000000..4f754e485e --- /dev/null +++ b/src/components/pages/Concepts.vue @@ -0,0 +1,515 @@ + + + + + diff --git a/src/locales/en.js b/src/locales/en.js index a2f87d67f9..34a0a5f01c 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -134,6 +134,16 @@ export default { } }, + concepts: { + title: 'Concepts', + fields: { + entity_type: 'Entity type', + created_at: 'Creation date', + updated_at: 'Update date', + // last_comment_date: 'Last comment', + } + }, + custom_actions: { create_error: 'An error occurred while saving this custom action. Are you sure that there is no other action with the same name?', delete_text: 'Are you sure you want to remove custom action {name} from your database?', @@ -1065,6 +1075,7 @@ export default { color: 'Color', is_artist_allowed: 'Is artist allowed', is_client_allowed: 'Is client allowed', + is_concept: 'Is concept', is_done: 'Is done', is_feedback_request: 'Is feedback request', is_retake: 'Has retake value', From c5e7d979daa367abbb7f1cee450f1a1182c78d19 Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Tue, 21 Nov 2023 10:40:37 +0100 Subject: [PATCH 02/41] [concepts] add new type of Task Status --- src/components/lists/TaskStatusList.vue | 7 ++++- src/components/modals/EditTaskStatusModal.vue | 7 +++++ src/store/modules/user.js | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/components/lists/TaskStatusList.vue b/src/components/lists/TaskStatusList.vue index 48b7082dc4..49008da30e 100644 --- a/src/components/lists/TaskStatusList.vue +++ b/src/components/lists/TaskStatusList.vue @@ -28,6 +28,9 @@ {{ $t('task_status.fields.is_feedback_request') }} + + {{ $t('task_status.fields.is_concept') }} + @@ -52,6 +55,7 @@ class="is-feedback-request" :value="entry.is_feedback_request" /> + + { + // FIXME: remove mock data + context.task_status.push( + { + id: 'concept-neutral', + name: 'Neutral', + archived: false, + short_name: 'neutral', + color: '#CCCCCC', + // is_default: true, + is_concept: true + }, + { + id: 'concept-approved', + name: 'Approved', + archived: false, + short_name: 'approved', + color: '#66BB6A', + is_concept: true + }, + { + id: 'concept-rejected', + name: 'Rejected', + archived: false, + short_name: 'rejected', + color: '#E81123', + is_concept: true + } + ) + commit(LOAD_USER_FILTERS_END, context.search_filters) commit(LOAD_USER_FILTER_GROUPS_END, context.search_filter_groups) commit(LOAD_PRODUCTION_STATUS_END, context.project_status) From 23d3e30f550eef01f748e12718ee9608f9dc47ed Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Tue, 21 Nov 2023 10:42:20 +0100 Subject: [PATCH 03/41] [concepts] add new section on Topbar menu --- src/components/tops/Topbar.vue | 8 ++++++++ src/lib/path.js | 3 ++- src/router/routes.js | 7 +++++++ src/testrouter/routes.js | 7 +++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/tops/Topbar.vue b/src/components/tops/Topbar.vue index 8e9cd7af91..1e4d1f3022 100644 --- a/src/components/tops/Topbar.vue +++ b/src/components/tops/Topbar.vue @@ -441,6 +441,14 @@ export default { options.push({ label: 'Episodes', value: 'episodes' }) } + options = options.concat([{ label: 'separator', value: 'separator' }]) + + if (!this.isCurrentUserClient) { + options = options.concat([ + { label: this.$t('concepts.title'), value: 'concepts' } + ]) + } + if (!this.isCurrentUserClient) { options = options.concat([ { label: this.$t('breakdown.title'), value: 'breakdown' } diff --git a/src/lib/path.js b/src/lib/path.js index 8a8f1fe600..3143c6dbe1 100644 --- a/src/lib/path.js +++ b/src/lib/path.js @@ -117,7 +117,8 @@ export const getProductionPath = ( 'quota', 'team', 'episodes', - 'episode-stats' + 'episode-stats', + 'concepts' ].includes(section) ) { route = episodifyRoute(route, episodeId || 'all') diff --git a/src/router/routes.js b/src/router/routes.js index 114c9f5779..f8085ffe61 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -25,6 +25,7 @@ const Asset = () => import('@/components/pages/Asset.vue') const AssetTypes = () => import('@/components/pages/AssetTypes.vue') const Backgrounds = () => import('@/components/pages/Backgrounds.vue') const Breakdown = () => import('@/components/pages/Breakdown.vue') +const Concepts = () => import('@/components/pages/Concepts.vue') const CustomActions = () => import('@/components/pages/CustomActions.vue') const Departments = () => import('@/components/pages/Departments.vue') const Edit = () => import('@/components/pages/Edit.vue') @@ -531,6 +532,12 @@ export const routes = [ ] }, + { + path: 'productions/:production_id/concepts', + component: Concepts, + name: 'concepts' + }, + { path: 'productions/:production_id/assets', component: Assets, diff --git a/src/testrouter/routes.js b/src/testrouter/routes.js index 3ad734d323..d0a2a1f3d1 100644 --- a/src/testrouter/routes.js +++ b/src/testrouter/routes.js @@ -25,6 +25,7 @@ import Asset from '@/components/pages/Asset' import AssetTypes from '@/components/pages/AssetTypes' import Backgrounds from '@/components/pages/Backgrounds' import Breakdown from '@/components/pages/Breakdown' +import Concepts from '@/components/pages/Concepts' import CustomActions from '@/components/pages/CustomActions' import Departments from '@/components/pages/departments/Departments' import Edit from '@/components/pages/Edit' @@ -591,6 +592,12 @@ export const routes = [ ] }, + { + path: 'productions/:production_id/concepts', + component: Concepts, + name: 'concepts' + }, + { path: 'productions/:production_id/assets', component: Assets, From 1a12912d9ca82eb3ab03a55c749f54ac75ad8124 Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Tue, 21 Nov 2023 22:48:34 +0100 Subject: [PATCH 04/41] [concepts] init data store --- src/store/api/concepts.js | 25 ++++++ src/store/index.js | 2 + src/store/modules/concepts.js | 143 ++++++++++++++++++++++++++++++++++ src/store/mutation-types.js | 8 ++ 4 files changed, 178 insertions(+) create mode 100644 src/store/api/concepts.js create mode 100644 src/store/modules/concepts.js diff --git a/src/store/api/concepts.js b/src/store/api/concepts.js new file mode 100644 index 0000000000..7f6bf4da22 --- /dev/null +++ b/src/store/api/concepts.js @@ -0,0 +1,25 @@ +import client from '@/store/api/client' + +export default { + getConcepts(production) { + return client.pget( + `/api/data/concepts/with-tasks?project_id=${production.id}` + ) + }, + + getConcept(conceptId) { + return client.getModel('concepts', conceptId, true) + }, + + newConcept(concept) { + return client.ppost('/api/data/concepts/', concept) + }, + + updateConcept(concept) { + return client.pput(`/api/data/concepts/${concept.id}`, concept) + }, + + deleteConcept(concept) { + return client.pdel(`/api/data/concepts/${concept.id}?force=true`) + } +} diff --git a/src/store/index.js b/src/store/index.js index 59dd6a3cf3..dbb6349c45 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -7,6 +7,7 @@ import assets from '@/store/modules/assets' import assetTypes from '@/store/modules/assettypes' import backgrounds from '@/store/modules/backgrounds' import breakdown from '@/store/modules/breakdown' +import concepts from '@/store/modules/concepts' import customActions from '@/store/modules/customactions' import departments from '@/store/modules/departments' import edits from '@/store/modules/edits' @@ -35,6 +36,7 @@ const modules = { assetTypes, backgrounds, breakdown, + concepts, customActions, departments, edits, diff --git a/src/store/modules/concepts.js b/src/store/modules/concepts.js new file mode 100644 index 0000000000..2d890e7560 --- /dev/null +++ b/src/store/modules/concepts.js @@ -0,0 +1,143 @@ +import conceptsApi from '@/store/api/concepts' +import tasksApi from '@/store/api/tasks' + +import { + LOAD_CONCEPTS_START, + LOAD_CONCEPTS_ERROR, + LOAD_CONCEPTS_END, + EDIT_CONCEPT_END, + DELETE_CONCEPT_END, + RESET_ALL +} from '@/store/mutation-types' + +const initialState = { + concepts: [], + conceptMap: new Map(), + displayedConcepts: [], + conceptSearchText: '', + conceptSearchQueries: [] +} + +const state = { + ...initialState +} + +const getters = { + concepts: state => state.concepts, + conceptMap: state => state.conceptMap + // editConcept: state => state.editConcept, + // deleteConcept: state => state.deleteConcept +} + +const actions = { + async loadConcepts({ commit }) { + commit(LOAD_CONCEPTS_START) + try { + const concepts = await conceptsApi.getConcepts() + commit(LOAD_CONCEPTS_END, concepts) + } catch (err) { + commit(LOAD_CONCEPTS_ERROR) + } + }, + + async loadConcept({ commit }, conceptId) { + try { + const concept = await conceptsApi.getConcept(conceptId) + commit(EDIT_CONCEPT_END, concept) + } catch (err) { + console.error(err) + } + }, + + async newConcept({ commit }, { file, ...data }) { + let concept = await conceptsApi.newConcept(data) + const preview = concept.tasks[0].previews[0] + const { request, promise } = tasksApi.uploadPreview(preview.id, file) + request.on('progress', e => { + // commit(SET_UPLOAD_PROGRESS, { + // previewId: preview.id, + // percent: e.percent, + // name: file.name + // }) + }) + concept = await promise + commit(EDIT_CONCEPT_END, concept) + return concept + }, + + async saveConcept({ commit }, data) { + const concept = await conceptsApi.updateConcept(data) + commit(EDIT_CONCEPT_END, concept) + return concept + }, + + async deleteConcept({ commit }, concept) { + await conceptsApi.deleteConcept(concept) + commit(DELETE_CONCEPT_END, concept) + } +} + +const mutations = { + [LOAD_CONCEPTS_START](state) { + state.concepts = [] + state.conceptMap = new Map() + state.displayedConcepts = [] + }, + + [LOAD_CONCEPTS_ERROR](state) { + state.concepts = [] + state.conceptMap = new Map() + state.displayedConcepts = [] + }, + + [LOAD_CONCEPTS_END](state, concepts) { + concepts.forEach(concept => { + concept.url = `/api/pictures/preview-files/${concept.id}.${concept.extension}` + concept.thumbnail = `/api/pictures/thumbnails/preview-files/${concept.id}.png` + }) + // state.concepts = sortByName(concepts) + state.concepts = concepts + state.conceptMap = new Map( + state.concepts.map(concept => [concept.id, concept]) + ) + }, + + [EDIT_CONCEPT_END](state, newConcept) { + newConcept.url = `/api/pictures/preview-files/${newConcept.id}.${newConcept.extension}` + newConcept.thumbnail = `/api/pictures/thumbnails/preview-files/${newConcept.id}.png` + + const concept = state.conceptMap.get(newConcept.id) + + if (concept?.id) { + Object.assign(concept, newConcept) + state.conceptMap.delete(concept.id) + state.conceptMap.set(concept.id, concept) + // state.concepts = sortByName(state.concepts) + } else { + state.concepts.push(newConcept) + state.conceptMap.set(newConcept.id, newConcept) + // state.concepts = sortByName(state.concepts) + } + }, + + [DELETE_CONCEPT_END](state, conceptToDelete) { + const conceptToDeleteIndex = state.concepts.findIndex( + ({ id }) => id === conceptToDelete.id + ) + if (conceptToDeleteIndex >= 0) { + state.concepts.splice(conceptToDeleteIndex, 1) + } + delete state.conceptMap.get(conceptToDelete.id) + }, + + [RESET_ALL](state) { + Object.assign(state, { ...initialState }) + } +} + +export default { + state, + getters, + actions, + mutations +} diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js index af46a2fe04..f6eaaee5a5 100644 --- a/src/store/mutation-types.js +++ b/src/store/mutation-types.js @@ -636,3 +636,11 @@ export const LOAD_BACKGROUNDS_ERROR = 'LOAD_BACKGROUNDS_ERROR' export const LOAD_BACKGROUNDS_END = 'LOAD_BACKGROUNDS_END' export const EDIT_BACKGROUND_END = 'EDIT_BACKGROUND_END' export const DELETE_BACKGROUND_END = 'DELETE_BACKGROUND_END' + +// Concepts + +export const LOAD_CONCEPTS_START = 'LOAD_CONCEPTS_START' +export const LOAD_CONCEPTS_ERROR = 'LOAD_CONCEPTS_ERROR' +export const LOAD_CONCEPTS_END = 'LOAD_CONCEPTS_END' +export const EDIT_CONCEPT_END = 'EDIT_CONCEPT_END' +export const DELETE_CONCEPT_END = 'DELETE_CONCEPT_END' From 7ffc43bcafcb008d3f5f20bd0633fec44ac3e2b3 Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Wed, 22 Nov 2023 13:23:44 +0100 Subject: [PATCH 05/41] [concepts] add create concept --- src/components/modals/AddPreviewModal.vue | 36 +++++++++++---- src/components/pages/Concepts.vue | 56 +++++++++++++++++++---- src/locales/en.js | 3 ++ 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/components/modals/AddPreviewModal.vue b/src/components/modals/AddPreviewModal.vue index bd4a6af978..3ea3579a78 100644 --- a/src/components/modals/AddPreviewModal.vue +++ b/src/components/modals/AddPreviewModal.vue @@ -15,7 +15,7 @@ {{ $t('tasks.change_preview') }}

- {{ $t('tasks.add_preview') }} + {{ isConcept ? $t('concepts.add_concept') : $t('tasks.add_preview') }}

@@ -25,7 +25,7 @@

- {{ $t('tasks.add_preview_error') }} + {{ + isConcept + ? $t('concepts.add_concept_error') + : $t('tasks.add_preview_error') + }}

@@ -73,9 +77,9 @@

-
+
- {{ $t('tasks.revision_preview_file') }} + {{ $t(message) }}
@@ -89,7 +93,11 @@ }" @click="$emit('confirm', forms)" > - {{ $t('tasks.add_revision_confirm') }} + {{ + isConcept + ? $t('main.confirmation') + : $t('tasks.add_revision_confirm') + }}
@@ -56,8 +55,8 @@
@@ -113,13 +112,25 @@
+ +
@@ -134,6 +145,7 @@ import { sortByName } from '@/lib/sorting' import { searchMixin } from '@/components/mixins/search' +import AddPreviewModal from '@/components/modals/AddPreviewModal' import ButtonSimple from '@/components/widgets/ButtonSimple' import Combobox from '@/components/widgets/Combobox.vue' import ComboboxStatus from '@/components/widgets/ComboboxStatus.vue' @@ -149,6 +161,7 @@ export default { mixins: [searchMixin], components: { + AddPreviewModal, ButtonSimple, Combobox, ComboboxStatus, @@ -163,10 +176,12 @@ export default { data() { return { loading: { + addingConcept: false, loadingConcepts: true, savingSearch: false }, - error: { + errors: { + addingConcept: false, loadingConcepts: false }, filters: { @@ -175,6 +190,12 @@ export default { entityType: null, sortBy: 'created_at' }, + form: { + file: null + }, + modals: { + addConcept: false + }, // TODO: module getters currentTask: null, @@ -228,7 +249,7 @@ export default { // FIXME: remove fake loading setTimeout(() => { this.loading.loadingConcepts = false - }, 2000) + }, 500) this.setSearch(this.$route.query.search) @@ -331,7 +352,7 @@ export default { }, methods: { - ...mapActions([]), + ...mapActions(['newConcept']), // TODO: module actions setConceptSearch: searchQuery => Promise.resolve(), @@ -387,8 +408,23 @@ export default { : null }, - onAddConceptClicked() { - alert('onAddConceptClicked') + openAddConceptModal() { + this.modals.addConcept = true + }, + + closeAddConceptModal() { + this.modals.addConcept = false + }, + + async confirmAddConceptModal(forms) { + const file = forms[0].get('file') + try { + await this.newConcept({ file }) + this.closeAddConceptModal() + } catch (error) { + console.error(error) + this.errors.addingConcept = true + } } }, diff --git a/src/locales/en.js b/src/locales/en.js index 34a0a5f01c..ad14894c8a 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -135,6 +135,9 @@ export default { }, concepts: { + add_new_concept: 'Add a new reference to concepts', + add_concept: 'Add file for a new concept revision', + add_concept_error: 'An error occurred while adding concept.', title: 'Concepts', fields: { entity_type: 'Entity type', From 4c6d0fefe4d5cf5f6cdac798acb8c980a4d7608a Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Thu, 23 Nov 2023 17:45:55 +0100 Subject: [PATCH 06/41] [concepts] add load concepts --- src/components/pages/Concepts.vue | 70 ++++++++++--------------------- src/store/modules/concepts.js | 16 +++---- 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue index 462bec3730..49d69f4a69 100644 --- a/src/components/pages/Concepts.vue +++ b/src/components/pages/Concepts.vue @@ -50,7 +50,7 @@

- {{ $t('concepts.title') }} ({{ concepts?.length || 0 }}) + {{ $t('concepts.title') }} ({{ filteredConcepts?.length || 0 }})

@@ -206,59 +206,20 @@ export default { name: 'test', search_query: 'test' } - ], - concepts: [ - { - id: 1, - name: 'concept 1', - url: 'https://placehold.co/300x200', - status: 'concept-neutral', - task: null, - assignees: ['9609db21-9fe7-4c98-a536-5590f7ff1676'], // me - tags: ['tag1', 'tag2', 'tag3'], - created_at: '2023-11-20T00:00:00', - updated_at: '2023-11-23T00:00:00' - }, - { - id: 2, - name: 'concept 2', - url: 'https://placehold.co/300x200', - status: 'concept-approved', - task: null, - assignees: ['532028cc-a249-4cc6-ace6-86f63ca9c5c8'], // alicia cooper - tags: [], - created_at: '2023-11-21T00:00:00', - updated_at: '2023-11-24T00:00:00' - }, - { - id: 3, - name: 'concept 3', - url: 'https://placehold.co/300x200', - status: 'concept-rejected', - task: null, - assignees: [], - tags: [], - created_at: '2023-11-23T00:00:00', - updated_at: '2023-11-23T00:00:00' - } ] } }, mounted() { - // FIXME: remove fake loading - setTimeout(() => { - this.loading.loadingConcepts = false - }, 500) - this.setSearch(this.$route.query.search) - + this.refreshConcepts() this.searchField.focus() }, computed: { ...mapGetters([ 'currentProduction', + 'displayedConcepts', 'isDarkTheme', 'personMap', 'taskStatusMap', @@ -306,7 +267,7 @@ export default { }, filteredConcepts() { - let concepts = this.concepts.slice() + let concepts = this.displayedConcepts.slice() if (this.filters.taskStatusId) { concepts = concepts.filter( concept => concept.status === this.filters.taskStatusId @@ -352,7 +313,7 @@ export default { }, methods: { - ...mapActions(['newConcept']), + ...mapActions(['loadConcepts', 'newConcept']), // TODO: module actions setConceptSearch: searchQuery => Promise.resolve(), @@ -389,6 +350,18 @@ export default { }) }, + async refreshConcepts() { + this.loading.loadingConcepts = true + try { + await this.loadConcepts() + } catch (err) { + console.error(err) + this.errors.loadingConcepts = true + } finally { + this.loading.loadingConcepts = false + } + }, + onPreviewClicked() { alert('onPreviewClicked') }, @@ -417,13 +390,16 @@ export default { }, async confirmAddConceptModal(forms) { + this.loading.addingConcept = true const file = forms[0].get('file') try { await this.newConcept({ file }) this.closeAddConceptModal() - } catch (error) { - console.error(error) + } catch (err) { + console.error(err) this.errors.addingConcept = true + } finally { + this.loading.addingConcept = false } } }, diff --git a/src/store/modules/concepts.js b/src/store/modules/concepts.js index 2d890e7560..8c7ca2a3b1 100644 --- a/src/store/modules/concepts.js +++ b/src/store/modules/concepts.js @@ -24,7 +24,8 @@ const state = { const getters = { concepts: state => state.concepts, - conceptMap: state => state.conceptMap + conceptMap: state => state.conceptMap, + displayedConcepts: state => state.displayedConcepts // editConcept: state => state.editConcept, // deleteConcept: state => state.deleteConcept } @@ -100,6 +101,7 @@ const mutations = { state.conceptMap = new Map( state.concepts.map(concept => [concept.id, concept]) ) + state.displayedConcepts = concepts }, [EDIT_CONCEPT_END](state, newConcept) { @@ -120,14 +122,12 @@ const mutations = { } }, - [DELETE_CONCEPT_END](state, conceptToDelete) { - const conceptToDeleteIndex = state.concepts.findIndex( - ({ id }) => id === conceptToDelete.id - ) - if (conceptToDeleteIndex >= 0) { - state.concepts.splice(conceptToDeleteIndex, 1) + [DELETE_CONCEPT_END](state, concept) { + const conceptIndex = state.concepts.findIndex(({ id }) => id === concept.id) + if (conceptIndex >= 0) { + state.concepts.splice(conceptIndex, 1) } - delete state.conceptMap.get(conceptToDelete.id) + delete state.conceptMap.get(concept.id) }, [RESET_ALL](state) { From c7e9bbd0c335ec4f9756b0c0e74f5029e69eac9d Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Tue, 28 Nov 2023 19:16:36 +0100 Subject: [PATCH 07/41] [concepts] update list of concepts --- src/components/pages/Concepts.vue | 71 +++++++++++------------- src/components/widgets/EntityPreview.vue | 2 +- src/store/modules/concepts.js | 8 --- 3 files changed, 32 insertions(+), 49 deletions(-) diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue index 49d69f4a69..adbac2237e 100644 --- a/src/components/pages/Concepts.vue +++ b/src/components/pages/Concepts.vue @@ -70,13 +70,13 @@ :key="concept.id" @click="onSelectConcept(concept)" > -
{{ concept.name }} @@ -101,7 +101,7 @@ :person="personMap.get(personId)" :size="25" :font-size="14" - v-for="personId in concept.assignees" + v-for="personId in concept.tasks[0].assignees" />
@@ -132,7 +132,7 @@ />
- +
@@ -149,9 +149,10 @@ import AddPreviewModal from '@/components/modals/AddPreviewModal' import ButtonSimple from '@/components/widgets/ButtonSimple' import Combobox from '@/components/widgets/Combobox.vue' import ComboboxStatus from '@/components/widgets/ComboboxStatus.vue' +import EntityPreview from '@/components/widgets/EntityPreview.vue' +import PeopleAvatar from '@/components/widgets/PeopleAvatar' import PeopleField from '@/components/widgets/PeopleField' import SearchField from '@/components/widgets/SearchField' -import PeopleAvatar from '@/components/widgets/PeopleAvatar' import SearchQueryList from '@/components/widgets/SearchQueryList' import TableInfo from '@/components/widgets/TableInfo' import TaskInfo from '@/components/sides/TaskInfo' @@ -165,9 +166,10 @@ export default { ButtonSimple, Combobox, ComboboxStatus, + EntityPreview, + PeopleAvatar, PeopleField, SearchField, - PeopleAvatar, SearchQueryList, TableInfo, TaskInfo @@ -222,22 +224,24 @@ export default { 'displayedConcepts', 'isDarkTheme', 'personMap', - 'taskStatusMap', - 'taskTypes' + 'taskStatusMap' ]), assignees() { - const assignees = [] - const assigneesMap = {} - this.filteredConcepts.forEach(task => { - task.assignees.forEach(personId => { - if (!assigneesMap[personId]) { - assignees.push(this.personMap.get(personId)) - assigneesMap[personId] = true - } + const assignees = new Map() + this.filteredConcepts.forEach(concept => { + concept.tasks.forEach(task => { + task.assignees.forEach(personId => { + if (!assignees.has(personId)) { + const person = this.personMap.get(personId) + if (person) { + assignees.set(personId, person) + } + } + }) }) }) - return sortByName(assignees) + return sortByName([...assignees.values()]) }, entityTypeOptions() { @@ -275,7 +279,8 @@ export default { } if (this.filters.assignee) { concepts = concepts.filter(concept => - concept.assignees?.includes(this.filters.assignee.id) + // FIXME: loop instead of tasks[0] ??? + concept.tasks[0].assignees?.includes(this.filters.assignee.id) ) } if (this.filters.entityType) { @@ -362,10 +367,6 @@ export default { } }, - onPreviewClicked() { - alert('onPreviewClicked') - }, - onSelectConcept(concept) { // FIXME: remove mock data concept.task = { @@ -377,7 +378,7 @@ export default { this.currentTask = !this.currentTask || this.currentTask.entity_id !== concept.id - ? concept.task + ? concept.tasks[0] : null }, @@ -418,7 +419,6 @@ export default { .filters { display: flex; align-items: flex-end; - flex-wrap: wrap; gap: 0 20px; padding: 10px; @@ -460,17 +460,6 @@ export default { background-color: var(--background-hover); } - .preview { - cursor: zoom-in; - background-color: $black; - border-top-left-radius: inherit; - border-top-right-radius: inherit; - max-width: 300px; - min-width: 300px; - max-height: 200px; - min-height: 200px; - } - .description { display: flex; flex-direction: column; @@ -519,6 +508,8 @@ export default { } .footer { + position: sticky; + bottom: 0; display: flex; justify-content: center; padding: 3em; diff --git a/src/components/widgets/EntityPreview.vue b/src/components/widgets/EntityPreview.vue index f68397c29d..652b42a681 100644 --- a/src/components/widgets/EntityPreview.vue +++ b/src/components/widgets/EntityPreview.vue @@ -30,7 +30,7 @@ 'border-top-left-radius': isRoundedTopBorder ? '10px' : '', 'border-top-right-radius': isRoundedTopBorder ? '10px' : '' }" - @click="onPictureClicked()" + @click.stop="onPictureClicked()" v-else > { - concept.url = `/api/pictures/preview-files/${concept.id}.${concept.extension}` - concept.thumbnail = `/api/pictures/thumbnails/preview-files/${concept.id}.png` - }) - // state.concepts = sortByName(concepts) state.concepts = concepts state.conceptMap = new Map( state.concepts.map(concept => [concept.id, concept]) @@ -105,9 +100,6 @@ const mutations = { }, [EDIT_CONCEPT_END](state, newConcept) { - newConcept.url = `/api/pictures/preview-files/${newConcept.id}.${newConcept.extension}` - newConcept.thumbnail = `/api/pictures/thumbnails/preview-files/${newConcept.id}.png` - const concept = state.conceptMap.get(newConcept.id) if (concept?.id) { From 026ae66a5b077b37015de6e39d3db671f38fbe9e Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Tue, 28 Nov 2023 19:31:30 +0100 Subject: [PATCH 08/41] [concepts] update task panel to support the concepts --- src/components/previews/PreviewPlayer.vue | 45 ++++++++++++---------- src/components/sides/TaskInfo.vue | 6 ++- src/components/tops/ActionPanel.vue | 47 ++++++++++++++++++++++- src/components/widgets/AddComment.vue | 6 +++ src/components/widgets/Comment.vue | 7 +++- 5 files changed, 87 insertions(+), 24 deletions(-) diff --git a/src/components/previews/PreviewPlayer.vue b/src/components/previews/PreviewPlayer.vue index 6413502871..f5782025c3 100644 --- a/src/components/previews/PreviewPlayer.vue +++ b/src/components/previews/PreviewPlayer.vue @@ -205,7 +205,7 @@ -
+
@@ -263,7 +263,7 @@ class="flexrow-item flexrow-item" :title="$t('playlists.actions.annotation_redo')" icon="redo" - v-if="!readOnly && fullScreen" + v-if="!readOnly && fullScreen && !isConcept" @click="redoLastAction" /> @@ -272,7 +272,7 @@ icon="remove" :title="$t('playlists.actions.annotation_delete')" @click="onDeleteClicked" - v-if="!readOnly && fullScreen" + v-if="!readOnly && fullScreen && !isConcept" /> @@ -295,7 +295,7 @@ :active="isTyping" :title="$t('playlists.actions.annotation_text')" @click="onTypeClicked" - v-if="!readOnly && (!light || fullScreen)" + v-if="!readOnly && (!light || fullScreen) && !isConcept" /> @@ -326,7 +326,7 @@ :active="isDrawing" :title="$t('playlists.actions.annotation_draw')" @click="onPencilAnnotateClicked" - v-if="!readOnly && (!light || fullScreen)" + v-if="!readOnly && (!light || fullScreen) && !isConcept" /> -
-
- -
+
+
-
+
@@ -198,6 +204,18 @@
+ + +
+
+
Tag linked to Concept
+
    +
  • tag1
  • +
+
+
Date: Wed, 29 Nov 2023 12:46:11 +0100 Subject: [PATCH 09/41] [concepts] fix concept creation --- src/components/modals/AddPreviewModal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modals/AddPreviewModal.vue b/src/components/modals/AddPreviewModal.vue index 3ea3579a78..a1d41b9de8 100644 --- a/src/components/modals/AddPreviewModal.vue +++ b/src/components/modals/AddPreviewModal.vue @@ -179,7 +179,7 @@ export default { ...mapActions([]), onFileSelected(forms) { - this.forms = this.forms.concat(forms) + this.forms = this.isMultiple ? this.forms.concat(forms) : [forms] this.$emit('fileselected', this.forms) }, From cc4b3d9c29d19acd2df8983e5c14ecf60975b9c7 Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Thu, 30 Nov 2023 13:30:03 +0100 Subject: [PATCH 10/41] [concepts] fix concept selection --- src/components/pages/Concepts.vue | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue index adbac2237e..1d10787b91 100644 --- a/src/components/pages/Concepts.vue +++ b/src/components/pages/Concepts.vue @@ -200,7 +200,6 @@ export default { }, // TODO: module getters - currentTask: null, conceptSearchQueries: [ { id: 'filter-test-1', @@ -224,6 +223,7 @@ export default { 'displayedConcepts', 'isDarkTheme', 'personMap', + 'selectedTasks', 'taskStatusMap' ]), @@ -244,6 +244,10 @@ export default { return sortByName([...assignees.values()]) }, + currentTask() { + return this.selectedTasks.values().next().value + }, + entityTypeOptions() { const allEntityTypeOptions = { label: this.$t('main.all'), @@ -318,7 +322,12 @@ export default { }, methods: { - ...mapActions(['loadConcepts', 'newConcept']), + ...mapActions([ + 'addSelectedTask', + 'clearSelectedTasks', + 'loadConcepts', + 'newConcept' + ]), // TODO: module actions setConceptSearch: searchQuery => Promise.resolve(), @@ -368,18 +377,13 @@ export default { }, onSelectConcept(concept) { - // FIXME: remove mock data - concept.task = { - id: `task-${concept.id}`, - entity_id: concept.id, - task_type_id: this.taskTypes[0].id, - project_id: this.currentProduction.id + const task = concept.tasks[0] + if (this.selectedTasks.has(task.id)) { + this.clearSelectedTasks() + } else { + this.clearSelectedTasks() + this.addSelectedTask(task) } - - this.currentTask = - !this.currentTask || this.currentTask.entity_id !== concept.id - ? concept.tasks[0] - : null }, openAddConceptModal() { From 66316380d2f4861687ebd2a4bd055ac18d45c9d0 Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Fri, 1 Dec 2023 23:58:42 +0100 Subject: [PATCH 11/41] [concepts] add delete concepts --- src/components/pages/Concepts.vue | 52 ++++-- src/components/sides/TaskInfo.vue | 5 +- src/components/tops/ActionPanel.vue | 149 ++++++++++++------ .../tops/actions/DeleteEntities.vue | 12 +- src/locales/en.js | 8 +- src/store/modules/concepts.js | 57 ++++++- src/store/mutation-types.js | 2 + 7 files changed, 208 insertions(+), 77 deletions(-) diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue index 1d10787b91..47691b1c2e 100644 --- a/src/components/pages/Concepts.vue +++ b/src/components/pages/Concepts.vue @@ -64,11 +64,13 @@
  • -
    - +
    +
    @@ -223,7 +225,7 @@ export default { 'displayedConcepts', 'isDarkTheme', 'personMap', - 'selectedTasks', + 'selectedConcepts', 'taskStatusMap' ]), @@ -245,7 +247,13 @@ export default { }, currentTask() { - return this.selectedTasks.values().next().value + return this.currentConcept?.tasks[0] + }, + + currentConcept() { + return this.selectedConcepts.size === 1 + ? this.selectedConcepts.values().next().value + : null }, entityTypeOptions() { @@ -275,7 +283,7 @@ export default { }, filteredConcepts() { - let concepts = this.displayedConcepts.slice() + let concepts = [...this.displayedConcepts] if (this.filters.taskStatusId) { concepts = concepts.filter( concept => concept.status === this.filters.taskStatusId @@ -283,7 +291,6 @@ export default { } if (this.filters.assignee) { concepts = concepts.filter(concept => - // FIXME: loop instead of tasks[0] ??? concept.tasks[0].assignees?.includes(this.filters.assignee.id) ) } @@ -323,7 +330,9 @@ export default { methods: { ...mapActions([ + 'addSelectedConcepts', 'addSelectedTask', + 'clearSelectedConcepts', 'clearSelectedTasks', 'loadConcepts', 'newConcept' @@ -376,13 +385,28 @@ export default { } }, - onSelectConcept(concept) { - const task = concept.tasks[0] - if (this.selectedTasks.has(task.id)) { - this.clearSelectedTasks() + isSelected(concept) { + return this.selectedConcepts.has(concept.id) + }, + + onSelectConcept(concept, isMultipleSelection = false) { + const selection = isMultipleSelection + ? new Map(this.selectedConcepts) + : new Map() + if ( + (isMultipleSelection && this.isSelected(concept)) || + (!isMultipleSelection && concept === this.currentConcept) + ) { + selection.delete(concept.id) } else { - this.clearSelectedTasks() - this.addSelectedTask(task) + selection.set(concept.id, concept) + } + this.clearSelectedConcepts() + this.addSelectedConcepts(selection) + + this.clearSelectedTasks() + if (this.currentTask) { + this.addSelectedTask(this.currentTask) } }, diff --git a/src/components/sides/TaskInfo.vue b/src/components/sides/TaskInfo.vue index 381534254d..836b45df6e 100644 --- a/src/components/sides/TaskInfo.vue +++ b/src/components/sides/TaskInfo.vue @@ -50,7 +50,7 @@ class="flexrow-item task-type" :task-type="currentTaskType" :production-id="currentProduction.id" - v-if="currentTaskType" + v-if="currentTaskType && !isConceptTask" />
    @@ -442,6 +442,7 @@ export default { 'previewFormData', 'productionMap', 'selectedAssets', + 'selectedConcepts', 'selectedEdits', 'selectedShots', 'selectedTasks', @@ -459,7 +460,7 @@ export default { }, nbSelectedEntities() { - return this.selectedEntities ? this.selectedEntities.size : 0 + return this.selectedEntities?.size || 0 }, selectedEntities() { diff --git a/src/components/tops/ActionPanel.vue b/src/components/tops/ActionPanel.vue index ea0e9ea5e8..ef0794fa5d 100644 --- a/src/components/tops/ActionPanel.vue +++ b/src/components/tops/ActionPanel.vue @@ -98,6 +98,20 @@
    + + - - + +
    + +
  • @@ -791,6 +812,7 @@ export default { assetDeletion: false, changePriority: false, changeStatus: false, + conceptDeletion: false, editDeletion: false, episodeDeletion: false, taskCreation: false, @@ -802,6 +824,7 @@ export default { errors: { assetDeletion: false, taskDeletion: false, + conceptDeletion: false, editDeletion: false, episodeDeletion: false, shotDeletion: false @@ -836,6 +859,7 @@ export default { 'personMap', 'productionMap', 'selectedAssets', + 'selectedConcepts', 'selectedEdits', 'selectedShots', 'selectedTasks', @@ -860,8 +884,8 @@ export default { currentEntityType() { if (this.isCurrentViewAsset) return 'asset' - else if (this.isCurrentViewShot) return 'shot' - else if (this.isCurrentViewEdit) return 'edit' + if (this.isCurrentViewShot) return 'shot' + if (this.isCurrentViewEdit) return 'edit' return 'episode' }, @@ -907,20 +931,26 @@ export default { return this.selectedEdits.size }, + nbSelectedConcepts() { + return this.selectedConcepts.size + }, + isHidden() { return ( (this.nbSelectedTasks === 0 && this.nbSelectedValidations === 0 && this.nbSelectedAssets === 0 && this.nbSelectedShots === 0 && - this.nbSelectedEdits === 0) || + this.nbSelectedEdits === 0 && + this.nbSelectedConcepts === 0) || !( this.isCurrentViewAsset || this.isCurrentViewTodos || this.isCurrentViewShot || this.isCurrentViewEpisode || this.isCurrentViewSequence || - this.isCurrentViewEdit + this.isCurrentViewEdit || + this.isCurrentViewConcept ) ) }, @@ -940,17 +970,15 @@ export default { }, isCurrentViewAsset() { - return ( - this.$route.path.indexOf('asset') > 0 && !this.$route.params.shot_id - ) + return this.$route.path.includes('asset') && !this.$route.params.shot_id }, isCurrentViewShot() { - return this.$route.path.indexOf('shot') > 0 && !this.$route.params.shot_id + return this.$route.path.includes('shot') && !this.$route.params.shot_id }, isCurrentViewEdit() { - return this.$route.path.indexOf('edit') > 0 && !this.$route.params.edit_id + return this.$route.path.includes('edit') && !this.$route.params.edit_id }, isCurrentViewConcept() { @@ -959,21 +987,21 @@ export default { isCurrentViewTodos() { return ( - this.$route.path.indexOf('my-tasks') > 0 || - this.$route.path.indexOf('people/') > 0 + this.$route.path.includes('my-tasks') || + this.$route.path.includes('people/') ) }, isCurrentViewPerson() { - return this.$route.path.indexOf('people/') > 0 + return this.$route.path.includes('people/') }, isCurrentViewPersonTasks() { - return this.$route.path.indexOf('todos') > 0 + return this.$route.path.includes('todos') }, isCurrentViewTaskType() { - return this.$route.path.indexOf('task-type') > 0 + return this.$route.path.includes('task-type') }, isCurrentViewEntity() { @@ -992,7 +1020,7 @@ export default { this.isCurrentViewAsset || this.isCurrentViewShot || this.isCurrentViewEdit - ) && this.$route.path.indexOf('episodes') > 0 + ) && this.$route.path.includes('episodes') ) }, @@ -1002,7 +1030,7 @@ export default { this.isCurrentViewAsset || this.isCurrentViewShot || this.isCurrentViewEdit - ) && this.$route.path.indexOf('sequences') > 0 + ) && this.$route.path.includes('sequences') ) }, @@ -1038,7 +1066,12 @@ export default { storagePrefix() { let prefix = 'todos-' - if (this.isCurrentViewAsset || this.isCurrentViewShot) { + if ( + this.isCurrentViewAsset || + this.isCurrentViewShot || + this.isCurrentViewEdit || + this.isCurrentViewConcept + ) { prefix = 'entities-' } if (this.isCurrentViewTaskType) prefix = 'tasks-' @@ -1052,12 +1085,14 @@ export default { 'clearSelectedAssets', 'clearSelectedShots', 'clearSelectedEdits', + 'clearSelectedConcepts', 'createSelectedTasks', 'deleteSelectedAssets', 'deleteSelectedShots', 'deleteSelectedTasks', 'deleteSelectedEdits', 'deleteSelectedEpisodes', + 'deleteSelectedConcepts', 'changeSelectedTaskStatus', 'changeSelectedPriorities', 'clearSelectedTasks', @@ -1142,14 +1177,13 @@ export default { }, confirmTaskCreation() { - const type = - this.$route.path.indexOf('shots') > 0 - ? 'shots' - : this.$route.path.indexOf('assets') > 0 - ? 'assets' - : this.$route.path.indexOf('edits') > 0 - ? 'edits' - : 'episodes' + const type = this.$route.path.includes('shots') + ? 'shots' + : this.$route.path.includes('assets') + ? 'assets' + : this.$route.path.includes('edits') + ? 'edits' + : 'episodes' this.loading.taskCreation = true this.createSelectedTasks({ type, @@ -1223,6 +1257,21 @@ export default { }) }, + confirmConceptDeletion() { + this.loading.deleteConcept = true + this.errors.deleteConcept = false + this.deleteSelectedConcepts() + .then(() => { + this.loading.deleteConcept = false + this.clearSelectedConcepts() + }) + .catch(err => { + console.error(err) + this.loading.deleteConcept = false + this.errors.deleteConcept = true + }) + }, + confirmPlaylistGeneration() { this.modals.playlist = true this.selectedBar = '' @@ -1362,18 +1411,25 @@ export default { this.selectedBar = 'delete-edits' return } + if (this.isCurrentViewConcept && this.nbSelectedConcepts > 1) { + this.selectedBar = 'delete-concepts' + return + } if (this.nbSelectedTasks === 1) { this.selectedBar = '' } - const prefix = this.storagePrefix - const lastSelection = localStorage.getItem(`${prefix}-selected-bar`) + const lastSelection = localStorage.getItem( + `${this.storagePrefix}-selected-bar` + ) if (lastSelection) { this.selectedBar = lastSelection - } else { - if (this.isCurrentViewAsset || this.isCurrentViewShot) { - this.selectedBar = 'change-status' - } + } else if ( + this.isCurrentViewAsset || + this.isCurrentViewShot || + this.isCurrentViewEdit + ) { + this.selectedBar = 'change-status' } } else { window.removeEventListener('keydown', this.onKeyDown) @@ -1414,7 +1470,12 @@ export default { nbSelectedEdits() { this.autoChooseSelectBar() - if (this.nbSelectedShots > 0) this.clearSelectedTasks() + if (this.nbSelectedEdits > 0) this.clearSelectedTasks() + }, + + nbSelectedConcepts() { + this.autoChooseSelectBar() + if (this.nbSelectedConcepts > 1) this.clearSelectedTasks() }, isHidden() { diff --git a/src/components/tops/actions/DeleteEntities.vue b/src/components/tops/actions/DeleteEntities.vue index 63c484d808..2ab080b747 100644 --- a/src/components/tops/actions/DeleteEntities.vue +++ b/src/components/tops/actions/DeleteEntities.vue @@ -15,8 +15,6 @@ diff --git a/src/locales/en.js b/src/locales/en.js index ad14894c8a..fc2aa47de3 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -138,6 +138,8 @@ export default { add_new_concept: 'Add a new reference to concepts', add_concept: 'Add file for a new concept revision', add_concept_error: 'An error occurred while adding concept.', + delete_for_selection: 'Delete the selected concept | Delete the {nbSelectedConcepts} selected concepts', + multiple_delete_error: 'An error occurred while deleting a concept. There is probably some data linked to a concept. Are you sure there is no task linked to a selected concept?', title: 'Concepts', fields: { entity_type: 'Entity type', @@ -585,14 +587,16 @@ export default { change_status: 'Change status', create_tasks: 'Create tasks', delete_assets: 'Delete assets', - delete_shots: 'Delete shots', + delete_concepts: 'Delete concepts', delete_edits: 'Delete edits', + delete_shots: 'Delete shots', delete_tasks: 'Delete tasks', generate_playlist: 'Generate a playlist', run_custom_action: 'Run custom action', set_estimations: 'Set estimations', set_thumbnails: 'Set thumbnails from last preview', - subscribe: 'Subscribe to notifications' + subscribe: 'Subscribe to notifications', + tag_concepts: "Edit concept tags", }, news: { diff --git a/src/store/modules/concepts.js b/src/store/modules/concepts.js index ab104b9414..40300975b0 100644 --- a/src/store/modules/concepts.js +++ b/src/store/modules/concepts.js @@ -1,3 +1,4 @@ +import async from 'async' import conceptsApi from '@/store/api/concepts' import tasksApi from '@/store/api/tasks' @@ -7,6 +8,8 @@ import { LOAD_CONCEPTS_END, EDIT_CONCEPT_END, DELETE_CONCEPT_END, + ADD_SELECTED_CONCEPTS, + CLEAR_SELECTED_CONCEPTS, RESET_ALL } from '@/store/mutation-types' @@ -15,7 +18,8 @@ const initialState = { conceptMap: new Map(), displayedConcepts: [], conceptSearchText: '', - conceptSearchQueries: [] + conceptSearchQueries: [], + selectedConcepts: new Map() } const state = { @@ -25,9 +29,8 @@ const state = { const getters = { concepts: state => state.concepts, conceptMap: state => state.conceptMap, - displayedConcepts: state => state.displayedConcepts - // editConcept: state => state.editConcept, - // deleteConcept: state => state.deleteConcept + displayedConcepts: state => state.displayedConcepts, + selectedConcepts: state => state.selectedConcepts } const actions = { @@ -75,6 +78,41 @@ const actions = { async deleteConcept({ commit }, concept) { await conceptsApi.deleteConcept(concept) commit(DELETE_CONCEPT_END, concept) + }, + + addSelectedConcepts({ commit }, concept) { + commit(ADD_SELECTED_CONCEPTS, concept) + }, + + deleteSelectedConcepts({ state, dispatch }) { + return new Promise((resolve, reject) => { + let selectedConceptIds = [...state.selectedConcepts.values()] + .filter(concept => !concept.canceled) + .map(concept => concept.id) + if (selectedConceptIds.length === 0) { + selectedConceptIds = [...state.selectedConcepts.keys()] + } + async.eachSeries( + selectedConceptIds, + (conceptId, next) => { + const concept = state.conceptMap.get(conceptId) + if (concept) { + dispatch('deleteConcept', concept) + } + next() + }, + err => { + if (err) reject(err) + else { + resolve() + } + } + ) + }) + }, + + clearSelectedConcepts({ commit }) { + commit(CLEAR_SELECTED_CONCEPTS) } } @@ -122,6 +160,17 @@ const mutations = { delete state.conceptMap.get(concept.id) }, + [ADD_SELECTED_CONCEPTS](state, concepts) { + concepts.forEach(concept => { + state.selectedConcepts.set(concept.id, concept) + }) + state.selectedConcepts = new Map(state.selectedConcepts) // for reactivity + }, + + [CLEAR_SELECTED_CONCEPTS](state) { + state.selectedConcepts = new Map() + }, + [RESET_ALL](state) { Object.assign(state, { ...initialState }) } diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js index f6eaaee5a5..53bb01c78b 100644 --- a/src/store/mutation-types.js +++ b/src/store/mutation-types.js @@ -644,3 +644,5 @@ export const LOAD_CONCEPTS_ERROR = 'LOAD_CONCEPTS_ERROR' export const LOAD_CONCEPTS_END = 'LOAD_CONCEPTS_END' export const EDIT_CONCEPT_END = 'EDIT_CONCEPT_END' export const DELETE_CONCEPT_END = 'DELETE_CONCEPT_END' +export const ADD_SELECTED_CONCEPTS = 'ADD_SELECTED_CONCEPTS' +export const CLEAR_SELECTED_CONCEPTS = 'CLEAR_SELECTED_CONCEPTS' From c41becb4dd9840965478a33fbbcfa68da3b87b0e Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Thu, 7 Dec 2023 12:27:12 +0100 Subject: [PATCH 12/41] [concepts] add asset tagging --- src/components/pages/Concepts.vue | 24 ++- src/components/previews/PreviewPlayer.vue | 22 +++ src/components/tops/ActionPanel.vue | 181 ++++++++++++++++++++- src/components/widgets/SearchQueryList.vue | 4 +- src/locales/en.js | 4 + 5 files changed, 218 insertions(+), 17 deletions(-) diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue index 47691b1c2e..49389e989a 100644 --- a/src/components/pages/Concepts.vue +++ b/src/components/pages/Concepts.vue @@ -81,10 +81,14 @@ is-rounded-top-border />
    - {{ concept.name }}
    @@ -418,6 +422,10 @@ export default { this.modals.addConcept = false }, + onSelectedTag(tag) { + // TODO: select ta action ??? + }, + async confirmAddConceptModal(forms) { this.loading.addingConcept = true const file = forms[0].get('file') @@ -494,12 +502,10 @@ export default { flex-wrap: wrap; row-gap: 10px; padding: 0.3em 1em; - margin-bottom: 0.3em; - color: var(--text-strong); - - font-weight: 500; - - text-transform: uppercase; + margin: 0.3em 0; + // color: var(--text-strong); + // font-weight: 500; + // text-transform: uppercase; } .tags { diff --git a/src/components/previews/PreviewPlayer.vue b/src/components/previews/PreviewPlayer.vue index f5782025c3..61d591d4ac 100644 --- a/src/components/previews/PreviewPlayer.vue +++ b/src/components/previews/PreviewPlayer.vue @@ -488,6 +488,14 @@
    +
    +
      +
    • + {{ tag.name }} +
    • +
    +
    +
    -
    Tag linked to Concept
    -
      -
    • tag1
    • +

      {{ $t('concepts.actions.title') }}

      +
        +
      • + {{ $t('concepts.actions.empty') }} +
      • +
    @@ -708,6 +718,47 @@
    +
    +
    +

    + {{ $t('breakdown.all_assets') }} +

    +
    + + +
    + + +
    +
    + import { mapGetters, mapActions } from 'vuex' + import { intersection } from '@/lib/array' import { sortPeople } from '@/lib/sorting' import func from '@/lib/func' @@ -733,13 +785,17 @@ import { PlayCircleIcon, TagIcon, TrashIcon, + Trash2Icon, UserIcon } from 'vue-feather-icons' + +import ButtonSimple from '@/components/widgets/ButtonSimple' import ComboboxModel from '@/components/widgets/ComboboxModel' import ComboboxStatus from '@/components/widgets/ComboboxStatus' import ComboboxStyled from '@/components/widgets/ComboboxStyled' import DeleteEntities from '@/components/tops/actions/DeleteEntities' import PeopleField from '@/components/widgets/PeopleField' +import SearchField from '@/components/widgets/SearchField' import Spinner from '@/components/widgets/Spinner' import ViewPlaylistModal from '@/components/modals/ViewPlaylistModal' @@ -755,6 +811,7 @@ export default { components: { AlertCircleIcon, + ButtonSimple, CheckSquareIcon, ComboboxModel, ComboboxStatus, @@ -766,10 +823,12 @@ export default { ImageIcon, PeopleField, PlayCircleIcon, + SearchField, Spinner, UserIcon, TagIcon, TrashIcon, + Trash2Icon, ViewPlaylistModal }, @@ -819,6 +878,7 @@ export default { taskDeletion: false, setThumbnails: false, shotDeletion: false, + tags: false, tasksSubscription: false }, errors: { @@ -835,6 +895,12 @@ export default { mounted() { this.customAction = this.defaultCustomAction this.setCurrentTeam() + + // FIXME: hack to init available tags + this.loading.tags = true + this.loadAssets(true).finally(() => { + this.loading.tags = false + }) }, beforeDestroy() { @@ -846,6 +912,7 @@ export default { 'allCustomActions', 'assetMap', 'assetCustomActions', + 'assetsByType', 'currentProduction', 'getPersonOptions', 'isCurrentUserArtist', @@ -889,6 +956,10 @@ export default { return 'episode' }, + currentConcept() { + return this.selectedConcepts.values().next().value + }, + defaultCustomAction() { if (this.customActions.length > 0) { return this.customActions[0] @@ -1069,13 +1140,25 @@ export default { if ( this.isCurrentViewAsset || this.isCurrentViewShot || - this.isCurrentViewEdit || - this.isCurrentViewConcept + this.isCurrentViewEdit ) { prefix = 'entities-' } + if (this.isCurrentViewConcept) prefix = 'concepts-' if (this.isCurrentViewTaskType) prefix = 'tasks-' return prefix + }, + + availableTagsByType() { + const assetGroups = [...this.assetsByType] + const result = assetGroups.map(assets => ({ + type: assets[0].asset_type_name, + tags: assets.map(asset => ({ + id: asset.id, + name: asset.name + })) + })) + return result } }, @@ -1096,6 +1179,7 @@ export default { 'changeSelectedTaskStatus', 'changeSelectedPriorities', 'clearSelectedTasks', + 'loadAssets', 'postCustomAction', 'setLastTaskPreview', 'subscribeToTask', @@ -1454,6 +1538,18 @@ export default { } else { this.availableTaskStatuses = this.taskStatusForCurrentUser } + }, + + removeTag(tag) { + // TODO: remove tag from concept + }, + + onSearchTagChange(query) { + // TODO: search in available tags + }, + + onSelectTag(tag) { + // TODO: link tag to concept } }, @@ -1708,4 +1804,79 @@ div.assignation { margin: auto; margin-top: 0.5em; } + +.tags { + display: inline-flex; + gap: 10px; + margin-left: 0; + height: 21px; + + .tag { + display: inline- flex; + gap: 1em; + border: 1px solid $light-green; + + .action { + background: $light-grey; + border-radius: 50%; + color: white; + cursor: pointer; + display: none; + height: 14px; + width: 14px; + line-height: 8px; + + &:hover { + background: $dark-grey-lighter; + } + } + + &:hover { + transform: scale(1.1); + + .action { + display: inline-block; + } + } + } +} + +.concept-tags { + overflow-y: auto; + padding: 1em; + background: $white; + border: 1px solid $white-grey; + box-shadow: 0 0 6px #e0e0e0; + border-radius: 1em; + + .subtitle { + margin-top: 0; + border-bottom: 0; + } + + .tag-types { + list-style: none; + margin-left: 0; + } + + .tag-type { + .subtitle { + text-transform: uppercase; + color: $grey; + border-bottom: 1px solid $light-grey; + font-size: 1.2em; + margin-top: 1em; + margin-bottom: 1em; + } + + .tag { + border-color: $light-grey; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } + } + } +} diff --git a/src/components/widgets/SearchQueryList.vue b/src/components/widgets/SearchQueryList.vue index 002071df34..0c042c6d60 100644 --- a/src/components/widgets/SearchQueryList.vue +++ b/src/components/widgets/SearchQueryList.vue @@ -87,9 +87,7 @@ @click="changeSearch(searchQuery)" v-for="searchQuery in userFilters" > - - {{ searchQuery.name }} - + {{ searchQuery.name }} diff --git a/src/locales/en.js b/src/locales/en.js index fc2aa47de3..830214e6eb 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -146,6 +146,10 @@ export default { created_at: 'Creation date', updated_at: 'Update date', // last_comment_date: 'Last comment', + }, + actions: { + title: 'Tags linked to Concept', + empty: 'No tags' } }, From c93c5652cbe9f864ba2d6f721cee849364f6e7df Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Fri, 8 Dec 2023 18:02:01 +0100 Subject: [PATCH 13/41] [concepts] list linked concepts in Asset view --- src/components/pages/Asset.vue | 97 ++++++++++++++++++++++++++++++++-- src/locales/en.js | 1 + 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/components/pages/Asset.vue b/src/components/pages/Asset.vue index 91fbeef32b..dae71d9c10 100644 --- a/src/components/pages/Asset.vue +++ b/src/components/pages/Asset.vue @@ -110,7 +110,7 @@
    @@ -256,6 +256,46 @@
    +
    + +
    + +
    + {{ $t('assets.no_concept') }} +
    +
    +
    +
    concept.status === this.currentConceptStatus + ) } }, @@ -564,6 +631,17 @@ export default { }) }, + conceptPath(concept) { + // TODO: add concept_id to the targeted route + return { + name: 'concepts', + params: { + production_id: this.currentProduction.id, + concept_id: concept.concept_id + } + } + }, + shotPath(shot) { return { name: shot.episode_id ? 'episode-shot' : 'shot', @@ -701,12 +779,17 @@ h2.subtitle { } .asset-list, -.shot-list { +.shot-list, +.concept-list { color: var(--text); display: flex; flex-wrap: wrap; } +.concept-list { + gap: 10px; +} + .asset-link, .shot-link { color: inherit; @@ -727,6 +810,14 @@ h2.subtitle { word-wrap: break-word; } +.concept-link { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + cursor: pointer; +} + .field-label { font-weight: bold; width: 120px; diff --git a/src/locales/en.js b/src/locales/en.js index 830214e6eb..e1236d69ff 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -18,6 +18,7 @@ export default { new_assets: 'Create assets', new_success: 'Asset {name} successfully created.', no_cast_in: 'This asset is not cast in any shot.', + no_concept: 'No concepts linked to this assets.', number: 'asset | assets', restore_text: 'Are you sure you want to restore {name} from your archive?', restore_error: 'An error occurred while restoring this asset.', From c22177396506c0a0f7174fe2c921f831aa613ef9 Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Mon, 11 Dec 2023 10:23:01 +0100 Subject: [PATCH 14/41] [concepts] fix task status edition --- src/components/lists/TaskStatusList.vue | 4 ++-- src/components/modals/EditTaskStatusModal.vue | 6 +++--- src/components/pages/Concepts.vue | 2 +- src/locales/en.js | 2 +- src/store/api/taskstatus.js | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/lists/TaskStatusList.vue b/src/components/lists/TaskStatusList.vue index 49008da30e..e37034d6b5 100644 --- a/src/components/lists/TaskStatusList.vue +++ b/src/components/lists/TaskStatusList.vue @@ -29,7 +29,7 @@ {{ $t('task_status.fields.is_feedback_request') }} - {{ $t('task_status.fields.is_concept') }} + {{ $t('task_status.fields.for_concept') }} @@ -55,7 +55,7 @@ class="is-feedback-request" :value="entry.is_feedback_request" /> - + @@ -224,7 +224,7 @@ export default { is_feedback_request: String( this.taskStatusToEdit.is_feedback_request || false ), - is_concept: String(this.taskStatusToEdit.is_concept || false), + for_concept: String(this.taskStatusToEdit.for_concept || false), archived: String(this.taskStatusToEdit.archived || false) } } diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue index 49389e989a..5d81dcc968 100644 --- a/src/components/pages/Concepts.vue +++ b/src/components/pages/Concepts.vue @@ -325,7 +325,7 @@ export default { } const conceptTaskStatusList = sortByName( Array.from(this.taskStatusMap.values()).filter( - status => status.is_concept + status => status.for_concept ) ) return [allStatusItem].concat(conceptTaskStatusList) diff --git a/src/locales/en.js b/src/locales/en.js index e1236d69ff..6fbd2455ec 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -1085,9 +1085,9 @@ export default { title: 'Task Status', fields: { color: 'Color', + for_concept: 'For concept', is_artist_allowed: 'Is artist allowed', is_client_allowed: 'Is client allowed', - is_concept: 'Is concept', is_done: 'Is done', is_feedback_request: 'Is feedback request', is_retake: 'Has retake value', diff --git a/src/store/api/taskstatus.js b/src/store/api/taskstatus.js index 0bc9f50a15..f1c6423baa 100644 --- a/src/store/api/taskstatus.js +++ b/src/store/api/taskstatus.js @@ -4,6 +4,7 @@ const sanitizeTaskStatus = taskStatus => { return { name: taskStatus.name, short_name: taskStatus.short_name, + for_concept: Boolean(taskStatus.for_concept === 'true'), is_default: Boolean(taskStatus.is_default === 'true'), is_done: Boolean(taskStatus.is_done === 'true'), is_retake: Boolean(taskStatus.is_retake === 'true'), From 71c6c5e36529ee5afaa18b69b6e1614da2a04f4a Mon Sep 17 00:00:00 2001 From: Nicolas Pennec Date: Mon, 11 Dec 2023 17:44:40 +0100 Subject: [PATCH 15/41] [concepts] fix UI for the new API data --- src/components/modals/EditCommentModal.vue | 21 +++-- src/components/pages/Concepts.vue | 81 +++++++++++-------- src/components/previews/PreviewPlayer.vue | 9 ++- src/components/sides/TaskInfo.vue | 16 +++- src/components/tops/ActionPanel.vue | 21 +++-- src/locales/en.js | 3 +- src/store/api/concepts.js | 9 ++- src/store/modules/concepts.js | 93 +++++++++++++++++----- src/store/modules/user.js | 29 ------- 9 files changed, 180 insertions(+), 102 deletions(-) diff --git a/src/components/modals/EditCommentModal.vue b/src/components/modals/EditCommentModal.vue index ffa0672aeb..205c90aa58 100644 --- a/src/components/modals/EditCommentModal.vue +++ b/src/components/modals/EditCommentModal.vue @@ -14,9 +14,9 @@

    - @@ -157,7 +157,7 @@ import { XIcon } from 'vue-feather-icons' import AtTa from 'vue-at/dist/vue-at-textarea' import Checklist from '@/components/widgets/Checklist' -import ComboBoxStatus from '@/components/widgets/ComboboxStatus.vue' +import ComboboxStatus from '@/components/widgets/ComboboxStatus.vue' import FileUpload from '@/components/widgets/FileUpload' import ModalFooter from '@/components/modals/ModalFooter' import PeopleAvatar from '@/components/widgets/PeopleAvatar' @@ -168,7 +168,7 @@ export default { components: { AtTa, Checklist, - ComboBoxStatus, + ComboboxStatus, FileUpload, ModalFooter, PeopleAvatar, @@ -228,7 +228,18 @@ export default { 'isCurrentUserClient', 'productionDepartmentIds', 'taskStatusForCurrentUser' - ]) + ]), + + taskStatuses() { + return this.taskStatusForCurrentUser.filter( + status => Boolean(status.for_concept) === this.isConceptTask + ) + }, + + isConceptTask() { + // FIXME: write correct logic + return this.$route.path.includes('concept') + } }, methods: { diff --git a/src/components/pages/Concepts.vue b/src/components/pages/Concepts.vue index 5d81dcc968..a7f67cc251 100644 --- a/src/components/pages/Concepts.vue +++ b/src/components/pages/Concepts.vue @@ -31,6 +31,7 @@ :label="$t('concepts.fields.entity_type')" :options="entityTypeOptions" v-model="filters.entityType" + disabled /> {{ $t('concepts.title') }} ({{ filteredConcepts?.length || 0 }}) - - -
    +
    -
    +
    - {{ taskStatusMap?.get(concept.status).short_name }} + {{ getTaskStatus(concept).short_name }}
    -
    +
    + {{ $t('concepts.empty') }} +
    +
    ({ + return ['created_at', 'updated_at', 'last_comment_date'].map(name => ({ label: name, value: name })) @@ -288,28 +285,27 @@ export default { filteredConcepts() { let concepts = [...this.displayedConcepts] + if (this.filters.taskStatusId) { concepts = concepts.filter( - concept => concept.status === this.filters.taskStatusId + concept => + concept.tasks[0].task_status_id === this.filters.taskStatusId ) } if (this.filters.assignee) { concepts = concepts.filter(concept => - concept.tasks[0].assignees?.includes(this.filters.assignee.id) + concept.tasks[0].assignees.includes(this.filters.assignee.id) ) } if (this.filters.entityType) { concepts = concepts.filter(concept => - concept.entities?.some( + concept.tags?.some( // FIXME: condition related to many-to-many relationship entity => entity.type === this.filters.entityType ) ) } - - return concepts.sort( - firstBy(this.filters.sortBy, -1).thenBy('created_at') - ) + return concepts.sort(firstBy(this.filters.sortBy, -1)) }, searchField() { @@ -389,6 +385,14 @@ export default { } }, + getTaskStatus(concept) { + return this.taskStatusMap.get(concept.tasks[0].task_status_id) + }, + + hasTask(concept) { + return concept.tasks?.length + }, + isSelected(concept) { return this.selectedConcepts.has(concept.id) }, @@ -428,9 +432,9 @@ export default { async confirmAddConceptModal(forms) { this.loading.addingConcept = true - const file = forms[0].get('file') + const form = forms[0] try { - await this.newConcept({ file }) + await this.newConcept({ form }) this.closeAddConceptModal() } catch (err) { console.error(err) @@ -441,7 +445,13 @@ export default { } }, - watch: {}, + watch: { + currentProduction() { + this.clearSelectedConcepts() + this.clearSelectedTasks() + this.refreshConcepts() + } + }, metaInfo() { return { @@ -471,6 +481,10 @@ export default { } } +.concept-list { + overflow-x: auto; +} + .items { cursor: pointer; display: flex; @@ -478,6 +492,7 @@ export default { gap: 20px; list-style: none; margin: 0; + width: 100vw; .item { width: 300px; @@ -492,6 +507,7 @@ export default { &.selected-item { background-color: var(--background-selected); } + &:hover { background-color: var(--background-hover); } @@ -503,12 +519,10 @@ export default { row-gap: 10px; padding: 0.3em 1em; margin: 0.3em 0; - // color: var(--text-strong); - // font-weight: 500; - // text-transform: uppercase; } .tags { + cursor: pointer; display: inline-flex; gap: 10px; margin-left: 0; @@ -519,6 +533,11 @@ export default { } } + .tag { + font-weight: 500; + letter-spacing: 1px; + } + .status { display: flex; justify-content: space-between; @@ -533,11 +552,6 @@ export default { gap: 5px; } } - - .tag { - font-weight: 500; - letter-spacing: 1px; - } } } @@ -547,6 +561,5 @@ export default { display: flex; justify-content: center; padding: 3em; - margin-top: 3em; } diff --git a/src/components/previews/PreviewPlayer.vue b/src/components/previews/PreviewPlayer.vue index 61d591d4ac..1457ffe3b1 100644 --- a/src/components/previews/PreviewPlayer.vue +++ b/src/components/previews/PreviewPlayer.vue @@ -100,6 +100,7 @@ :silent="isCommentsHidden" :current-frame="currentFrame" :current-parent-preview="currentPreview" + :entity-type="entityType" @comment-added="$emit('comment-added')" @time-code-clicked="timeCodeClicked" v-show="!isCommentsHidden" @@ -488,7 +489,7 @@
    -
    +
    • {{ tag.name }} @@ -619,6 +620,9 @@ export default { extraWide: { type: Boolean, default: false + }, + entityType: { + type: String } }, @@ -821,8 +825,7 @@ export default { }, isConcept() { - // FIXME: write correct logic - return this.$route.path.includes('concept') + return this.entityType === 'Concept' }, isPicture() { diff --git a/src/components/sides/TaskInfo.vue b/src/components/sides/TaskInfo.vue index 836b45df6e..1cba534713 100644 --- a/src/components/sides/TaskInfo.vue +++ b/src/components/sides/TaskInfo.vue @@ -45,12 +45,12 @@
      -
      +
      @@ -91,6 +91,7 @@