diff --git a/Module/main.js b/Module/main.js index bc17aee..91349db 100644 --- a/Module/main.js +++ b/Module/main.js @@ -1,17 +1,16 @@ - // Namespaced isEmpty handlebars helper for testing if // a given input is does not contain additional values -Handlebars.registerHelper('bugs-isEmpty', (input) => { - if (!input) { - return true; - } - if (input instanceof Array) { - return input.length < 1; - } - if (input instanceof Set) { - return input.size < 1; - } - return isObjectEmpty(input); +Handlebars.registerHelper("bugs-isEmpty", (input) => { + if (!input) { + return true; + } + if (input instanceof Array) { + return input.length < 1; + } + if (input instanceof Set) { + return input.size < 1; + } + return isObjectEmpty(input); }); /** @@ -25,620 +24,636 @@ const generateActiveModuleList = (separator = "--", versionMarker = "v") => { let schema = `
\nActive Modules\n\n{REPL_MODULES}\n
`; let data = []; let activeModules = [...game.modules].filter(([name, opts]) => opts.active); - activeModules.forEach(([name, opts]) => data = [...data, `${name}${separator}${versionMarker}${opts.data.version};`]); + activeModules.forEach( + ([name, opts]) => (data = [...data, `${name}${separator}${versionMarker}${opts.data.version};`]) + ); return schema.replace(/{REPL_MODULES}/gi, data.join("\n")); -} +}; /** * For a given module determine all settings that are bound * to its scope, collect them, and format them. This only * keeps scalars to prevent mile long descriptions. - * + * * @param {module} mod Module data as retrived from game.modules.get() * @return {String} Markdown formatted collapsible list of settings. */ const generateModuleSettings = (mod) => { - // Find all keys in settings that belong to our module - let modSettings = []; - game.settings.settings.forEach((setting) => { - if (setting.module === mod.data.name) { - // only allow scalars - if (setting.config && setting.type !== "object") { - let trimmedKey = setting.key.replace("-", "").replace("_", "").replace(" ", "").toLowerCase(); - const ignore = ["cookie", "password", "secret", "token", "bearer", "email"].some(badKey => trimmedKey.includes(badKey)); - if (!ignore){ - let setVal = game.settings.get(mod.data.name, setting.key); - modSettings.push(`${setting.key}: ${setVal}`); + // Find all keys in settings that belong to our module + let modSettings = []; + game.settings.settings.forEach((setting) => { + if (setting.module === mod.data.name) { + // only allow scalars + if (setting.config && setting.type !== "object") { + let trimmedKey = setting.key.replace("-", "").replace("_", "").replace(" ", "").toLowerCase(); + const ignore = ["cookie", "password", "secret", "token", "bearer", "email"].some((badKey) => + trimmedKey.includes(badKey) + ); + if (!ignore) { + let setVal = game.settings.get(mod.data.name, setting.key); + modSettings.push(`${setting.key}: ${setVal}`); + } + } } - } - } - }) - // collapsible field for module settings - const schema = `
\nModule Settings\n\n\`\`\`js\n${modSettings.join(",\n")}\n\`\`\`\n
\n`; + }); + // collapsible field for module settings + const schema = `
\nModule Settings\n\n\`\`\`js\n${modSettings.join( + ",\n" + )}\n\`\`\`\n
\n`; - return schema; -} + return schema; +}; /** * Format given API Details (usually pre-filled via the BugReporterAPI) in a way that is similar to what * is already used in our bug report structure. - * + * * @param {String} apiDetails Details such as errors and/or stack traces to append to the bug report. - * @returns + * @returns */ const generateAdditionalDetails = (apiDetails) => { - let schema = `
\nAdditional Details\n\n{REPL_DETAILS}\n
` - return schema.replace(/{REPL_DETAILS}/gi, apiDetails); -} - + let schema = `
\nAdditional Details\n\n{REPL_DETAILS}\n
`; + return schema.replace(/{REPL_DETAILS}/gi, apiDetails); +}; /** * Based off of Moo Man's Excellent WFRP4e Bug Reporter * https://github.com/moo-man/WFRP4e-FoundryVTT/blob/master/modules/apps/bug-report.js */ class BugReportForm extends FormApplication { - constructor(app, { selectedModule, title = "", description = "" }) { - super(app); - this.endpoint = "https://foundryvttbugreporter.azurewebsites.net/api/ReportBugFunction?code=VCvrWib1lha2nf9Pza7fOaThNTksbmHdEjVhIudCHwXg3zyg4vPprg=="; - this.module = game.modules.get(selectedModule) || game.system; - this.github = this.module.data.bugs.includes("github"); - this.gitlab = this.module.data.bugs.includes("gitlab"); + constructor(app, { selectedModule, title = "", description = "" }) { + super(app); + this.endpoint = + "https://foundryvttbugreporter.azurewebsites.net/api/ReportBugFunction?code=VCvrWib1lha2nf9Pza7fOaThNTksbmHdEjVhIudCHwXg3zyg4vPprg=="; + this.module = game.modules.get(selectedModule) || game.system; + this.github = this.module.data.bugs.includes("github"); + this.gitlab = this.module.data.bugs.includes("gitlab"); + + this.useBugReporter = + (this.module.data.flags?.allowBugReporter || this.module.data.allowBugReporter) && + (this.github || this.gitlab); + + if (this.module.data.allowBugReporter) { + console.warn( + `Root level allowBugReporter fields are deprecated as of BugReporter 1.3.0 and will be removed in a future release. The module ${this.module.data.name} should update its manifest to place allowBugReporter in the flags object.` + ); + } + + this.formFields = { + bugTitle: title || "", + issuer: "", + issueLabel: "", + bugDescription: "", + apiDetails: description || "", + }; - this.useBugReporter = (this.module.data.flags?.allowBugReporter || this.module.data.allowBugReporter) && (this.github || this.gitlab); + this.foundIssues = []; - if (this.module.data.allowBugReporter) { - console.warn(`Root level allowBugReporter fields are deprecated as of BugReporter 1.3.0 and will be removed in a future release. The module ${this.module.data.name} should update its manifest to place allowBugReporter in the flags object.`); + this.isSending = false; // true while waiting for server response + this.submittedIssue = undefined; } - this.formFields = { - bugTitle: title || '', - issuer: '', - issueLabel: '', - bugDescription: '', - apiDetails: description || '', + // helper for rendering spinner + get isEditable() { + return this.options.editable && !this.isSending && !this.submittedIssue; } - this.foundIssues = []; - - this.isSending = false; // true while waiting for server response - this.submittedIssue = undefined; - } - - // helper for rendering spinner - get isEditable() { - return this.options.editable && !this.isSending && !this.submittedIssue; - } - - // render options for our FormApplication - static get defaultOptions() { - const options = { - ...super.defaultOptions, - closeOnSubmit: false, - classes: ['bug-report'], - submitOnChange: false, - submitOnClose: false, - id: "bug-report", - template: "modules/bug-reporter/templates/bug-report.html", - height: 'auto', - width: 600, - minimizable: true, - title: "Post Your Bugs", - }; - return options; - } - - /** - * bugs endpoint is the html version for Github - * and the api version for Gitlab - * search endpoint is the api version for Github - * and the html version for Gitlab - * - * This is due to the server using a Github library - * but not a Gitlab library (for ease of use) - * - * SPEC: https://docs.github.com/en/rest/reference/issues - * SPEC: https://docs.gitlab.com/ee/api/issues.html - * - * @return {object} Dictionary containing bugs and search urls - */ - get endpoints() { - let bugs, search; - // Github - if (this.github) { - // https://github.com/user/repo/issues - const regex = /github.com\/(.+)\/issues/g; - - const match = regex.exec(this.module.data.bugs); - - const repo = match?.[1].toLowerCase(); - - bugs = this.module.data.bugs; - search = `https://api.github.com/search/issues?q=repo:${repo}`; - // Gitlab - } else if (this.gitlab) { - // https://gitlab.com/user/repo/-/issues - const regex = /gitlab.com\/(.+)\/-\/issues/g; - - const match = regex.exec(this.module.data.bugs); - - const repo = match?.[1].toLowerCase(); - - bugs = `https://gitlab.com/api/v4/projects/${encodeURIComponent(repo)}/issues`; - search = bugs; + // render options for our FormApplication + static get defaultOptions() { + const options = { + ...super.defaultOptions, + closeOnSubmit: false, + classes: ["bug-report"], + submitOnChange: false, + submitOnClose: false, + id: "bug-report", + template: "modules/bug-reporter/templates/bug-report.html", + height: "auto", + width: 600, + minimizable: true, + title: "Post Your Bugs", + }; + return options; } - return { bugs: bugs, search: search }; - } - - /** - * Search our module's manifest for any known conflicts - * For each conflict determine if the conflict is based - * on a certain version, or if the conflict is with the - * module in general. - * - * TODO: Fix this for 0.8.3+ where the M+ spec is getting a rework - * - * SPEC: https://foundryvtt.wiki/en/development/manifest-plus#conflicts - * - * @return {object} conflicts, version, update status - */ - get conflicts() { - return this.module.data.conflicts?.map((conflict) => { - const mod = game.modules.get(conflict.name); - let conflictingVersion = false; - let versionChecks = false; - // Check if utilizing option versionMin / versionMax fields - if ("versionMin" in conflict && "versionMax" in conflict) { - versionChecks = true; - // find if current module version is within the versionMin and versionMax fields - if (isNewerVersion(mod.data.version, conflict.versionMin) && !isNewerVersion(mod.data.version, conflict.versionMax)) { - conflictingVersion = true; + /** + * bugs endpoint is the html version for Github + * and the api version for Gitlab + * search endpoint is the api version for Github + * and the html version for Gitlab + * + * This is due to the server using a Github library + * but not a Gitlab library (for ease of use) + * + * SPEC: https://docs.github.com/en/rest/reference/issues + * SPEC: https://docs.gitlab.com/ee/api/issues.html + * + * @return {object} Dictionary containing bugs and search urls + */ + get endpoints() { + let bugs, search; + // Github + if (this.github) { + // https://github.com/user/repo/issues + const regex = /github.com\/(.+)\/issues/g; + + const match = regex.exec(this.module.data.bugs); + + const repo = match?.[1].toLowerCase(); + + bugs = this.module.data.bugs; + search = `https://api.github.com/search/issues?q=repo:${repo}`; + // Gitlab + } else if (this.gitlab) { + // https://gitlab.com/user/repo/-/issues + const regex = /gitlab.com\/(.+)\/-\/issues/g; + + const match = regex.exec(this.module.data.bugs); + + const repo = match?.[1].toLowerCase(); + + bugs = `https://gitlab.com/api/v4/projects/${encodeURIComponent(repo)}/issues`; + search = bugs; } - } else { - versionChecks = false; - } - - return { - name: mod.data.title, - active: mod.active, - version: mod.data.version, - conflictingVersion, - versionChecks, - } - }) - } - - /** - * Search the module's manifest for known dependencies - * and determine if they are up to date or not. - * - * SPEC: https://foundryvtt.com/article/module-development/ - * - * @return {object} dependencies, their version, and update status - */ - get dependencies() { - return this.module.data.dependencies?.map((dependency) => { - let plugin; - - switch (dependency.type) { - case "module": - plugin = game.modules.get(dependency.name); - break; - case "system": - plugin = game.system; - break; - case "world": - plugin = game.world; - } - - let upToDate; - // get remote manifest - let remote = this.checkVer(plugin); - // determine status - if (!isNewerVersion(remote.manifest?.version, plugin.data.version)) { - // we are up to date - upToDate = true; - } else { - // update required - upToDate = false; - } - // assemble return - return { - name: plugin.data.title, - // If plugin is a module, then check if it's active - // otherwise it's a system/world that is always active. - active: plugin.type === "module" ? plugin.active : true, - version: plugin.data.version, - upToDate - } - }) - } - - /** - * Data supplied to the handlebars template when it is rendered - * conflicts, dependencies, and isSending are get methods that are - * automaticall called when their respective properties are accessed - * - * @return {object} combined object for handlebars template to use - */ - getData() { - let data = { - ...super.getData(), - conflicts: this.conflicts, - contactInfo: game.settings.get("bug-reporter", "contactInfo"), - dependencies: this.dependencies, - formFields: this.formFields, - foundIssues: this.foundIssues, - isSending: this.isSending, - module: this.module, - submittedIssue: this.submittedIssue, - useBugReporter: this.useBugReporter, - // if core version > 0.8.10 (like 0.9.X) - unsupportedCore: isNewerVersion(game.data.version, "0.8.10"), - }; - - return data; - } - - /** - * override - * Calls search when we change away from the bugTitle field - */ - _onChangeInput(event) { - const el = event.target; - - const inputField = el.name.split('.')[1]; // super brittle - - this.formFields[inputField] = el.value; - - if (el.name === 'formFields.bugTitle') { - this.search(event); + + return { bugs: bugs, search: search }; } - } - - /** - * Assemble the request for the server, send it, and - * recieve the response - * @param {[type]} ev Provided by Foundry, not used - * @param {[type]} formData HTML formData (includes all input elements with their values) - */ - async _updateObject(ev, formData) { - // obtain original data - const mod = this.module; - const {formFields: { apiDetails, bugTitle, bugDescription, issuer, label, sendActiveModules, sendModSettings }} = expandObject(formData); - - // if any of our warnings are not checked, throw - if (!bugTitle || !bugDescription) { - const errorMessage = game.i18n.localize('BUG.form.errors.incomplete'); - ui.notifications.error(errorMessage); - - throw errorMessage; + + /** + * Search our module's manifest for any known conflicts + * For each conflict determine if the conflict is based + * on a certain version, or if the conflict is with the + * module in general. + * + * TODO: Fix this for 0.8.3+ where the M+ spec is getting a rework + * + * SPEC: https://foundryvtt.wiki/en/development/manifest-plus#conflicts + * + * @return {object} conflicts, version, update status + */ + get conflicts() { + return this.module.data.conflicts?.map((conflict) => { + const mod = game.modules.get(conflict.name); + let conflictingVersion = false; + let versionChecks = false; + // Check if utilizing option versionMin / versionMax fields + if ("versionMin" in conflict && "versionMax" in conflict) { + versionChecks = true; + // find if current module version is within the versionMin and versionMax fields + if ( + isNewerVersion(mod.data.version, conflict.versionMin) && + !isNewerVersion(mod.data.version, conflict.versionMax) + ) { + conflictingVersion = true; + } + } else { + versionChecks = false; + } + + return { + name: mod.data.title, + active: mod.active, + version: mod.data.version, + conflictingVersion, + versionChecks, + }; + }); } - // update default contactInfo if different from stored default - if (issuer !== game.settings.get("bug-reporter", "contactInfo")) { - await game.settings.set("bug-reporter", "contactInfo", issuer); + /** + * Search the module's manifest for known dependencies + * and determine if they are up to date or not. + * + * SPEC: https://foundryvtt.com/article/module-development/ + * + * @return {object} dependencies, their version, and update status + */ + get dependencies() { + return this.module.data.dependencies?.map((dependency) => { + let plugin; + + switch (dependency.type) { + case "module": + plugin = game.modules.get(dependency.name); + break; + case "system": + plugin = game.system; + break; + case "world": + plugin = game.world; + } + + let upToDate; + // get remote manifest + let remote = this.checkVer(plugin); + // determine status + if (!isNewerVersion(remote.manifest?.version, plugin.data.version)) { + // we are up to date + upToDate = true; + } else { + // update required + upToDate = false; + } + // assemble return + return { + name: plugin.data.title, + // If plugin is a module, then check if it's active + // otherwise it's a system/world that is always active. + active: plugin.type === "module" ? plugin.active : true, + version: plugin.data.version, + upToDate, + }; + }); } - // assemble header strings - const descriptionString = `**Description**:\n${bugDescription}`; - const issuerString = issuer ? `**Submitted By**: ${issuer}` : ''; - const labelString = label ? `**Feedback Type**: ${label}` : ''; - - // find and assemble version details - const versions = [ - `**Core:** ${game.data.version}`, - `**System:** ${game.system.id} v${game.system.data.version}`, - `**Module Version:** ${mod.data.name} v${mod.data.version}` - ]; - - // If any dependencies are present, add their details - if (this.dependencies) { - this.dependencies.forEach((depend) => { - if (depend.active) { - versions.push(`**Dependency Version:** ${depend.name} v${depend.version}`); - } - }); + /** + * Data supplied to the handlebars template when it is rendered + * conflicts, dependencies, and isSending are get methods that are + * automaticall called when their respective properties are accessed + * + * @return {object} combined object for handlebars template to use + */ + getData() { + let data = { + ...super.getData(), + conflicts: this.conflicts, + contactInfo: game.settings.get("bug-reporter", "contactInfo"), + dependencies: this.dependencies, + formFields: this.formFields, + foundIssues: this.foundIssues, + isSending: this.isSending, + module: this.module, + submittedIssue: this.submittedIssue, + useBugReporter: this.useBugReporter, + }; + + return data; } - // make user inputted data one string - const fullDescription = [[issuerString, labelString].join('\n'), versions.join('\n'), descriptionString].join('\n \n'); + /** + * override + * Calls search when we change away from the bugTitle field + */ + _onChangeInput(event) { + const el = event.target; - let bugsUrl = this.endpoints.bugs; + const inputField = el.name.split(".")[1]; // super brittle - // generating active module list from game.modules - const moduleList = sendActiveModules ? generateActiveModuleList() : ""; - // generate module settings - const moduleSettings = sendModSettings ? generateModuleSettings(mod) : ""; - // format errors / stack traces provided by API - const additionalDetails = (apiDetails != false) ? generateAdditionalDetails(apiDetails): ""; + this.formFields[inputField] = el.value; - // construct gitlab link (if applicable) - if (this.gitlab) { - bugsUrl = bugsUrl + `?title=${encodeURIComponent(bugTitle)}&description=${encodeURIComponent(fullDescription + "\n" + moduleList + moduleSettings + additionalDetails)}`; + if (el.name === "formFields.bugTitle") { + this.search(event); + } } - // let the app know we're ready to send stuff - this.isSending = true; - this.render(); - - // forward request to server - await fetch(this.endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - title: bugTitle, - body: fullDescription, - repo: bugsUrl, - moduleList, - moduleSettings: moduleSettings, - additionalDetails - }), - }) - // wait for the response - .then(async (res) => { - // if successful - if (res.status == 201) { - await res.json().then((message) => { - // map response to expected htmlUrl for web link - if (this.gitlab) { - message.htmlUrl = message.web_url; - } - // Completed! - this.submittedIssue = message; - console.log( - "Thank you for your submission. If you wish to monitor or follow up with additional details like screenshots, you can find your issue here:", - message.htmlUrl - ); - }); - } else { - ui.notifications.error( - game.i18n.localize('BUG.error') - ); - console.error('Bug Reporter encountered the following problem submitting your issue. Oh the irony...', res); + /** + * Assemble the request for the server, send it, and + * recieve the response + * @param {[type]} ev Provided by Foundry, not used + * @param {[type]} formData HTML formData (includes all input elements with their values) + */ + async _updateObject(ev, formData) { + // obtain original data + const mod = this.module; + const { + formFields: { apiDetails, bugTitle, bugDescription, issuer, label, sendActiveModules, sendModSettings }, + } = expandObject(formData); + + // if any of our warnings are not checked, throw + if (!bugTitle || !bugDescription) { + const errorMessage = game.i18n.localize("BUG.form.errors.incomplete"); + ui.notifications.error(errorMessage); + + throw errorMessage; + } + + // update default contactInfo if different from stored default + if (issuer !== game.settings.get("bug-reporter", "contactInfo")) { + await game.settings.set("bug-reporter", "contactInfo", issuer); + } + + // assemble header strings + const descriptionString = `**Description**:\n${bugDescription}`; + const issuerString = issuer ? `**Submitted By**: ${issuer}` : ""; + const labelString = label ? `**Feedback Type**: ${label}` : ""; + + // find and assemble version details + const versions = [ + `**Core:** ${game.data.version}`, + `**System:** ${game.system.id} v${game.system.data.version}`, + `**Module Version:** ${mod.data.name} v${mod.data.version}`, + ]; + + // If any dependencies are present, add their details + if (this.dependencies) { + this.dependencies.forEach((depend) => { + if (depend.active) { + versions.push(`**Dependency Version:** ${depend.name} v${depend.version}`); + } + }); } - }) - .catch((err) => { - ui.notifications.error("Something went wrong."); - console.error(err); - }) - // stop the spinner, show final product - .finally(() => { - this.isSending = false; + + // make user inputted data one string + const fullDescription = [[issuerString, labelString].join("\n"), versions.join("\n"), descriptionString].join( + "\n \n" + ); + + let bugsUrl = this.endpoints.bugs; + + // generating active module list from game.modules + const moduleList = sendActiveModules ? generateActiveModuleList() : ""; + // generate module settings + const moduleSettings = sendModSettings ? generateModuleSettings(mod) : ""; + // format errors / stack traces provided by API + const additionalDetails = apiDetails != false ? generateAdditionalDetails(apiDetails) : ""; + + // construct gitlab link (if applicable) + if (this.gitlab) { + bugsUrl = + bugsUrl + + `?title=${encodeURIComponent(bugTitle)}&description=${encodeURIComponent( + fullDescription + "\n" + moduleList + moduleSettings + additionalDetails + )}`; + } + + // let the app know we're ready to send stuff + this.isSending = true; this.render(); - }); - } - - /** - * Get Issues from GH / GL and put into this.foundIssues, then this.render(); - */ - async search(event) { - let query = $(event.currentTarget).val(); - - let endpoint; - // construct search endpoint with queries - if (this.github) { - endpoint = `${this.endpoints.search}+"${query}"`; - } else if (this.gitlab) { - endpoint = `${this.endpoints.search}?search=${encodeURIComponent(query)}&scope=all`; - } - // if no results, don't show anything - if (query === '') { - this.element.find("#bug-reporter-issues-found").empty(); - this.element.find('.found-issues').addClass('hidden'); - return; - } - // retrieve similar issues - const fetchedIssues = await fetch(endpoint, { - method: "GET", - }); - const message = await fetchedIssues.json(); - - // map our found issues into a similar format - if (this.github) { - this.foundIssues = message.items.map( - ({html_url, state, created_at, title}) => ({ - html_url, - state, - openedLabel: new Date(created_at).toLocaleDateString(), - title - }) - ); - } else if (this.gitlab) { - this.foundIssues = message.map( - ({web_url, state, created_at, title}) => ({ - html_url: web_url, - state, - openedLabel: new Date(created_at).toLocaleDateString(), - title + + // forward request to server + await fetch(this.endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: bugTitle, + body: fullDescription, + repo: bugsUrl, + moduleList, + moduleSettings: moduleSettings, + additionalDetails, + }), }) - ); + // wait for the response + .then(async (res) => { + // if successful + if (res.status == 201) { + await res.json().then((message) => { + // map response to expected htmlUrl for web link + if (this.gitlab) { + message.htmlUrl = message.web_url; + } + // Completed! + this.submittedIssue = message; + console.log( + "Thank you for your submission. If you wish to monitor or follow up with additional details like screenshots, you can find your issue here:", + message.htmlUrl + ); + }); + } else { + ui.notifications.error(game.i18n.localize("BUG.error")); + console.error( + "Bug Reporter encountered the following problem submitting your issue. Oh the irony...", + res + ); + } + }) + .catch((err) => { + ui.notifications.error("Something went wrong."); + console.error(err); + }) + // stop the spinner, show final product + .finally(() => { + this.isSending = false; + this.render(); + }); } - // show similar issues - this.render(); - } - - activateListeners(html) { - super.activateListeners(html); - - $(html).on('click', 'a', function() { - this.close(); - }.bind(this)); - - let message = this.checkVer(this.module); - this.updateStatus(message); - } - - /** - * Utilizes the Forge's API to determine the remote version of a module. - * @param {object} mod Foundry Module object - * @return {object} Wrapper for the most current manifest for the module - */ - async checkVer(mod) { - fetch( - "https://forge-vtt.com/api/bazaar/manifest/" + - mod.data.name + - "?coreVersion=" + - game.data.version - ).then((res) => { - res.json().then((message) => { - return message; - }); - }); - } - - /** - * Takes a response from checkVer() and updates the UI to reflect updated status - * @param {object} message result of checkVer(this.module) - */ - updateStatus(message) { - if (message.manifest === null) { - return; + /** + * Get Issues from GH / GL and put into this.foundIssues, then this.render(); + */ + async search(event) { + let query = $(event.currentTarget).val(); + + let endpoint; + // construct search endpoint with queries + if (this.github) { + endpoint = `${this.endpoints.search}+"${query}"`; + } else if (this.gitlab) { + endpoint = `${this.endpoints.search}?search=${encodeURIComponent(query)}&scope=all`; + } + // if no results, don't show anything + if (query === "") { + this.element.find("#bug-reporter-issues-found").empty(); + this.element.find(".found-issues").addClass("hidden"); + return; + } + // retrieve similar issues + const fetchedIssues = await fetch(endpoint, { + method: "GET", + }); + const message = await fetchedIssues.json(); + + // map our found issues into a similar format + if (this.github) { + this.foundIssues = message.items.map(({ html_url, state, created_at, title }) => ({ + html_url, + state, + openedLabel: new Date(created_at).toLocaleDateString(), + title, + })); + } else if (this.gitlab) { + this.foundIssues = message.map(({ web_url, state, created_at, title }) => ({ + html_url: web_url, + state, + openedLabel: new Date(created_at).toLocaleDateString(), + title, + })); + } + + // show similar issues + this.render(); } - if (!isNewerVersion(message.manifest?.version, this.module.data.version)) { - // we are up to date - this.element.find(".versionCheck .tag.success").removeClass("hidden"); - this.element.find(".versionCheck .tag.warning").addClass("hidden"); - } else { - // update required - this.element.find(".versionCheck .tag.success").addClass("hidden"); - this.element.find(".versionCheck .tag.warning").removeClass("hidden"); + activateListeners(html) { + super.activateListeners(html); + + $(html).on( + "click", + "a", + function () { + this.close(); + }.bind(this) + ); + + let message = this.checkVer(this.module); + this.updateStatus(message); } - } + /** + * Utilizes the Forge's API to determine the remote version of a module. + * @param {object} mod Foundry Module object + * @return {object} Wrapper for the most current manifest for the module + */ + async checkVer(mod) { + fetch("https://forge-vtt.com/api/bazaar/manifest/" + mod.data.name + "?coreVersion=" + game.data.version).then( + (res) => { + res.json().then((message) => { + return message; + }); + } + ); + } + + /** + * Takes a response from checkVer() and updates the UI to reflect updated status + * @param {object} message result of checkVer(this.module) + */ + updateStatus(message) { + if (message.manifest === null) { + return; + } + + if (!isNewerVersion(message.manifest?.version, this.module.data.version)) { + // we are up to date + this.element.find(".versionCheck .tag.success").removeClass("hidden"); + this.element.find(".versionCheck .tag.warning").addClass("hidden"); + } else { + // update required + this.element.find(".versionCheck .tag.success").addClass("hidden"); + this.element.find(".versionCheck .tag.warning").removeClass("hidden"); + } + } } /** - * Dialog that presents the user a choice of which module they would like to + * Dialog that presents the user a choice of which module they would like to * report a bug, make a suggestion, etc. for. This list is filtered to only * modules that have explicitly allowed Bug Reporter to work with them. * @return {Promise} Chosen module's name */ function getModuleSelection() { - return new Promise((resolve, reject) => { - // filter modules to only modules & sytem that have a bugs field - // and is active - const moduleOptions = [...game.modules.values(), game.system] - .filter( - (mod) => - (mod.active || mod.template) && !!mod.data.bugs - ) - // we don't need all of the info, just these bits - .map((mod) => ({ - title: mod.data.title, - name: mod.data.name, - }) - ); - - // spawn an interactive HTML form inside Foundry - // with buttons and localization! - new Dialog({ - title: game.i18n.localize('BUG.moduleSelect.title'), - content: ` + return new Promise((resolve, reject) => { + // filter modules to only modules & sytem that have a bugs field + // and is active + const moduleOptions = [...game.modules.values(), game.system] + .filter((mod) => (mod.active || mod.template) && !!mod.data.bugs) + // we don't need all of the info, just these bits + .map((mod) => ({ + title: mod.data.title, + name: mod.data.name, + })); + + // spawn an interactive HTML form inside Foundry + // with buttons and localization! + new Dialog({ + title: game.i18n.localize("BUG.moduleSelect.title"), + content: ` -

${game.i18n.localize('BUG.moduleSelect.helper')}

+

${game.i18n.localize("BUG.moduleSelect.helper")}

`, - buttons: { - no: { - icon: '', - label: game.i18n.localize('Cancel'), - }, - yes: { - icon: '', - label: game.i18n.localize('BUG.proceed'), - callback: (html) => { - const formValues = { - selectedModule: html.find('[name="selectedModule"]').val(), - }; - - resolve(formValues); - }, - }, - }, - default: 'yes', - close: () => { - reject("No module chosen to report bugs to."); - }, - }).render(true); - }); + buttons: { + no: { + icon: '', + label: game.i18n.localize("Cancel"), + }, + yes: { + icon: '', + label: game.i18n.localize("BUG.proceed"), + callback: (html) => { + const formValues = { + selectedModule: html.find('[name="selectedModule"]').val(), + }; + + resolve(formValues); + }, + }, + }, + default: "yes", + close: () => { + reject("No module chosen to report bugs to."); + }, + }).render(true); + }); } /** * API class that allows other modules to interact with Bug Reporter */ class BugReporterAPI { - /** - * This function allows you to check if a given module (modid) is supported - * by the Bug Reporter Module. - * - * NOTE: This evaluation currently does NOT check if the given `bugs` url from - * the module's manifest is legitimate, so in theory there can be a false positive. - * - * @param {'String'} modid ID of the module in question - * @returns True if Bug Reporter is functional with the provided module ID - */ - allowBugReporter(modid) { - const module = game.modules.get(modid) || game.system; - - if (module.type === "module") { - if (module.active === "false"){ - return false; - } + /** + * This function allows you to check if a given module (modid) is supported + * by the Bug Reporter Module. + * + * NOTE: This evaluation currently does NOT check if the given `bugs` url from + * the module's manifest is legitimate, so in theory there can be a false positive. + * + * @param {'String'} modid ID of the module in question + * @returns True if Bug Reporter is functional with the provided module ID + */ + allowBugReporter(modid) { + const module = game.modules.get(modid) || game.system; + + if (module.type === "module") { + if (module.active === "false") { + return false; + } + } + return module.data.flags?.allowBugReporter || module.data.allowBugReporter; } - return (module.data.flags?.allowBugReporter || module.data.allowBugReporter); - } - - /** - * Begin the workflow for a given module/system (modid) and potentially provide some - * pre-filled details to the form like the Title of the bug report and some details. Note - * that the details must be a String and will undergo some slight modifications (as well as - * some content moderation from the FoundryVttBugReporterApi hosted on Azure) - * - * @param {String} modid ID of the module to start the workflow for - * @param {String} title What you want to be pre-populated into the `title` field. - * @param {String} details What you want to be included in the "body" of the bug report. - */ - bugWorkflow(modid, title = "", details = "") { - if (this.allowBugReporter(modid)) { - new BugReportForm(undefined, { selectedModule: modid, title, description: details} ).render(true); - } else { - console.error("The module " + modid + " does not support bug-reporter.") + + /** + * Begin the workflow for a given module/system (modid) and potentially provide some + * pre-filled details to the form like the Title of the bug report and some details. Note + * that the details must be a String and will undergo some slight modifications (as well as + * some content moderation from the FoundryVttBugReporterApi hosted on Azure) + * + * @param {String} modid ID of the module to start the workflow for + * @param {String} title What you want to be pre-populated into the `title` field. + * @param {String} details What you want to be included in the "body" of the bug report. + */ + bugWorkflow(modid, title = "", details = "") { + if (this.allowBugReporter(modid)) { + new BugReportForm(undefined, { selectedModule: modid, title, description: details }).render(true); + } else { + console.error("The module " + modid + " does not support bug-reporter."); + } } - } } // Once Foundry has initialized, add our Hook to listen for sidebar render Hooks.once("init", () => { - Hooks.on("renderSidebarTab", async (app, html) => { - // Add our special Post Bug label - if (app.options.id == "settings") { - let button = $(``); - // Attach a listener to spawn our Form - button.click(async (ev) => { - const { selectedModule } = await getModuleSelection(); - new BugReportForm(undefined, { selectedModule } ).render(true); - }); - // Adds our button at the top of the sidebar, underneath - // game version - button.insertAfter(html.find("#game-details")); - } - // game.modules isn't populated until init, so we insert our API here. - game.modules.get("bug-reporter").api = new BugReporterAPI; - }); - - game.settings.register("bug-reporter", "contactInfo", { - scope: "client", - config: false, - default: "", - type: String, - }); + Hooks.on("renderSidebarTab", async (app, html) => { + // Add our special Post Bug label + if (app.options.id == "settings") { + let button = $( + `` + ); + // Attach a listener to spawn our Form + button.click(async (ev) => { + const { selectedModule } = await getModuleSelection(); + new BugReportForm(undefined, { selectedModule }).render(true); + }); + // Adds our button at the top of the sidebar, underneath + // game version + button.insertAfter(html.find("#game-details")); + } + // game.modules isn't populated until init, so we insert our API here. + game.modules.get("bug-reporter").api = new BugReporterAPI(); + }); + + game.settings.register("bug-reporter", "contactInfo", { + scope: "client", + config: false, + default: "", + type: String, + }); }); diff --git a/Module/module.json b/Module/module.json index 18b7c20..f772ad1 100644 --- a/Module/module.json +++ b/Module/module.json @@ -2,9 +2,9 @@ "name": "bug-reporter", "title": "🐛 Bug Reporter", "description": "Adds a utility in game to report bugs without having to leave Foundry.", - "version": "1.3.0", - "minimumCoreVersion": "0.8.0", - "compatibleCoreVersion": "0.8.8", + "version": "Replaced in releases", + "minimumCoreVersion": "9", + "compatibleCoreVersion": "9", "author": "Ethck", "authors": [ { @@ -47,11 +47,11 @@ "media": [ { "type": "cover", - "link": "https://raw.githubusercontent.com/League-of-Foundry-Developers/bug-reporter/master/cover-image.jpg" + "url": "https://raw.githubusercontent.com/League-of-Foundry-Developers/bug-reporter/master/cover-image.jpg" }, { "type": "screenshot", - "link": "https://raw.githubusercontent.com/League-of-Foundry-Developers/bug-reporter/master/form-flow.png" + "url": "https://raw.githubusercontent.com/League-of-Foundry-Developers/bug-reporter/master/form-flow.png" } ], "esmodules": ["/main.js"], diff --git a/Module/templates/bug-report.html b/Module/templates/bug-report.html index 99b58c1..f5d83af 100644 --- a/Module/templates/bug-report.html +++ b/Module/templates/bug-report.html @@ -9,11 +9,6 @@

- - {{#if unsupportedCore}} -
0.9.X versions of Foundry are in ALPHA and are likely to be extremely unstable.
- {{/if}} -

Module Conflicts