diff --git a/server/index.js b/server/index.js index 399ae2ec..acaad24e 100644 --- a/server/index.js +++ b/server/index.js @@ -45,7 +45,7 @@ app.use(cors()); // POST requests made to /upload will be handled here. app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) { // TODO: Putting this on the header isn't great. The body has the zipped folder. And usernames in the URL doesn't look great either. Maybe improve this somehow. - const user = req.headers.user + const user = req.headers.user; if (!user) { // Send back error if the user uploading the storyline was not provided. responseMessages.push({ @@ -72,11 +72,7 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) { // Upload the file to the server, into the /files/ folder. form.parse(req, async function (err, field, file) { if (err) { - responseMessages.push({ - type: 'WARNING', - message: 'Upload Aborted: an error has occurred while parsing the uploaded form: ' + err - }); - logger('WARNING', 'Upload Aborted: an error has occurred while parsing the uploaded form: ', err); + logger('WARNING', 'Upload Aborted: an error has occurred while parsing the uploaded form: ' + err); res.status(500).send({ status: 'Internal Server Error' }); return; } @@ -90,10 +86,6 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) { //if (!projectNameRegex.test(fileName)) { // SECURITY FEATURE (temporary): Make sure the project name isn't `scripts`, or `help`, and doesn't contain . or / in order to prevent overwriting folders. if (fileName !== 'scripts' && fileName !== 'help' && !fileName.includes('/') && !fileName.includes('.')) { - responseMessages.push({ - type: 'WARNING', - message: 'Upload Aborted: file does not match Storylines UUID standards.' - }); logger('WARNING', 'Upload Aborted: file does not match Storylines UUID standards.'); // Delete the uploaded zip file. safeRM(secureFilename, UPLOAD_PATH); @@ -103,12 +95,10 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) { // Before unzipping, create the product folder in /public/ if it doesn't exist already. if (!fs.existsSync(fileName)) { - responseMessages.push({ type: 'INFO', message: `Successfully created new product ${fileName}` }); logger('INFO', `Successfully created new product ${fileName}`); fs.mkdirSync(fileName); newStorylines = true; } - // Unzip the contents of the uploaded zip file into the target directory. Will overwrite // old files in the folder. decompress(secureFilename, fileName).then(async () => { @@ -118,15 +108,13 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) { // files.forEach((file) => { // validateFile(file, fileName); // }); - responseMessages.push({ type: 'INFO', message: `Uploaded files to product ${fileName}` }); logger('INFO', `Uploaded files to product ${fileName}`); // Initialize a new git repo if this is a new storyline. // Otherwise, simply create a new commit with the zipped folder. if (!newStorylines) { - await commitToRepo(fileName, user, false) - } - else { - await initGitRepo(fileName, user) + await commitToRepo(fileName, user, false); + } else { + await initGitRepo(fileName, user); } // Finally, delete the uploaded zip file. safeRM(secureFilename, UPLOAD_PATH); @@ -143,10 +131,10 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) { // GET requests made to /retrieve/ID/commitHash will be handled here. // Calling this with commitHash as "latest" simply fetches the product as normal. app.route(ROUTE_PREFIX + '/retrieve/:id/:hash').get(function (req, res, next) { - // This user is only needed for backwards compatibility. + // This user is only needed for backwards compatibility. // If we have an existing storylines product that is not a git repo, we need to initialize a git repo // and make an initial commit for it, but we need the user for the commit. - const user = req.headers.user + const user = req.headers.user; if (!user) { // Send back error if the user uploading the storyline was not provided. responseMessages.push({ @@ -161,7 +149,7 @@ app.route(ROUTE_PREFIX + '/retrieve/:id/:hash').get(function (req, res, next) { var archive = archiver('zip'); const PRODUCT_PATH = `${TARGET_PATH}/${req.params.id}`; const uploadLocation = `${UPLOAD_PATH}/${req.params.id}-outgoing.zip`; - const commitHash = req.params.hash + const commitHash = req.params.hash; // Check if the product exists. if ( @@ -169,27 +157,26 @@ app.route(ROUTE_PREFIX + '/retrieve/:id/:hash').get(function (req, res, next) { if (!error) { // Backwards compatibility. If the existing product is not a git repo i.e. it existed before git version control, // we make a git repo for it before returning the version history. Otherwise, the code below will explode. - await initGitRepo(PRODUCT_PATH, user) + await initGitRepo(PRODUCT_PATH, user); const git = simpleGit(PRODUCT_PATH); // Get the current branch. We do it this way instead of assuming its "main" in case someone has it set to master. - const branches = await git.branchLocal() - const currBranch = branches.current + const branches = await git.branchLocal(); + const currBranch = branches.current; if (commitHash !== 'latest') { - // If the user does not ask for the latest commit, we checkout a new branch at the point of the requested commit, + // If the user does not ask for the latest commit, we checkout a new branch at the point of the requested commit, // and then proceed with getting the zipped folder below. try { // First, we check if the requested commit exists. // NOTE: When calling from frontend, the catch block should never run. const commitExists = await git.catFile(['-t', commitHash]); if (commitExists !== 'commit\n') { - throw new Error() + throw new Error(); } } catch (error) { - responseMessages.push({ - type: 'INFO', - message: `Access attempt to version ${commitHash} of product ${req.params.id} failed, does not exist.` - }); - logger('INFO', `Access attempt to version ${commitHash} of product ${req.params.id} failed, does not exist.`); + logger( + 'INFO', + `Access attempt to version ${commitHash} of product ${req.params.id} failed, does not exist.` + ); res.status(404).send({ status: 'Not Found' }); return; } @@ -219,10 +206,9 @@ app.route(ROUTE_PREFIX + '/retrieve/:id/:hash').get(function (req, res, next) { // Since the user has not asked for the latest commit, we need to clean up. // Go back to the main branch and delete the newly created branch. await git.checkout(currBranch); - await git.deleteLocalBranch(`version-${commitHash}`) + await git.deleteLocalBranch(`version-${commitHash}`); } }); - }); // Write the product data to the ZIP file. @@ -233,14 +219,8 @@ app.route(ROUTE_PREFIX + '/retrieve/:id/:hash').get(function (req, res, next) { }); archive.finalize(); - responseMessages.push({ type: 'INFO', message: `Successfully loaded product ${req.params.id}` }); logger('INFO', `Successfully loaded product ${req.params.id}`); - } else { - responseMessages.push({ - type: 'INFO', - message: `Access attempt to ${req.params.id} failed, does not exist.` - }); logger('INFO', `Access attempt to ${req.params.id} failed, does not exist.`); res.status(404).send({ status: 'Not Found' }); } @@ -260,29 +240,17 @@ app.route(ROUTE_PREFIX + '/retrieve/:id/:lang').get(function (req, res) { if (!err) { // return JSON config file as response const configJson = JSON.parse(data.toString()); - responseMessages.push({ - type: 'INFO', - message: `Successfully loaded config file for ${req.params.id}, language ${req.params.lang}` - }); logger( 'INFO', `Successfully loaded config file for ${req.params.id}, language ${req.params.lang}` ); res.json(configJson); } else { - responseMessages.push({ - type: 'INFO', - message: `Access attempt to ${req.params.id} failed, error status ${err.status}` - }); logger('INFO', `Access attempt to ${req.params.id} failed, error status ${err.status}`); res.status(err.status); } }); } else { - responseMessages.push({ - type: 'INFO', - message: `Access attempt to ${req.params.id} failed, does not exist.` - }); logger('INFO', `Access attempt to ${req.params.id} failed, does not exist.`); res.status(404).send({ status: 'Not Found' }); } @@ -291,10 +259,10 @@ app.route(ROUTE_PREFIX + '/retrieve/:id/:lang').get(function (req, res) { }); app.route(ROUTE_PREFIX + '/history/:id').get(function (req, res, next) { - // This user is only needed for backwards compatibility. + // This user is only needed for backwards compatibility. // If we have an existing storylines product that is not a git repo, we need to initialize a git repo // and make an initial commit for it, but we need the user for the commit. - const user = req.headers.user + const user = req.headers.user; if (!user) { // Send back error if the user uploading the storyline was not provided. responseMessages.push({ @@ -316,23 +284,119 @@ app.route(ROUTE_PREFIX + '/history/:id').get(function (req, res, next) { }); logger('INFO', `Access attempt to versions of ${req.params.id} failed, does not exist.`); res.status(404).send({ status: 'Not Found' }); - } - else { + } else { // Backwards compatibility. If the existing product is not a git repo i.e. it existed before git version control, // we make a git repo for it before returning the version history. Otherwise, the code below will explode. - await initGitRepo(PRODUCT_PATH, user) + await initGitRepo(PRODUCT_PATH, user); // Get version history for this product via git log command const git = simpleGit(PRODUCT_PATH); - const log = await git.log() + const log = await git.log(); // TODO: Remove the 10 version limit once pagination is implemented - const history = log.all.slice(0, 10).map((commit) => ({hash: commit.hash, created: commit.date, storylineUUID: req.params.id})) - res.json(history) + const history = log.all.slice(0, 10).map((commit) => ({ + hash: commit.hash, + created: commit.date, + storylineUUID: req.params.id + })); + res.json(history); + } + }); +}); + +/** + * Handles renaming of products. Payload should contain both the old and the new UUID, as well as the newly updated configs. + */ +app.route(ROUTE_PREFIX + '/rename').post(function (req, res) { + const oldId = req.body.previousUuid; + const newId = req.body.newUuid; + const user = req.body.user; + const configs = req.body.configs; + const PRODUCT_PATH = `${TARGET_PATH}/${oldId}`; // the existing product path + const NEW_PATH = `${TARGET_PATH}/${newId}`; + + if (!oldId || !newId) { + res.status(400).send({ status: 'Bad Request' }); + logger('INFO', 'Rename attempt failed. Payload was missing old or new UUID.'); + return; + } + + // Check if the old product exists. + fs.access(PRODUCT_PATH, (error) => { + if (error) { + res.status(400).send({ status: 'Bad Request' }); + logger('INFO', 'Rename attempt failed. Provided old UUID does not exist in file system.'); + return; + } else { + // Check to see if the new UUID is already in use (this is also checked on the front-end, but we should double check). + fs.access(NEW_PATH, (error) => { + if (error) { + if (error.code === 'ENOENT') { + // file does not exist, so we can rename the product. + fs.rename(PRODUCT_PATH, NEW_PATH, async (err) => { + if (err) { + res.status(500).send({ status: 'Internal Server Error' }); + logger('WARNING', 'Error occured while renaming a Storylines product.' + err); + return; + } else { + // Delete the two previous configuration files. + fs.rmSync(NEW_PATH + `/${oldId}_en.json`); + fs.rmSync(NEW_PATH + `/${oldId}_fr.json`); + + // Delete and then re-initialize the Git repo to remove previous history. + fs.rmSync(NEW_PATH + `/.git`, { recursive: true, force: true }); + const git = simpleGit(NEW_PATH); + await git.init(); + + // Create the new config files and commit them. + fs.writeFileSync(NEW_PATH + `/${newId}_en.json`, configs.en); + fs.writeFileSync(NEW_PATH + `/${newId}_fr.json`, configs.fr); + await commitToRepo(NEW_PATH, user, true); + + res.status(200).send({ status: 'OK' }); + logger('INFO', `Product successfully renamed product from ${oldId} to ${newId}`); + return; + } + }); + } else { + res.status(500).send({ status: 'Internal Server Error' }); + logger('WARNING', 'Error occured while renaming a Storylines product.', error); + return; + } + } else { + res.status(400).send({ status: 'Bad Request' }); + logger('INFO', 'Rename attempt failed. Provided new UUID already exists in file system.'); + return; + } + }); } - }) + }); +}); -}) +/** + * Checks to see if the provided UUID already exists. + */ +app.route(ROUTE_PREFIX + '/check/:id').get(function (req, res) { + const PRODUCT_PATH = `${TARGET_PATH}/${req.params.id}`; + fs.access(PRODUCT_PATH, (err) => { + if (err) { + if (err.code === 'ENOENT') { + // file does not exist. We won't log this to the console because this endpoint + // may be hit quite often. + res.status(404).send({ status: 'Not Found' }); + return; + } else { + res.status(500).send({ status: err.code }); + logger('WARNING', 'Product check failed with non-ENOENT error: ', err); + return; + } + } else { + // If the file system access request succeeds, the folder already exists. + res.status(200).send({ status: 'Found' }); + return; + } + }); +}); -// GET reuests made to /retrieveMessages will recieve all the responseMessages currently queued. +// GET requests made to /retrieveMessages will recieve all the responseMessages currently queued. app.route(ROUTE_PREFIX + '/retrieveMessages').get(function (req, res) { res.json({ messages: responseMessages }); responseMessages.length = 0; @@ -341,7 +405,7 @@ app.route(ROUTE_PREFIX + '/retrieveMessages').get(function (req, res) { /* * Initializes a git repo at the requested path, if one does not already exist. * Creates an initial commit with any currently existing files in the directory. - * + * * @param {string} path the path of the git repo * @param {string} username the name of the user initializing the repo */ @@ -357,16 +421,16 @@ async function initGitRepo(path, username) { // Product directory is in a git repo but not top-level, we are working locally. repoExists = false; } - } catch(error) { + } catch (error) { // Product directory is not a git repo nor is it within a git repo. repoExists = false; } if (!repoExists) { - // Repo does not exist for the storyline product. + // Repo does not exist for the storyline product. // Initialize a git repo and add an initial commit with all existing files. - await git.init() - await commitToRepo(path, username, true) + await git.init(); + await commitToRepo(path, username, true); } } @@ -378,20 +442,22 @@ async function initGitRepo(path, username) { * @param {boolean} initial specifies whether this is the initial commit */ async function commitToRepo(path, username, initial) { - const date = moment().format('YYYY-MM-DD') - const time = moment().format('hh:mm:ss a') + const date = moment().format('YYYY-MM-DD'); + const time = moment().format('hh:mm:ss a'); // Initialize git const git = simpleGit(path); - let versionNumber = 1 + let versionNumber = 1; if (!initial) { // Compute version number for storyline if this is not the initial commit. - const log = await git.log() - const lastMessage = log.latest.message - versionNumber = lastMessage.split(' ')[3] + const log = await git.log(); + const lastMessage = log.latest.message; + versionNumber = lastMessage.split(' ')[3]; versionNumber = Number(versionNumber) + 1; } // Commit the files for this storyline to its repo. - await git.add('./*').commit(`Add product version ${versionNumber} on ${date} at ${time}`, {'--author': `"${username} <>"`}) + await git.add('./*').commit(`Add product version ${versionNumber} on ${date} at ${time}`, { + '--author': `"${username} <>"` + }); } /* @@ -432,6 +498,9 @@ function safeRM(path, folder) { function logger(type, message) { let currentDate = moment().format('MM/DD/YYYY HH:mm:ss a'); + // Push to responseMessages. + responseMessages.push({ type: type, message: message }); + // Append to log file. logFile.write(`${currentDate} [${type}] ${message}\n`); diff --git a/src/components/metadata-editor.vue b/src/components/metadata-editor.vue index a77822f5..7f2b51b1 100644 --- a/src/components/metadata-editor.vue +++ b/src/components/metadata-editor.vue @@ -59,12 +59,12 @@ @@ -76,8 +76,8 @@
-
- +
+ {{ $t('editor.rename') }} -
+
@@ -144,7 +149,7 @@ :class="{ 'input-error': error || !reqFields.uuid, 'input-success': loadStatus === 'loaded', - 'input-warning': warning !== 'none' || (!renaming && renamed) + 'input-warning': warning !== 'none' }" v-tippy="{ content: $t('editor.editMetadata.input.tooltip'), @@ -248,41 +253,6 @@

