From 4a09f886a62fc9bf4dfdc3ea6d2c06f40ca762cc Mon Sep 17 00:00:00 2001 From: Ryan Coulson Date: Wed, 24 Apr 2024 14:57:59 -0400 Subject: [PATCH] add ability to change product UUID --- package-lock.json | 27 ++ package.json | 2 + src/components/editor/metadata-editor.vue | 373 ++++++++++++++++------ src/lang/lang.csv | 10 +- 4 files changed, 315 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index b16d81948..a4d8cd149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "markdown-it": "^12.0.6", "nouislider": "^15.5.0", "ramp-config-editor_editeur-config-pcar": "^1.1.8", + "throttle-debounce": "^5.0.0", "uuid": "^9.0.0", "vue": "^3.3.4", "vue-class-component": "^8.0.0-rc.1", @@ -40,6 +41,7 @@ "@types/clone-deep": "^4.0.1", "@types/file-saver": "^2.0.5", "@types/markdown-it": "^12.0.1", + "@types/throttle-debounce": "^5.0.2", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", "@vue/cli-plugin-babel": "^4.5.19", @@ -2496,6 +2498,12 @@ "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==", "dev": true }, + "node_modules/@types/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==", + "dev": true + }, "node_modules/@types/uglify-js": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", @@ -18258,6 +18266,14 @@ "node": ">=4.0.0" } }, + "node_modules/throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -22910,6 +22926,12 @@ "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==", "dev": true }, + "@types/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==", + "dev": true + }, "@types/uglify-js": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", @@ -35027,6 +35049,11 @@ } } }, + "throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index f93f90c10..8724dd95d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "markdown-it": "^12.0.6", "nouislider": "^15.5.0", "ramp-config-editor_editeur-config-pcar": "^1.1.8", + "throttle-debounce": "^5.0.0", "uuid": "^9.0.0", "vue": "^3.3.4", "vue-class-component": "^8.0.0-rc.1", @@ -40,6 +41,7 @@ "@types/clone-deep": "^4.0.1", "@types/file-saver": "^2.0.5", "@types/markdown-it": "^12.0.1", + "@types/throttle-debounce": "^5.0.2", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", "@vue/cli-plugin-babel": "^4.5.19", diff --git a/src/components/editor/metadata-editor.vue b/src/components/editor/metadata-editor.vue index 90b28dff5..629a1a263 100644 --- a/src/components/editor/metadata-editor.vue +++ b/src/components/editor/metadata-editor.vue @@ -11,99 +11,186 @@ {{ configLang === 'en' ? $t('editor.frenchConfig') : $t('editor.englishConfig') }} - -
- -
+
+
+ -
+ + + +
+ +
+
+
+ +
+ +
+
    +
  • + {{ storyline.uuid }} +
  • +
+
+
+ + + + + + +
+ +
+ +
  • - {{ storyline.uuid }} + {{ formatDate(history.created) }}
+
- - - - - +
+ + +
+ + + + + + {{ + $t(`editor.warning.${warning}`) + }}
- {{ $t('editor.uuid.exists') }} - - - -
- -
-
-
    -
  • + +
    + + + + + + {{ + $t('editor.changingUuid', { + changeUuid: renamed + }) + }}
    +
    + + + +
    +
  • -
- + {{ renaming ? $t('editor.cancel') : $t('editor.changeUuid') }} + +

@@ -130,14 +217,18 @@
@@ -205,6 +296,7 @@ import { Options, Prop, Vue } from 'vue-property-decorator'; import { RouteLocationNormalized } from 'vue-router'; import { AxiosResponse } from 'axios'; +import { throttle } from 'throttle-debounce'; import { AudioPanel, BasePanel, @@ -215,6 +307,7 @@ import { ImagePanel, MapPanel, MetadataContent, + PanelType, Slide, SlideshowPanel, SourceCounts, @@ -280,9 +373,10 @@ export default class MetadataEditorV extends Vue { loadExisting = false; reloadExisting = false; loadStatus = 'waiting'; + checkingUuid = false; loadEditor = false; error = false; // whether an error has occurred - warning = false; // used for duplicate uuid warning + warning: 'none' | 'uuid' | 'rename' = 'none'; // used for duplicate uuid warning configLang = 'en'; showDropdown = false; @@ -297,6 +391,10 @@ export default class MetadataEditorV extends Vue { // Form properties. uuid = ''; + baseUuid = ''; // to save the original UUID + changeUuid = ''; + renaming = false; + renamed = ''; logoImage: undefined | File = undefined; metadata: MetadataContent = { title: '', @@ -456,6 +554,12 @@ export default class MetadataEditorV extends Vue { */ generateRemoteConfig(): void { this.loadStatus = 'loading'; + + // Reset fields + this.baseUuid = this.uuid; + this.renamed = ''; + this.changeUuid = ''; + // Attempt to fetch the project from the server. fetch(this.apiUrl + `/retrieve/${this.uuid}`) .then((res: Response) => { @@ -562,6 +666,80 @@ export default class MetadataEditorV extends Vue { } } + async renameProduct(): Promise { + if (!this.configFileStructure) { + return; + } + + // Fetch the two existing configuration files. + const enFile = this.configFileStructure?.zip.file(`${this.uuid}_en.json`); + const frFile = this.configFileStructure?.zip.file(`${this.uuid}_fr.json`); + + if (enFile && frFile) { + // Remove the files from the ZIP folder. + this.configFileStructure?.zip.remove(enFile.name); + this.configFileStructure?.zip.remove(frFile.name); + + // Fetch the contents of the two files, and perform a find/replace on the UUID for each source. + const englishConfig = await enFile?.async('string').then((res: string) => JSON.parse(res)); + const frenchConfig = await frFile?.async('string').then((res: string) => JSON.parse(res)); + [englishConfig, frenchConfig].forEach((config) => this.renameSources(config)); + + // Convert the configs back into a string and re-add them to the ZIP with the new UUID. + this.configFileStructure?.zip.file(`${this.changeUuid}_en.json`, JSON.stringify(englishConfig, null, 4)); + this.configFileStructure?.zip.file(`${this.changeUuid}_fr.json`, JSON.stringify(frenchConfig, null, 4)); + + this.uuid = this.changeUuid; + + // Reset source counts and re-generate the config file structure. + this.sourceCounts = {}; + this.configFileStructureHelper(this.configFileStructure.zip); + } + + this.renaming = false; + this.renamed = this.uuid; + } + + // Given a Storylines config, replace instances of the current UUID with a new UUID. + renameSources(config: StoryRampConfig): void { + const _renameHelper = (panel: any): any => { + switch (panel.type) { + case PanelType.Dynamic: + (panel as DynamicPanel).children.forEach((child) => { + _renameHelper(child.panel); + }); + break; + case PanelType.Slideshow: + (panel as SlideshowPanel).items.forEach((child) => { + _renameHelper(child); + }); + break; + default: + // Base case. This is a panel that doesn't have any children (i.e., not dynamic, slideshow). + // Rename the source. + if (panel.src) { + panel.src = panel.src.replace(`${this.uuid}/`, `${this.changeUuid}/`); + } + if (panel.config && typeof panel.config === 'string') { + panel.config = panel.config.replace(`${this.uuid}/`, `${this.changeUuid}/`); + } + } + }; + + if (config?.introSlide.logo?.src) { + config.introSlide.logo.src = config.introSlide.logo.src.replace( + `${this.uuid}/assets/`, + `${this.changeUuid}/assets/` + ); + } + + config.slides.forEach((slide) => { + slide.panel.forEach((panel) => { + _renameHelper(panel); + }); + }); + } + findSources(configs: { [key: string]: StoryRampConfig | undefined }): void { ['en', 'fr'].forEach((lang) => { if (configs[lang]?.introSlide.logo?.src) { @@ -578,34 +756,34 @@ export default class MetadataEditorV extends Vue { panelSourceHelper(panel: BasePanel): void { switch (panel.type) { - case 'dynamic': + case PanelType.Dynamic: (panel as DynamicPanel).children.forEach((subPanel: DynamicChildItem) => { this.panelSourceHelper(subPanel.panel); }); break; - case 'slideshow': + case PanelType.Slideshow: (panel as SlideshowPanel).items.forEach((item: ChartPanel | TextPanel | ImagePanel | MapPanel) => { this.panelSourceHelper(item); }); break; - case 'chart': + case PanelType.Chart: this.incrementSourceCount((panel as ChartPanel).src); break; - case 'image': + case PanelType.Image: this.incrementSourceCount((panel as ImagePanel).src); break; - case 'video': + case PanelType.Video: if ((panel as VideoPanel).videoType === 'local') { this.incrementSourceCount((panel as VideoPanel).src); } break; - case 'audio': + case PanelType.Audio: this.incrementSourceCount((panel as AudioPanel).src); break; - case 'map': + case PanelType.Map: this.incrementSourceCount((panel as MapPanel).config); break; - case 'text': + case PanelType.Text: break; default: break; @@ -678,7 +856,7 @@ export default class MetadataEditorV extends Vue { return; } - if (this.loadExisting) { + if (this.loadExisting && !this.renamed) { this.loadStatus = 'waiting'; Message.success('Successfully loaded storyline!'); } else { @@ -934,13 +1112,18 @@ export default class MetadataEditorV extends Vue { } } - checkUuid(): void { - if (!this.loadExisting) { - fetch(this.apiUrl + `/retrieve/${this.uuid}`).then((res: Response) => { + checkUuid = throttle(300, (rename?: boolean): void => { + if (rename) this.checkingUuid = true; + + if (!this.loadExisting || rename) { + // If renaming, show the loading spinner while we check whether the UUID is taken. + fetch(this.apiUrl + `/retrieve/${rename ? this.changeUuid : this.uuid}`).then((res: Response) => { if (res.status !== 404) { - this.warning = true; + this.warning = rename ? 'rename' : 'uuid'; } + if (rename) this.checkingUuid = false; + fetch(this.apiUrl + `/retrieveMessages`) .then((res: any) => { if (res.ok) return res.json(); @@ -955,8 +1138,8 @@ export default class MetadataEditorV extends Vue { .catch((error: any) => console.log(error.response || error)); }); } - this.warning = false; - } + this.warning = 'none'; + }); /** * React to param changes in URL. diff --git a/src/lang/lang.csv b/src/lang/lang.csv index 0ca79ec5f..34ffbff4e 100644 --- a/src/lang/lang.csv +++ b/src/lang/lang.csv @@ -29,8 +29,12 @@ editor.editProduct,Edit Existing Storylines Product,1,Modifier un produit de sc editor.editMetadata,Edit Project Metadata,1,Modifier les métadonnées d’un projet,1 editor.productDetails,Storylines product details,1,Détails du produit de scénarios,1 editor.metadata.instructions,Fill in metadata details about your new Storylines product. Use the "Preview" button to see what your slides will look like.,1,Inscrivez les métadonnées de votre nouveau produit de scénario. Utilisez la fonction « Afficher l’aperçu » pour voir à quoi ressemblent vos diapositives.,1 -editor.uuid,UUID,1,IDUU,1 -editor.uuid.exists,UUID already exists. Saving this will overwrite existing product.,1,L’IDUU existe déjà. Enregistrer ce produit écrasera le produit existant.,1 +editor.uuid,UUID,1,UUID,1 +editor.uuid.new,New UUID,1,New UUID,0 +editor.warning.uuid,UUID already exists. Saving this will overwrite existing product.,1,L’IDUU existe déjà. Enregistrer ce produit écrasera le produit existant.,1 +editor.warning.rename,UUID already in use. Please choose a different ID.,1,UUID déjà utilisé. Veuillez choisir un autre identifiant.,0 +editor.changeUuid,Click here to change UUID,1,Click here to change UUID,0 +editor.changingUuid,You are changing this product UUID to {changeUuid}. Save changes required.,1,You are changing this product UUID to {changeUuid}. Save changes required.,0 editor.title,Title,1,Titre,1 editor.logo,Logo,1,Logo,1 editor.logoPreview,Logo Preview,1,Aperçu du logo,1 @@ -44,6 +48,7 @@ editor.dateModified,Date Modified,1,Date de modification,1 editor.load,Load,1,Charger,1 editor.loadPrevious,Load Previous,1,[FR] Load Previous,0 editor.viewHistory,View Previous,1,[FR] View Previous,0 +editor.rename,Rename,1,Rename,0 editor.browse,Browse,1,Parcourir,1 editor.remove,Remove,1,Supprimer,1 editor.back,Back,1,Retour,1 @@ -57,6 +62,7 @@ editor.label.or,or,1,ou,1 editor.label.browse,browse,1,parcourir,1 editor.label.upload,to upload,1,téléverser,1 editor.savingChanges,Saving...,1,Enregistrement...,1 +editor.confirmOverwrite,Are you sure you want to overwrite product '{uuid}'?,1,Are you sure you want to overwrite product '{uuid}'?,0 editor.resetChanges,Reset Changes,1,Annuler les modifications,1 editor.refreshChanges.modal,"Are you sure you want to reload the product? All unsaved changes will be lost.",1,"Voulez-vous vraiment recharger ce produit? Toute modification non enregistrée sera perdue.",1 editor.changeLang.modal,"Are you sure you want to switch languages? Unsaved changes may be lost.",1,"Voulez-vous vraiment changer de langue? Toute modification non enregistrée sera perdue.",1