- - - - - - - - - -
- {{ - $t('editor.changingUuid', { - changeUuid: renamed - }) - }} - -
-
-
@@ -659,9 +629,9 @@ export default class MetadataEditorV extends Vue { // Form properties. uuid = ''; baseUuid = ''; // to save the original UUID - changeUuid = ''; - renaming = false; - renamed = ''; + changeUuid = ''; // the model for the rename input + renameMode = false; // if currently in rename mode + processingRename = false; // only true while we're waiting for the server to process a rename logoImage: undefined | File = undefined; metadata: MetadataContent = { title: '', @@ -981,7 +951,6 @@ export default class MetadataEditorV extends Vue { // Reset fields this.baseUuid = this.uuid; - this.renamed = ''; this.changeUuid = ''; // Attempt to fetch the project from the server. @@ -1041,46 +1010,64 @@ export default class MetadataEditorV extends Vue { return; } + const prevUuid = this.uuid; + const userStore = useUserStore(); + // 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) { - axios - .post(process.env.VUE_APP_NET_API_URL + '/api/version/update', { - uuid: this.uuid, - changeUuid: this.changeUuid + this.processingRename = true; + // Remove the current configuration files from the ZIP folder. + this.configFileStructure?.zip.remove(enFile.name); + this.configFileStructure?.zip.remove(frFile.name); + + // Fetch the contents of the two configuration 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 two configuration files into string format. + const convertedEnglish = JSON.stringify(englishConfig, null, 4); + const convertedFrench = JSON.stringify(frenchConfig, null, 4); + + // Re-add the configuration files to the ZIP with the new UUID. + this.configFileStructure?.zip.file(`${this.changeUuid}_en.json`, convertedEnglish); + this.configFileStructure?.zip.file(`${this.changeUuid}_fr.json`, convertedFrench); + + this.uuid = this.changeUuid; + + // Reset source counts. + this.sourceCounts = {}; + + // First, hit the Express server `rename` endpoint to perform the `rename` syscall on the file system. + await axios + .post(this.apiUrl + `/rename`, { + user: userStore.userProfile.userName || 'Guest', + previousUuid: prevUuid, + newUuid: this.changeUuid, + configs: { en: convertedEnglish, fr: convertedFrench } }) - .then(async (response: any) => { - // 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 = {}; + .then(async (res: AxiosResponse) => { + // Once the server has processed the renaming, update the UUID in the database if not in dev mode. + if (process.env.VUE_APP_NET_API_URL !== undefined) { + await axios.post(process.env.VUE_APP_NET_API_URL + '/api/version/update', { + uuid: this.uuid, + changeUuid: this.changeUuid + }); + } - if (this.configFileStructure?.zip) this.configFileStructureHelper(this.configFileStructure.zip); + // After the server and database have been updated, re-build configFileStructure, + // save the new config files to the server and fetch the new Git history. + if (this.configFileStructure?.zip) { + this.configFileStructureHelper(this.configFileStructure.zip, undefined, true).then(() => { + this.fetchHistory(); + this.renameMode = this.processingRename = false; + }); + } }); } - this.renaming = false; - this.renamed = this.uuid; } // Given a Storylines config, replace instances of the current UUID with a new UUID. @@ -1109,6 +1096,7 @@ export default class MetadataEditorV extends Vue { } }; + // Rename logo and introduction slide background image, if applicable. if (config?.introSlide.logo?.src) { config.introSlide.logo.src = config.introSlide.logo.src.replace( `${this.uuid}/assets/`, @@ -1116,10 +1104,26 @@ export default class MetadataEditorV extends Vue { ); } + if (config?.introSlide.backgroundImage) { + config.introSlide.backgroundImage = config.introSlide.backgroundImage.replace( + `${this.uuid}/assets/`, + `${this.changeUuid}/assets/` + ); + } + config.slides.forEach((slide) => { - slide.panel.forEach((panel) => { - _renameHelper(panel); - }); + if (Object.keys(slide).length !== 0) { + if ((slide as Slide).backgroundImage) { + (slide as Slide).backgroundImage = (slide as Slide).backgroundImage.replace( + `${this.uuid}/assets/`, + `${this.changeUuid}/assets/` + ); + } + + (slide as Slide).panel.forEach((panel) => { + _renameHelper(panel); + }); + } }); } @@ -1130,9 +1134,11 @@ export default class MetadataEditorV extends Vue { } configs[lang]?.slides.forEach((slide) => { - slide.panel.forEach((panel) => { - this.panelSourceHelper(panel); - }); + if (Object.keys(slide).length !== 0) { + (slide as Slide).panel.forEach((panel) => { + this.panelSourceHelper(panel); + }); + } }); }); } @@ -1185,7 +1191,7 @@ export default class MetadataEditorV extends Vue { * Generates or loads a ZIP file and creates required project folders if needed. * Returns an object that makes it easy to access any specific folder. */ - configFileStructureHelper(configZip: typeof JSZip, uploadLogo?: File | undefined): void { + configFileStructureHelper(configZip: typeof JSZip, uploadLogo?: File | undefined): Promise { const assetsFolder = configZip.folder('assets'); const chartsFolder = configZip.folder('charts'); const rampConfigFolder = configZip.folder('ramp-config'); @@ -1210,12 +1216,13 @@ export default class MetadataEditorV extends Vue { this.configFileStructure.assets[this.configLang].file(uploadLogo?.name, uploadLogo); } - this.loadConfig(); + return this.loadConfig(); } /** * Loads a configuration file from the product folder, and sets application data * as needed. + * @param config the configuration object to load. */ async loadConfig(config?: StoryRampConfig): Promise { if (config) { @@ -1239,7 +1246,7 @@ export default class MetadataEditorV extends Vue { return; } - if (this.loadExisting && !this.renamed) { + if (this.loadExisting) { this.loadStatus = 'waiting'; Message.success(this.$t('editor.editMetadata.message.successfulLoad')); } else { @@ -1553,28 +1560,26 @@ export default class MetadataEditorV extends Vue { if (!this.loadExisting || rename) { const user = useUserStore().userProfile.userName || 'Guest'; // If renaming, show the loading spinner while we check whether the UUID is taken. - fetch(this.apiUrl + `/retrieve/${rename ? this.changeUuid : this.uuid}/latest`, { headers: { user } }).then( - (res: Response) => { - if (res.status !== 404) { - this.warning = rename ? 'rename' : 'uuid'; - } + fetch(this.apiUrl + `/check/${rename ? this.changeUuid : this.uuid}`).then((res: Response) => { + if (res.status !== 404) { + this.warning = rename ? 'rename' : 'uuid'; + } - if (rename) this.checkingUuid = false; + if (rename) this.checkingUuid = false; - fetch(this.apiUrl + `/retrieveMessages`) - .then((res: any) => { - if (res.ok) return res.json(); - }) - .then((data) => { - axios - .post(import.meta.env.VITE_APP_NET_API_URL + '/api/log/create', { - messages: data.messages - }) - .catch((error: any) => console.log(error.response || error)); - }) - .catch((error: any) => console.log(error.response || error)); - } - ); + fetch(this.apiUrl + `/retrieveMessages`) + .then((res: any) => { + if (res.ok) return res.json(); + }) + .then((data) => { + axios + .post(import.meta.env.VITE_APP_NET_API_URL + '/api/log/create', { + messages: data.messages + }) + .catch((error: any) => console.log(error.response || error)); + }) + .catch((error: any) => console.log(error.response || error)); + }); } this.warning = 'none'; this.highlightedIndex = -1; diff --git a/src/definitions.ts b/src/definitions.ts index bc09e317..4b7ea6ae 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -133,6 +133,7 @@ export interface Intro { title: string; subtitle: string; blurb?: string; + backgroundImage: string; } export interface Slide { @@ -141,6 +142,7 @@ export interface Slide { // panel: [BasePanel, BasePanel | undefined]; panel: BasePanel[]; includeInToc?: boolean; + backgroundImage: string; } export interface MultiLanguageSlide { diff --git a/src/lang/lang.csv b/src/lang/lang.csv index d3af0f78..28aa8ef5 100644 --- a/src/lang/lang.csv +++ b/src/lang/lang.csv @@ -98,7 +98,6 @@ editor.metadata.uuidInstructions,"Please enter the UUID of an existing storyline editor.metadata.newUuidInstructions,"Enter a unique ID for your new storyline. One has been auto-generated for you, but you can also enter your own.",1,"Entrez un identifiant unique pour votre nouveau scénario. Un a été généré automatiquement pour vous, mais vous pouvez également saisir le vôtre.",0 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,Cliquez ici pour changer,0 -editor.changingUuid,You are changing this product UUID to '{changeUuid}'. Save changes required.,1,Vous modifiez l'UUID de ce produit en '{changeUuid}'. Enregistrez les modifications requises.,0 editor.title,Title,1,Titre,1 editor.respectTitle,RAMP Storylines Editor & Creation Tool,1,Éditeur et outil de création de scénarios RAMP,0 editor.logo,Logo,1,Logo,1