From e737bfe3d62d57cd720d4543118e700f8828e205 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Tue, 18 Jul 2023 23:12:23 -0400 Subject: [PATCH 01/14] Modify file permissions and image logo names and update storage depending on window focus. --- .../001_Notice of Appearance - pro se.pdf | Bin ...ge Non-Conviction - 7603 Nonstipulated.pdf | Bin clinicDocs/5119(g) Nonstipulated.pdf | Bin clinicDocs/5119(g) Stipulated.pdf | Bin clinicDocs/7602 NonStip.pdf | Bin clinicDocs/7602 Stipulated.pdf | Bin clinicDocs/7603 stipulated.pdf | Bin extensionDirectory/components/filings.vue | 54 ++++++++++-------- .../components/manage-counts.vue | 21 ++++--- extensionDirectory/components/popup.vue | 30 ++++++---- extensionDirectory/images/ACT_logo_color.png | Bin .../images/{icon_128x.png => icon_128.png} | Bin .../images/{icon_16x.png => icon_16.png} | Bin .../images/{icon_32x.png => icon_32.png} | Bin .../images/{icon_48x.png => icon_48.png} | Bin extensionDirectory/jsconfig.json | 0 extensionDirectory/manage-counts.js | 0 extensionDirectory/utils.js | 0 18 files changed, 64 insertions(+), 41 deletions(-) mode change 100755 => 100644 clinicDocs/001_Notice of Appearance - pro se.pdf mode change 100755 => 100644 clinicDocs/007_Motion to Expunge Non-Conviction - 7603 Nonstipulated.pdf mode change 100755 => 100644 clinicDocs/5119(g) Nonstipulated.pdf mode change 100755 => 100644 clinicDocs/5119(g) Stipulated.pdf mode change 100755 => 100644 clinicDocs/7602 NonStip.pdf mode change 100755 => 100644 clinicDocs/7602 Stipulated.pdf mode change 100755 => 100644 clinicDocs/7603 stipulated.pdf mode change 100755 => 100644 extensionDirectory/components/filings.vue mode change 100755 => 100644 extensionDirectory/components/manage-counts.vue mode change 100755 => 100644 extensionDirectory/images/ACT_logo_color.png rename extensionDirectory/images/{icon_128x.png => icon_128.png} (100%) rename extensionDirectory/images/{icon_16x.png => icon_16.png} (100%) rename extensionDirectory/images/{icon_32x.png => icon_32.png} (100%) rename extensionDirectory/images/{icon_48x.png => icon_48.png} (100%) mode change 100755 => 100644 extensionDirectory/jsconfig.json mode change 100755 => 100644 extensionDirectory/manage-counts.js mode change 100755 => 100644 extensionDirectory/utils.js diff --git a/clinicDocs/001_Notice of Appearance - pro se.pdf b/clinicDocs/001_Notice of Appearance - pro se.pdf old mode 100755 new mode 100644 diff --git a/clinicDocs/007_Motion to Expunge Non-Conviction - 7603 Nonstipulated.pdf b/clinicDocs/007_Motion to Expunge Non-Conviction - 7603 Nonstipulated.pdf old mode 100755 new mode 100644 diff --git a/clinicDocs/5119(g) Nonstipulated.pdf b/clinicDocs/5119(g) Nonstipulated.pdf old mode 100755 new mode 100644 diff --git a/clinicDocs/5119(g) Stipulated.pdf b/clinicDocs/5119(g) Stipulated.pdf old mode 100755 new mode 100644 diff --git a/clinicDocs/7602 NonStip.pdf b/clinicDocs/7602 NonStip.pdf old mode 100755 new mode 100644 diff --git a/clinicDocs/7602 Stipulated.pdf b/clinicDocs/7602 Stipulated.pdf old mode 100755 new mode 100644 diff --git a/clinicDocs/7603 stipulated.pdf b/clinicDocs/7603 stipulated.pdf old mode 100755 new mode 100644 diff --git a/extensionDirectory/components/filings.vue b/extensionDirectory/components/filings.vue old mode 100755 new mode 100644 index 9668db59..cd6f09f3 --- a/extensionDirectory/components/filings.vue +++ b/extensionDirectory/components/filings.vue @@ -30,14 +30,15 @@ const maxCountsOnNoA = 10; function detectChangesInChromeStorage(app) { chrome.storage.onChanged.addListener(function (changes, namespace) { var countsChange = changes['counts']; - var responsesChange = changes['responses']; - + var responsesChange = changes['responses'] if (countsChange === undefined && responsesChange === undefined) return; - // if (countsChange.newValue === undefined) { - // app.clearAll(); - // return; - // } - app.loadAll(function () {}); + if (countsChange.newValue === undefined) { + app.clearAll(); + return; + } + if (!document.hasFocus()) { + app.loadAll(function () { }); + } }); } @@ -169,19 +170,27 @@ export default { saveSettings: function () { // devLog("save settings", this.settings) settingString = JSON.stringify(this.settings); - localStorage.setItem('localExpungeVTSettings', settingString); + if (document.hasFocus()) { + localStorage.setItem('localExpungeVTSettings', settingString); + } }, saveResponses: function () { - devLog('save responses' + getError()); - chrome.storage.local.set({ - responses: this.responses, - }); + devLog( + 'save responses' + getError() + ); + if (document.hasFocus()) { + chrome.storage.local.set({ + responses: this.responses, + }); + } }, saveCounts: function () { devLog('saving counts'); - chrome.storage.local.set({ - counts: toRaw(this.saved), - }); + if (document.hasFocus()) { + chrome.storage.local.set({ + counts: toRaw(this.saved), + }); + } }, handleNewDocketNums: function (sheetNum) { if (sheetNum.toLowerCase().includes('-cr-')) { @@ -224,13 +233,10 @@ export default { } callback(); - // nextTick(function () { - // // call any vanilla js functions that need to run after vue is all done setting up. - // initAfterVue(); - // }); - // setTimeout(() => { - // initAfterVue(); - // }, 0); + //this.$nextTick(function () { + //call any vanilla js functions that need to run after vue is all done setting up. + //initAfterVue(); + //}); }); }, @@ -1273,9 +1279,9 @@ export default {
-
+
-

Filings for {{petitioner.name}}

+

Filings for {{petitioner.name}}

-
+
-

Counts for {{petitioner.name}}

+

Counts for {{petitioner.name}}

- `, - props: ['stipulated'], -}); - -Vue.component('filing-dated-city', { - template: ` -

- Dated in the city of , - on the day of , - in the month of ,
- in the year 20. -

- `, -}); - -Vue.component('pills-row', { - template: `
- - Mis - - - Fel - - - - Surcharge - -
- `, - props: ['count', 'dob'], - methods: { - decimalAgeInYears: function (value) { - if (!value) return ''; - if (!this.dob) return ''; - let fromTime = moment(value).diff(moment(this.dob)); - let duration = moment.duration(fromTime); - return (duration.asDays() / 365.25).toFixed(2); - }, - dispositionTrimmer: function (dispo) { - let trimmedDisp = ''; - if (dispo.length > 15) { - trimmedDisp = dispo.substring(0, 15) + '...'; - } else { - trimmedDisp = dispo; - } - return trimmedDisp; - }, - }, -}); - -/* TODO: implement or remove - * A seemingly useful component that returns a plain-english explaination of a given filing type. - */ -Vue.component('filing-type-heading', { - methods: { - getCheckoutPhrases(fType) { - checkoutPhrases = [ - { - type: 'ExC', - stipType: 'StipExC', - phrase: - 'The following are prior conviction(s) for which we prepared a petition to expunge:', - }, - { - type: 'ExNC', - stipType: 'StipExNC', - phrase: - 'The following are cases that DID NOT result in a conviction and we prepared a petition to expunge:', - }, - { - type: 'ExNCrim', - stipType: 'StipExNCrim', - phrase: - 'The following are counts that are no longer crimes and we prepared a petition to expunge:', - }, - { - type: 'SC', - stipType: 'StipSC', - phrase: - 'The following are prior convictions from under the age of 25, and we prepared a petition to seal:', - }, - { - type: 'SCAdult', - stipType: 'StipSCAdult', - phrase: - 'The following are prior convictions and we prepared a petition to seal:', - }, - { - type: 'SDui', - stipType: 'StipSDui', - phrase: - 'The following is a prior DUI conviction and we filed a petition to seal:', - }, - { - type: 'NegOp', - stipType: 'StipNegOp', - phrase: - 'The following is a prior Negligent Operation conviction and we filed a petition to seal:', - }, - ]; - - for (i = 0; i < checkoutPhrases.length; i++) { - if ( - checkoutPhrases[i]['type'] == fType || - checkoutPhrases[i]['stipType'] == fType - ) { - return checkoutPhrases[i]['phrase']; - } - } - }, - }, - template: ` -
-

- {{getCheckoutPhrases(heading)}} -

-
- `, - props: ['heading'], -}); - -Vue.component('checkout-offense-row', { - methods: { - isStipulated: function (filingType) { - return ( - filingType == 'StipExC' || - filingType == 'StipExNC' || - filingType == 'StipExNCrim' || - filingType == 'StipSC' || - filingType == 'StipSCAdult' || - filingType == 'StipSDui' - ); - }, - dateFormatSimple: function (value) { - if (!value) return ''; - return moment(value).format('MM/DD/YYYY'); - }, - toCountyCode: function (value) { - if (!value) return ''; - return countyCodeFromCounty(value); - }, - }, - template: ` - - -   - - {{filing.description}} - -
-
Offense: {{dateFormatSimple(filing.allegedOffenseDate)}}
- -
Arrest: {{dateFormatSimple(filing.arrestCitationDate)}}
- -
Disposed: {{dateFormatSimple(filing.dispositionDate)}}
- -
- - {{filing.offenseDisposition}} - {{filing.docketNum}} {{toCountyCode(filing.county)}} - `, - props: ['filing'], -}); \ No newline at end of file diff --git a/extensionDirectory/components/checkout-offense-row.vue b/extensionDirectory/components/checkout-offense-row.vue index d20edc1d..0ff48b35 100644 --- a/extensionDirectory/components/checkout-offense-row.vue +++ b/extensionDirectory/components/checkout-offense-row.vue @@ -16,7 +16,7 @@ export default { }, dateFormatSimple: function (value) { if (!value) return ""; - return moment(value).format("MM/DD/YYYY"); + return dayjs(value).format("MM/DD/YYYY"); }, toCountyCode: function (value) { if (!value) return ""; diff --git a/extensionDirectory/components/filings.vue b/extensionDirectory/components/filings.vue index aebce8e6..05699d55 100644 --- a/extensionDirectory/components/filings.vue +++ b/extensionDirectory/components/filings.vue @@ -1,2174 +1,2184 @@ - - - + + + diff --git a/extensionDirectory/components/manage-counts.vue b/extensionDirectory/components/manage-counts.vue index 217298b4..50a109a4 100644 --- a/extensionDirectory/components/manage-counts.vue +++ b/extensionDirectory/components/manage-counts.vue @@ -1,531 +1,535 @@ -filingNav - - - +filingNav + + + diff --git a/extensionDirectory/components/pills-row.vue b/extensionDirectory/components/pills-row.vue index 43f10e02..ac0bc226 100644 --- a/extensionDirectory/components/pills-row.vue +++ b/extensionDirectory/components/pills-row.vue @@ -1,5 +1,8 @@ + + diff --git a/extensionDirectory/test-utils.mjs b/extensionDirectory/test-utils.mjs new file mode 100644 index 00000000..c0891575 --- /dev/null +++ b/extensionDirectory/test-utils.mjs @@ -0,0 +1,98 @@ +import { expect } from 'chai'; +import puppeteer from 'puppeteer'; + +const extensionDir = '../../../../../build'; +const popup = 'popup.html'; + +// Selectors +const addEdit = '::-p-text(Add/Edit)'; +const checkbox = '.checkmark'; +const code4btvLogo = '#code4btv'; +const legalAidLogo = '#legal-aid'; +const actLogo = '.act-image'; +const name = '//label[contains(.,"Name")]/input'; +const viewPetitions = '::-p-text(View Petitions)'; + +export async function setup() { + global.browser = await puppeteer.launch({ + headless: false, + args: [ + `--disable-extensions-except=${extensionDir}`, + `--load-extension=${extensionDir}` + ] + }); + + const extension = await global.browser.waitForTarget( + target => target.type() === 'service_worker' + ); + + global.url = extension.url().match(/([^\/]*\/){1,3}/)[0]; +} + +export function teardown() { + global.browser.close(); + + global.browser = undefined; + global.url = undefined; +} + +export async function actLogoHasDimensions(page) { + await imageHasDimensions(page, actLogo); +} + +export async function clickAddEdit(page) { + let selector = await page.waitForSelector(addEdit); + const currentTarget = await page.target(); + await selector.click(); + const nextTarget = await global.browser.waitForTarget(target => target.opener() === currentTarget); + return nextTarget.page(); +} + +export async function clickCheckbox(page) { + let selector = await page.waitForSelector(checkbox); + await selector.click(); +} + +export async function code4btvLogoHasDimensions(page) { + await imageHasDimensions(page, code4btvLogo); +} + +export async function clickViewPetitions(page) { + let selector = await page.waitForSelector(viewPetitions); + await selector.click(); +} + +export async function legalAidLogoHasDimensions(page) { + await imageHasDimensions(page, legalAidLogo); +} + +export async function imageHasDimensions(page, selector) { + let [height, width] = await page.evaluate(() => { + let result = document.querySelector(selector); + return { + height: result.naturalHeight, + width: result.naturalWidth + } + }); + expect(height).to.be.above(0); + expect(width).to.be.above(0); +} + +export async function replaceTextInName(page, str) { + await page.goto(`https://developer.mozilla.org`) + let [selector] = await page.$x(name); + // let currentValue = await page.evaluate(x => return x.value, selector); + await selector.click(); + for (let characters = 0; characters < currentValue; characters++) { + await page.keyboard.press('Backspace'); + } + await page.keyboard.type(str); +} + +export async function openPopupInNewPage(browser) { + let page = await browser.newPage(); + await page.goto(`${global.url}/${popup}`, { + waitUntil: 'domcontentloaded', + }); + return page; +} \ No newline at end of file diff --git a/extensionDirectory/test/e2e/html-sample.spec.mjs b/extensionDirectory/test/e2e/html-sample.spec.mjs new file mode 100644 index 00000000..83a09784 --- /dev/null +++ b/extensionDirectory/test/e2e/html-sample.spec.mjs @@ -0,0 +1,27 @@ +import { actLogoHasDimensions, clickCheckbox, code4btvLogoHasDimensions, legalAidLogoHasDimensions, openPopupInNewPage, setup, teardown } from '../../test-utils.mjs'; + +describe('popup.html', async function() { + + beforeEach(async () => { + await setup(); + }); + + afterEach(async () => { + teardown(); + }); + + it('should display the Code for BTV, Vermont Legal Aid, and Alliance of Civic Technologists image logos', async function() { + let page = await openPopupInNewPage(global.browser); + actLogoHasDimensions(page); + code4btvLogoHasDimensions(page); + legalAidLogoHasDimensions(page); + }) + + it('should display a checkbox that can be clicked.', async function() { + let page = await openPopupInNewPage(global.browser); + await page.screenshot({ path: './screenshots/popup_inital_load.png'}); + await clickCheckbox(page); + await page.screenshot({ path: './screenshots/popup_terms_clicked.png'}); + + }) +}) diff --git a/extensionDirectory/utils.js b/extensionDirectory/utils.js index 324eb1d9..c2fdf50e 100644 --- a/extensionDirectory/utils.js +++ b/extensionDirectory/utils.js @@ -1,775 +1,784 @@ -import Gumshoe from 'gumshoejs'; -import moment from 'moment'; -import SmoothScroll from 'smooth-scroll'; - -import saveAllCountsToHtml from 'saveFile'; - -const maxCountsOnNoA = 10; - -function allDocketNumsObject(counts) { - allDocketNums = counts.map(function (count) { - return { - num: count.docketNum, - county: countyCodeFromCounty(count.county), - string: count.docketNum + ' ' + countyCodeFromCounty(count.county), - }; - }); - - //filter the docket number object array to make it unique - let result = allDocketNums.filter((e, i) => { - return ( - allDocketNums.findIndex((x) => { - return x.num == e.num && x.county == e.county; - }) == i - ); - }); - - return result; -} - -function allDocketSheetNumsObject(counts) { - allDocketSheetNums = counts.map(function (count) { - return { num: count.docketSheetNum }; - }); - - //filter the docket number object array to make it unique - let result = allDocketSheetNums.filter((e, i) => { - return ( - allDocketSheetNums.findIndex((x) => { - return x.num == e.num; - }) == i - ); - }); - - return result; -} - -export function autoExpand(field) { - if (field === undefined) return; - if (field.style === undefined) return; - // Reset field height - - field.style.height = 'inherit'; - // Get the computed styles for the element - let computed = window.getComputedStyle(field); - - // Calculate the height - let height = - parseInt(computed.getPropertyValue('border-top-width'), 5) + - parseInt(computed.getPropertyValue('padding-top'), 5) + - field.scrollHeight + - parseInt(computed.getPropertyValue('padding-bottom'), 5) + - parseInt(computed.getPropertyValue('border-bottom-width'), 5) - - 8; - - field.style.height = height + 'px'; -} - -export function clearAll() { - chrome.storage.local.remove(['counts', 'responses'], function () { - document.location.reload(); - }); -} - -export function confirmDeleteCount(vueApp, event, countId) { - event.stopPropagation(); - if (vueApp.saved.counts.length > 1) { - let currentCount = vueApp.saved.counts.filter( - (count) => count.uid === countId - )[0]; - if ( - confirm( - `Are you sure that you would like to delete the count \"${currentCount.description}\"?` - ) - ) { - deleteCount(vueApp, countId); - } - return; - } - if ( - confirm( - 'Are you sure that you would like to delete the last count, this will clear all petitioner information.' - ) - ) { - clearAll(); - } -} - -export function confirmClearData(vueApp) { - if (confirm('Are you sure you want to clear all data for this petitioner?')) { - clearAll(); - } -} - -export function countyCodeFromCounty(county) { - let countyCodes = { - Addison: 'Ancr', - Bennington: 'Bncr', - Caledonia: 'Cacr', - Chittenden: 'Cncr', - Essex: 'Excr', - Franklin: 'Frcr', - 'Grand Isle': 'Gicr', - Lamoille: 'Lecr', - Orange: 'Oecr', - Orleans: 'Oscr', - Rutland: 'Rdcr', - Washington: 'Wncr', - Windham: 'Wmcr', - Windsor: 'Wrcr', - }; - return countyCodes[county]; -} - -export function countyNameFromCountyCode(countyCode) { - counties = { - Ancr: 'Addison', - Bncr: 'Bennington', - Cacr: 'Caledonia', - Cncr: 'Chittenden', - Excr: 'Essex', - Frcr: 'Franklin', - Gicr: 'Grand Isle', - Lecr: 'Lamoille', - Oecr: 'Orange', - Oscr: 'Orleans', - Rdcr: 'Rutland', - Wncr: 'Washington', - Wmcr: 'Windham', - Wrcr: 'Windsor', - }; - return counties[countyCode]; -} - -export function createFilingsFromCounts(vueApp, counts, groupDockets = true) { - // get all counties that have counts associated with them - let filingCounties = groupByCounty(counts); - - //create an array to hold all county filing objects - let groupedFilings = []; - - //iterate through all counties and create the filings - for (let county in filingCounties) { - let countyName = filingCounties[county]; - - //filter all counts to the ones only needed for this county - let allEligibleCountsForThisCounty = counts.filter( - (count) => count.county == countyName && isFileable(count.filingType) - ); - - //figure out the filing types needed for this county. - let filingsForThisCounty = groupByFilingType( - allEligibleCountsForThisCounty - ); - - //if there are no filings needed for this county, move along to the next one. - if (filingsForThisCounty.length == 0) continue; - - //create an array to hold all of the filing objects for this county - let allFilingsForThisCountyObject = []; - - //add the notice of appearance filing to this county because we have petitions to file - //we can only fit a maximum of ~10 docket numbers, so we will create multiple Notices of Appearance to accomodate all docket numbers. - let maxDocketsPerNoA = maxCountsOnNoA || 10; - let allEligibleCountsForThisCountySegmented = groupCountsByMaxDocketNumber( - allEligibleCountsForThisCounty, - maxDocketsPerNoA - ); - - //iterate through the filing types needed for this county and push them into the array - for (let i in filingsForThisCounty) { - let filingType = filingsForThisCounty[i]; - - //if the filing is not one we're going to need a petition for, let's skip to the next filing type - if (!isFileable(filingType)) continue; - - //create the filing object that will be added to the array for this county - let filingObject = filterAndMakeFilingObject( - counts, - countyName, - filingType - ); - - //determine if we can use the filling object as is, or if we need to break it into multiple petitions. - //this is determined based on the state of the UI checkbox for grouping. - if (groupDockets || filingObject.numDocketSheets == 1) { - allFilingsForThisCountyObject.push(filingObject); - vueApp.createResponseObjectForFiling(filingObject.id); - } else { - //break the filing object into multiple petitions - for (let docketNumIndex in filingObject.docketSheetNums) { - let docketSheetNumUnique = - filingObject.docketSheetNums[docketNumIndex].num; - let brokenOutFilingObject = filterAndMakeFilingObject( - filingObject.counts, - countyName, - filingType, - docketSheetNumUnique - ); - allFilingsForThisCountyObject.push(brokenOutFilingObject); - vueApp.createResponseObjectForFiling(brokenOutFilingObject.id); - } - } - } - // insert NOAs into filings - const filingsWithNOAs = vueApp.settings.groupNoas - ? vueApp.insertNOAsForEachCounty(allFilingsForThisCountyObject) - : vueApp.insertNOAsForEachDocket(allFilingsForThisCountyObject); - - //add all filings for this county to the returned filing object. - groupedFilings.push({ - county: countyName, - filings: filingsWithNOAs, - }); - } - return groupedFilings; -} - -export function csvData(vueApp) { - return vueApp.rawCounts.map(function (count) { - return { - Petitioner_Name: vueApp.petitioner['name'], - Petitioner_DOB: vueApp.petitioner.dob, - Petitioner_Address: vueApp.petitioner.addressString, - Petitioner_Phone: vueApp.responses.phone, - County: count.county, - Docket_Sheet_Number: count.docketSheetNum, - Count_Docket_Number: count.docketNum, - Filing_Type: filingNameFromType(count.filingType), - Count_Description: count.description, - Count_Statute_Title: count.titleNum, - Count_Statute_Section: count.sectionNum, - Offense_Class: offenseAbbreviationToFull(count.offenseClass), - Offense_Disposition: count.offenseDisposition, - Offense_Disposition_Date: count.dispositionDate, - }; - }); -} - -function csvFilename(petitioner) { - let date = new Date(); - return slugify( - 'filings for ' + petitioner.name + ' ' + date.toDateString() + '.csv' - ); -} - -//Grabs name for header of filing -function filingNameFromType(filingType) { - switch (filingType) { - case 'NoA': - return 'Notice of Appearance'; - case 'feeWaiver': - return 'Motion to Waive Legal Financial Obligations'; - case 'feeWaiverAffidavit': - return "Petitioner's Sworn Statement in Support of Motion to Waive Legal Financial Obligations"; - case 'StipExC': - return 'Stipulated Petition to Expunge Conviction'; - case 'ExC': - return 'Petition to Expunge Conviction'; - case 'StipExNC': - return 'Stipulated Petition to Expunge Non-Conviction'; - case 'ExNC': - return 'Petition to Expunge Non-Conviction'; - case 'StipExNCrim': - return 'Stipulated Petition to Expunge Non-Criminal Conviction'; - case 'ExNCrim': - return 'Petition to Expunge Non-Criminal Conviction'; - case 'StipSC': - return 'Stipulated Petition to Seal Conviction of Minor'; - case 'SC': - return 'Petition to Seal Conviction of Minor'; - case 'StipSCAdult': - return 'Stipulated Petition to Seal Conviction'; - case 'SCAdult': - return 'Petition to Seal Conviction'; - case 'StipSDui': - return 'Stipulated Petition to Seal DUI Conviction'; - case 'SDui': - return 'Petition to Seal DUI Conviction'; - case 'StipNegOp': - return 'Stipulated Petition to Seal Negligent Operation Conviction'; - case 'NegOp': - return 'Petition to Seal Negligent Operation Conviction'; - case 'X': - return 'Ineligible'; - default: - return 'None'; - } -} - -function filterAndMakeFilingObject( - counts, - county, - filingType, - docketSheetNum = '' -) { - let countsOnThisFiling = counts.filter( - (count) => - count.county == county && - count.filingType == filingType && - (docketSheetNum == '' || docketSheetNum == count.docketSheetNum) - ); - return makeFilingObject(countsOnThisFiling, filingType, county); -} - -export function dateFormatSimple(value) { - if (!value) return ''; - return moment(value).format('MM/DD/YYYY'); -} - -export function deleteCount(vueApp, countId) { - index = vueApp.saved.counts.findIndex((x) => x.uid === countId); - vueApp.saved.counts.splice(index, 1); -} - -export function detectChangesInChromeStorage(app) { - chrome.storage.onChanged.addListener(function (changes, namespace) { - let countsChange = changes['counts']; - let responsesChange = changes['responses']; - - if (countsChange === undefined && responsesChange === undefined) return; - if (countsChange !== undefined && countsChange.newValue === undefined) { - clearAll(); - return; - } - if (!document.hasFocus()) { - app.loadAll(function () {}); - } - }); -} - -/** - * Replaces console.log() statements with a wrapper that prevents the extension from logging - * to the console unless it was installed by a developer. This will keep the console clean; a - * practice recommended for chrome extensions. - * - * @param {any} data Data to log to the console - * @todo find a way to make this reusuable, then delete the duplicate fn() in popup.js - */ -export function devLog(data) { - // see https://developer.chrome.com/extensions/management#method-getSelf - chrome.management.getSelf(function (self) { - if (this.installType == 'development') { - console.log(data); - } - }); -} - -export function getError() { - return 'TOOD: getError should work :('; // TODO: The code below explodes, so just no-op for now - // return new Error().stack - // .split('\n')[1] - // .split('filings.js')[1] - // .replace(')', '') -} - -export function getNextNotaryDate() { - let currentDate = moment(); - let janThisYear = moment(currentDate.format('YYYY') + '-01-31'); - - if (currentDate.isBefore(janThisYear) && isOdd(currentDate)) { - return janThisYear.format('MMMM DD, YYYY'); - } else if (!isOdd(currentDate)) { - return moment(janThisYear).add(1, 'years').format('MMMM DD, YYYY'); - } else if (currentDate.isAfter(janThisYear) && isOdd(currentDate)) { - return moment(janThisYear).add(2, 'years').format('MMMM DD, YYYY'); - } - function isOdd(num) { - let numInt = parseInt(num.format('YYYY')); - return numInt % 2; - } -} - -function groupByCounty(counts) { - let allCounties = counts.map(function (count) { - return count.county; - }); - return allCounties.filter((v, i, a) => a.indexOf(v) === i); -} - -function groupByFilingType(counts) { - let allCounts = counts.map(function (count) { - return count.filingType; - }); - return allCounts.filter((v, i, a) => a.indexOf(v) === i); -} - -/* Used when there are more docket numbers than will fit on a single Notice of Appearance - * form. This takes an array[counts], and returns an array[array[counts]]. For example, if - * the `max` is 10 dockets, then each inner array would have all the counts belonging to the - * next 10 dockets. - */ -function groupCountsByMaxDocketNumber(counts, maxLength) { - let allDocketNums = allDocketNumsObject(counts); - let numDocketGroups = Math.ceil(allDocketNums.length / maxLength); - let docketGroups = []; - - // divide all counts into arrays grouped by the `maxLength` number of dockets - for (let i = 0; i < numDocketGroups; i++) { - let start = i * maxLength; - let end = Math.min(i * maxLength + maxLength, allDocketNums.length); - let dockets = allDocketNums.slice(start, end); - let docketNums = dockets.map((docket) => docket.num); - let countGroup = counts.filter((f) => docketNums.includes(f.docketNum)); - docketGroups.push(countGroup); - } - - return docketGroups; -} - -export function handleNewDocketNums(sheetNum) { - if (sheetNum.toLowerCase().includes('-cr-')) { - return sheetNum.split(' ')[0]; - } else { - return sheetNum; - } -} - -export function handlePrintMacro(app) { - $(document).on('keydown', function (e) { - if ( - (e.ctrlKey || e.metaKey) && - (e.key == 'p' || e.charCode == 16 || e.charCode == 112 || e.keyCode == 80) - ) { - e.cancelBubble = true; - e.preventDefault(); - e.stopImmediatePropagation(); - app.printDocument(); - } - }); -} - -// TODO: implement or delete -export function initAfterFilingRefresh() { - setInitialExpandForTextAreas(); - initScrollDetection(); -} - -export function initAfterVue() { - //sets intital height of all text areas to show all text. - document.addEventListener('DOMContentLoaded', () => { - initScrollDetection(); - setInitialExpandForTextAreas(); - initTextAreaAutoExpand(); - initSmoothScroll(); - }); -} - -export function initScrollDetection() { - // initates the scrollspy for the filing-nav module. - // see: https://www.npmjs.com/package/gumshoejs#nested-navigation - let spy = new Gumshoe('#filing-nav a', { - nested: true, - nestedClass: 'active-parent', - offset: 200, // how far from the top of the page to activate a content area - reflow: true, // will update when the navigation chages (eg, user adds/changes a petition, or consolidates petitions/NOAs) - }); -} - -export function initSmoothScroll() { - let scroll = new SmoothScroll('a[href*="#"]', { - offset: 150, - durationMax: 300, - }); -} - -export function initTextAreaAutoExpand() { - document.addEventListener( - 'input', - function (event) { - if (event.target.tagName.toLowerCase() !== 'textarea') return; - autoExpand(event.target); - }, - false - ); -} - -function isEligible(filingType) { - return filingType != 'X'; -} - -function isFileable(filingType) { - return isSupported(filingType) && isEligible(filingType); -} - -function isSupported(filingType) { - switch (filingType) { - case 'NoA': - case 'StipExC': - case 'ExC': - case 'StipExNC': - case 'ExNC': - case 'StipExNCrim': - case 'ExNCrim': - case 'StipSC': - case 'StipSCAdult': - case 'StipSDui': - case 'SC': - case 'SCAdult': - case 'SDui': - case 'NegOp': - case 'StipNegOp': - case 'X': - return true; - default: - return false; - } -} - -function isStipulated(filingType) { - return ( - filingType == 'StipExC' || - filingType == 'StipExNC' || - filingType == 'StipExNCrim' || - filingType == 'StipSC' || - filingType == 'StipSCAdult' || - filingType == 'StipSDui' || - filingType == 'StipNegOp' - ); -} - -export function loadAll(vueApp, callback) { - if (callback === undefined) { - callback = function () {}; - } - devLog(localStorage.getItem('localExpungeVTSettings')); - localResult = JSON.parse(localStorage.getItem('localExpungeVTSettings')); - if (localResult !== undefined && localResult !== '' && localResult !== null) { - vueApp.settings = localResult; - } else { - vueApp.saveSettings(); - } - - chrome.storage.local.get(function (result) { - //test if we have any data - devLog('loading all'); - devLog(JSON.stringify(result)); - if (result.counts !== undefined) { - devLog(result.counts); - vueApp.saved = result.counts; - } - - if (result.responses !== undefined) { - vueApp.responses = result.responses; - } - - callback(); - //this.$nextTick(function () { - //call any vanilla js functions that need to run after vue is all done setting up. - //initAfterVue(); - //}); - }); -} - -export function linesBreaksFromArray(array) { - let string = ''; - let delimiter = '\r\n'; - let i; - for (i = 0; i < array.length; i++) { - if (i > 0) { - string += delimiter; - } - string += array[i]; - } - return string; -} - -export function lowercase(value) { - if (!value) return ''; - value = value.toString(); - return value.charAt(0).toLowerCase() + value.slice(1); -} - -/* - * Creates a filing object from data provided. - * NOTE: will fail without explaination on civil violations b/c this presumes `counts` is a non-empty array - */ -export function makeFilingObject(counts, filingType, county) { - let countsOnThisFiling = counts; - let numCounts = countsOnThisFiling.length; - let docketNums = allDocketNumsObject(countsOnThisFiling); - let numDockets = docketNums.length; - let docketSheetNums = allDocketSheetNumsObject(countsOnThisFiling); - let numDocketSheets = docketSheetNums.length; - let isMultipleCounts = numCounts > 1; - let filingId = filingType + '-' + county + '-' + docketNums[0].num; - - return { - id: filingId, - type: filingType, - title: filingNameFromType(filingType), - county: county, - numCounts: numCounts, - numDockets: numDockets, - numDocketSheets: numDocketSheets, - multipleCounts: isMultipleCounts, - numCountsString: pluralize('Count', numCounts), - numDocketsString: pluralize('Docket', numDockets), - isStipulated: isStipulated(filingType), - isEligible: isEligible(filingType), - docketNums: docketNums, - docketSheetNums: docketSheetNums, - counts: countsOnThisFiling, - }; -} - -function offenseAbbreviationToFull(offenseClass) { - switch (offenseClass) { - case 'mis': - return 'Misdemeanor'; - case 'fel': - return 'Felony'; - default: - return ''; - } -} - -export function openManagePage() { - chrome.tabs.query( - { - active: true, - currentWindow: true, - }, - (tabs) => { - let index = tabs[0].index; - chrome.tabs.create({ - url: chrome.runtime.getURL('./manage-counts.html'), - index: index + 1, - }); - } - ); -} - -export function pluralize(word, num) { - let phrase = num + ' ' + word; - if (num > 1) return phrase + 's'; - return phrase; -} - -export function nl2br(rawStr) { - let breakTag = '
'; - return (rawStr + '').replace( - /([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, - '$1' + breakTag + '$2' - ); -} - -export function openPetitionsPage() { - chrome.tabs.query( - { - active: true, - currentWindow: true, - }, - (tabs) => { - let index = tabs[0].index; - chrome.tabs.create({ - url: chrome.runtime.getURL('./filings.html'), - index: index + 1, - }); - } - ); -} - -export function resetSettings(vueApp, element) { - if (confirm('Are you sure you want to reset setting to the defaults?')) { - localStorage.removeItem(['localExpungeVTSettings']); - vueApp.settings = { - attorney: '', - attorneyAddress: '', - attorneyPhone: '', - footer1: '- Generated by ExpungeVT -', - footer2: '- A Code for BTV Project -', - role: 'AttyConsult', - forVla: false, - }; - } -} - -export function saveCounts(counts) { - if (document.hasFocus()) { - chrome.storage.local.set({ - counts: counts, - }); - } -} - -export function saveHtml(vueApp) { - let dataPojo = { - saved: vueApp.saved, - responses: vueApp.responses, - }; - saveAllCountsToHtml(JSON.stringify(dataPojo)); -} - -export function saveResponses(responses) { - devLog('save responses' + getError()); - if (document.hasFocus()) { - chrome.storage.local.set({ - responses: responses, - }); - } -} - -export function saveSettings(settings) { - settingString = JSON.stringify(settings); - if (document.hasFocus()) { - localStorage.setItem('localExpungeVTSettings', settingString); - } -} - -export function setInitialExpandForTextAreas() { - //sets the default size for all text areas based on their content. - //call this after vue has initialized and displayed - let textAreas = document.getElementsByTagName('textarea'); - for (let index in textAreas) { - let textArea = textAreas[index]; - if (textArea === undefined) return; - autoExpand(textArea); - } -} - -export function sinceNow(value) { - if (!value) return ''; - - let fromTime = moment(value).diff(moment(), 'milliseconds'); - let duration = moment.duration(fromTime); - let years = duration.years() / -1; - let months = duration.months() / -1; - let days = duration.days() / -1; - if (years > 0) { - let Ys = years == 1 ? years + 'y ' : years + 'y '; - let Ms = months == 1 ? months + 'm ' : months + 'm '; - return Ys + Ms; - } else { - if (months > 0) return months == 1 ? months + 'm ' : months + 'm '; - else return days == 1 ? days + 'd ' : days + 'd '; - } -} - -export function slugify(string) { - return string.replace(/\s+/g, '-').toLowerCase(); -} - -export function stringAgeInYearsAtDate(date, dob) { - if (!date) return ''; - if (!dob) return ''; - let fromTime = moment(date).diff(moment(dob)); - let duration = moment.duration(fromTime); - return (duration.asDays() / 365.25).toFixed(0) + ' yo'; -} - -export function toCountyCode(value) { - if (!value) return ''; - return countyCodeFromCounty(value); -} - -export function todayDate() { - date = moment().format('MMMM D[, ]YYYY'); - return date; -} - -export function uppercase(value) { - if (!value) return ''; - value = value.toString(); - return value.charAt(0).toUpperCase() + value.slice(1); -} +import Gumshoe from 'gumshoejs'; +import dayjs from 'dayjs'; +import SmoothScroll from 'smooth-scroll'; +import { toRaw } from 'vue'; + +import saveAllCountsToHtml from 'saveFile'; + +let duration = require('dayjs/plugin/duration'); +dayjs.extend(duration); + +const maxCountsOnNoA = 10; + +function allDocketNumsObject(counts) { + allDocketNums = counts.map(function (count) { + return { + num: count.docketNum, + county: countyCodeFromCounty(count.county), + string: count.docketNum + ' ' + countyCodeFromCounty(count.county), + }; + }); + + //filter the docket number object array to make it unique + let result = allDocketNums.filter((e, i) => { + return ( + allDocketNums.findIndex((x) => { + return x.num == e.num && x.county == e.county; + }) == i + ); + }); + + return result; +} + +function allDocketSheetNumsObject(counts) { + allDocketSheetNums = counts.map(function (count) { + return { num: count.docketSheetNum }; + }); + + //filter the docket number object array to make it unique + let result = allDocketSheetNums.filter((e, i) => { + return ( + allDocketSheetNums.findIndex((x) => { + return x.num == e.num; + }) == i + ); + }); + + return result; +} + +export function autoExpand(field) { + if (field === undefined) return; + if (field.style === undefined) return; + // Reset field height + + field.style.height = 'inherit'; + // Get the computed styles for the element + let computed = window.getComputedStyle(field); + + // Calculate the height + let height = + parseInt(computed.getPropertyValue('border-top-width'), 5) + + parseInt(computed.getPropertyValue('padding-top'), 5) + + field.scrollHeight + + parseInt(computed.getPropertyValue('padding-bottom'), 5) + + parseInt(computed.getPropertyValue('border-bottom-width'), 5) - + 8; + + field.style.height = height + 'px'; +} + +export function clearAll() { + chrome.storage.local.remove(['counts', 'responses'], function () { + document.location.reload(); + }); +} + +export function confirmDeleteCount(vueApp, event, countId) { + event.stopPropagation(); + if (vueApp.saved.counts.length > 1) { + let currentCount = vueApp.saved.counts.filter( + (count) => count.uid === countId + )[0]; + if ( + confirm( + `Are you sure that you would like to delete the count \"${currentCount.description}\"?` + ) + ) { + deleteCount(vueApp, countId); + } + return; + } + if ( + confirm( + 'Are you sure that you would like to delete the last count, this will clear all petitioner information.' + ) + ) { + clearAll(); + } +} + +export function confirmClearData(vueApp) { + if (confirm('Are you sure you want to clear all data for this petitioner?')) { + clearAll(); + } +} + +export function countyCodeFromCounty(county) { + let countyCodes = { + Addison: 'Ancr', + Bennington: 'Bncr', + Caledonia: 'Cacr', + Chittenden: 'Cncr', + Essex: 'Excr', + Franklin: 'Frcr', + 'Grand Isle': 'Gicr', + Lamoille: 'Lecr', + Orange: 'Oecr', + Orleans: 'Oscr', + Rutland: 'Rdcr', + Washington: 'Wncr', + Windham: 'Wmcr', + Windsor: 'Wrcr', + }; + return countyCodes[county]; +} + +export function countyNameFromCountyCode(countyCode) { + counties = { + Ancr: 'Addison', + Bncr: 'Bennington', + Cacr: 'Caledonia', + Cncr: 'Chittenden', + Excr: 'Essex', + Frcr: 'Franklin', + Gicr: 'Grand Isle', + Lecr: 'Lamoille', + Oecr: 'Orange', + Oscr: 'Orleans', + Rdcr: 'Rutland', + Wncr: 'Washington', + Wmcr: 'Windham', + Wrcr: 'Windsor', + }; + return counties[countyCode]; +} + +export function createFilingsFromCounts(vueApp, counts, groupDockets = true) { + // get all counties that have counts associated with them + let filingCounties = groupByCounty(counts); + + //create an array to hold all county filing objects + let groupedFilings = []; + + //iterate through all counties and create the filings + for (let county in filingCounties) { + let countyName = filingCounties[county]; + + //filter all counts to the ones only needed for this county + let allEligibleCountsForThisCounty = counts.filter( + (count) => count.county == countyName && isFileable(count.filingType) + ); + + //figure out the filing types needed for this county. + let filingsForThisCounty = groupByFilingType( + allEligibleCountsForThisCounty + ); + + //if there are no filings needed for this county, move along to the next one. + if (filingsForThisCounty.length == 0) continue; + + //create an array to hold all of the filing objects for this county + let allFilingsForThisCountyObject = []; + + //add the notice of appearance filing to this county because we have petitions to file + //we can only fit a maximum of ~10 docket numbers, so we will create multiple Notices of Appearance to accomodate all docket numbers. + let maxDocketsPerNoA = maxCountsOnNoA || 10; + let allEligibleCountsForThisCountySegmented = groupCountsByMaxDocketNumber( + allEligibleCountsForThisCounty, + maxDocketsPerNoA + ); + + //iterate through the filing types needed for this county and push them into the array + for (let i in filingsForThisCounty) { + let filingType = filingsForThisCounty[i]; + + //if the filing is not one we're going to need a petition for, let's skip to the next filing type + if (!isFileable(filingType)) continue; + + //create the filing object that will be added to the array for this county + let filingObject = filterAndMakeFilingObject( + counts, + countyName, + filingType + ); + + //determine if we can use the filling object as is, or if we need to break it into multiple petitions. + //this is determined based on the state of the UI checkbox for grouping. + if (groupDockets || filingObject.numDocketSheets == 1) { + allFilingsForThisCountyObject.push(filingObject); + vueApp.createResponseObjectForFiling(filingObject.id); + } else { + //break the filing object into multiple petitions + for (let docketNumIndex in filingObject.docketSheetNums) { + let docketSheetNumUnique = + filingObject.docketSheetNums[docketNumIndex].num; + let brokenOutFilingObject = filterAndMakeFilingObject( + filingObject.counts, + countyName, + filingType, + docketSheetNumUnique + ); + allFilingsForThisCountyObject.push(brokenOutFilingObject); + vueApp.createResponseObjectForFiling(brokenOutFilingObject.id); + } + } + } + // insert NOAs into filings + const filingsWithNOAs = vueApp.settings.groupNoas + ? vueApp.insertNOAsForEachCounty(allFilingsForThisCountyObject) + : vueApp.insertNOAsForEachDocket(allFilingsForThisCountyObject); + + //add all filings for this county to the returned filing object. + groupedFilings.push({ + county: countyName, + filings: filingsWithNOAs, + }); + } + return groupedFilings; +} + +export function csvData(vueApp) { + return vueApp.rawCounts.map(function (count) { + return { + Petitioner_Name: vueApp.petitioner['name'], + Petitioner_DOB: vueApp.petitioner.dob, + Petitioner_Address: vueApp.petitioner.addressString, + Petitioner_Phone: vueApp.responses.phone, + County: count.county, + Docket_Sheet_Number: count.docketSheetNum, + Count_Docket_Number: count.docketNum, + Filing_Type: filingNameFromType(count.filingType), + Count_Description: count.description, + Count_Statute_Title: count.titleNum, + Count_Statute_Section: count.sectionNum, + Offense_Class: offenseAbbreviationToFull(count.offenseClass), + Offense_Disposition: count.offenseDisposition, + Offense_Disposition_Date: count.dispositionDate, + }; + }); +} + +function csvFilename(petitioner) { + let date = new Date(); + return slugify( + 'filings for ' + petitioner.name + ' ' + date.toDateString() + '.csv' + ); +} + +//Grabs name for header of filing +function filingNameFromType(filingType) { + switch (filingType) { + case 'NoA': + return 'Notice of Appearance'; + case 'feeWaiver': + return 'Motion to Waive Legal Financial Obligations'; + case 'feeWaiverAffidavit': + return "Petitioner's Sworn Statement in Support of Motion to Waive Legal Financial Obligations"; + case 'StipExC': + return 'Stipulated Petition to Expunge Conviction'; + case 'ExC': + return 'Petition to Expunge Conviction'; + case 'StipExNC': + return 'Stipulated Petition to Expunge Non-Conviction'; + case 'ExNC': + return 'Petition to Expunge Non-Conviction'; + case 'StipExNCrim': + return 'Stipulated Petition to Expunge Non-Criminal Conviction'; + case 'ExNCrim': + return 'Petition to Expunge Non-Criminal Conviction'; + case 'StipSC': + return 'Stipulated Petition to Seal Conviction of Minor'; + case 'SC': + return 'Petition to Seal Conviction of Minor'; + case 'StipSCAdult': + return 'Stipulated Petition to Seal Conviction'; + case 'SCAdult': + return 'Petition to Seal Conviction'; + case 'StipSDui': + return 'Stipulated Petition to Seal DUI Conviction'; + case 'SDui': + return 'Petition to Seal DUI Conviction'; + case 'StipNegOp': + return 'Stipulated Petition to Seal Negligent Operation Conviction'; + case 'NegOp': + return 'Petition to Seal Negligent Operation Conviction'; + case 'X': + return 'Ineligible'; + default: + return 'None'; + } +} + +function filterAndMakeFilingObject( + counts, + county, + filingType, + docketSheetNum = '' +) { + let countsOnThisFiling = counts.filter( + (count) => + count.county == county && + count.filingType == filingType && + (docketSheetNum == '' || docketSheetNum == count.docketSheetNum) + ); + return makeFilingObject(countsOnThisFiling, filingType, county); +} + +export function dateFormatSimple(value) { + if (!value) return ''; + return dayjs(value).format('MM/DD/YYYY'); +} + +export function deleteCount(vueApp, countId) { + index = vueApp.saved.counts.findIndex((x) => x.uid === countId); + vueApp.saved.counts.splice(index, 1); +} + +export function detectChangesInChromeStorage(app) { + chrome.storage.onChanged.addListener(function (changes, namespace) { + let countsChange = changes['counts']; + let responsesChange = changes['responses']; + + if (countsChange === undefined && responsesChange === undefined) return; + if (countsChange !== undefined && countsChange.newValue === undefined) { + clearAll(); + return; + } + if (!document.hasFocus() || toRaw(app.saved.counts).length === 0 && Object.keys(toRaw(app.responses)).length === 0) { + app.loadAll(function () {}); + } + }); +} + +/** + * Replaces console.log() statements with a wrapper that prevents the extension from logging + * to the console unless it was installed by a developer. This will keep the console clean; a + * practice recommended for chrome extensions. + * + * @param {any} data Data to log to the console + * @todo find a way to make this reusuable, then delete the duplicate fn() in popup.js + */ +export function devLog(data) { + // see https://developer.chrome.com/extensions/management#method-getSelf + chrome.management.getSelf(function (self) { + if (this.installType == 'development') { + console.log(data); + } + }); +} + +export function getError() { + return 'TOOD: getError should work :('; // TODO: The code below explodes, so just no-op for now + // return new Error().stack + // .split('\n')[1] + // .split('filings.js')[1] + // .replace(')', '') +} + +export function getNextNotaryDate() { + let currentDate = dayjs(); + let janThisYear = dayjs(currentDate.format('YYYY') + '-01-31'); + + if (currentDate.isBefore(janThisYear) && isOdd(currentDate)) { + return janThisYear.format('MMMM DD, YYYY'); + } else if (!isOdd(currentDate)) { + return dayjs(janThisYear).add(1, 'years').format('MMMM DD, YYYY'); + } else if (currentDate.isAfter(janThisYear) && isOdd(currentDate)) { + return dayjs(janThisYear).add(2, 'years').format('MMMM DD, YYYY'); + } + function isOdd(num) { + let numInt = parseInt(num.format('YYYY')); + return numInt % 2; + } +} + +function groupByCounty(counts) { + let allCounties = counts.map(function (count) { + return count.county; + }); + return allCounties.filter((v, i, a) => a.indexOf(v) === i); +} + +function groupByFilingType(counts) { + let allCounts = counts.map(function (count) { + return count.filingType; + }); + return allCounts.filter((v, i, a) => a.indexOf(v) === i); +} + +/* Used when there are more docket numbers than will fit on a single Notice of Appearance + * form. This takes an array[counts], and returns an array[array[counts]]. For example, if + * the `max` is 10 dockets, then each inner array would have all the counts belonging to the + * next 10 dockets. + */ +function groupCountsByMaxDocketNumber(counts, maxLength) { + let allDocketNums = allDocketNumsObject(counts); + let numDocketGroups = Math.ceil(allDocketNums.length / maxLength); + let docketGroups = []; + + // divide all counts into arrays grouped by the `maxLength` number of dockets + for (let i = 0; i < numDocketGroups; i++) { + let start = i * maxLength; + let end = Math.min(i * maxLength + maxLength, allDocketNums.length); + let dockets = allDocketNums.slice(start, end); + let docketNums = dockets.map((docket) => docket.num); + let countGroup = counts.filter((f) => docketNums.includes(f.docketNum)); + docketGroups.push(countGroup); + } + + return docketGroups; +} + +export function handleNewDocketNums(sheetNum) { + if (sheetNum.toLowerCase().includes('-cr-')) { + return sheetNum.split(' ')[0]; + } else { + return sheetNum; + } +} + +export function handlePrintMacro(app) { + $(document).on('keydown', function (e) { + if ( + (e.ctrlKey || e.metaKey) && + (e.key == 'p' || e.charCode == 16 || e.charCode == 112 || e.keyCode == 80) + ) { + e.cancelBubble = true; + e.preventDefault(); + e.stopImmediatePropagation(); + app.printDocument(); + } + }); +} + +// TODO: implement or delete +export function initAfterFilingRefresh() { + setInitialExpandForTextAreas(); + initScrollDetection(); +} + +export function initAfterVue() { + //sets intital height of all text areas to show all text. + document.addEventListener('DOMContentLoaded', () => { + initScrollDetection(); + setInitialExpandForTextAreas(); + initTextAreaAutoExpand(); + initSmoothScroll(); + }); +} + +export function initScrollDetection() { + // initates the scrollspy for the filing-nav module. + // see: https://www.npmjs.com/package/gumshoejs#nested-navigation + let spy = new Gumshoe('#filing-nav a', { + nested: true, + nestedClass: 'active-parent', + offset: 200, // how far from the top of the page to activate a content area + reflow: true, // will update when the navigation chages (eg, user adds/changes a petition, or consolidates petitions/NOAs) + }); +} + +export function initSmoothScroll() { + let scroll = new SmoothScroll('a[href*="#"]', { + offset: 150, + durationMax: 300, + }); +} + +export function initTextAreaAutoExpand() { + document.addEventListener( + 'input', + function (event) { + if (event.target.tagName.toLowerCase() !== 'textarea') return; + autoExpand(event.target); + }, + false + ); +} + +function isEligible(filingType) { + return filingType != 'X'; +} + +function isFileable(filingType) { + return isSupported(filingType) && isEligible(filingType); +} + +function isSupported(filingType) { + switch (filingType) { + case 'NoA': + case 'StipExC': + case 'ExC': + case 'StipExNC': + case 'ExNC': + case 'StipExNCrim': + case 'ExNCrim': + case 'StipSC': + case 'StipSCAdult': + case 'StipSDui': + case 'SC': + case 'SCAdult': + case 'SDui': + case 'NegOp': + case 'StipNegOp': + case 'X': + return true; + default: + return false; + } +} + +function isStipulated(filingType) { + return ( + filingType == 'StipExC' || + filingType == 'StipExNC' || + filingType == 'StipExNCrim' || + filingType == 'StipSC' || + filingType == 'StipSCAdult' || + filingType == 'StipSDui' || + filingType == 'StipNegOp' + ); +} + +export function loadAll(vueApp, callback) { + if (callback === undefined) { + callback = function () {}; + } + devLog(localStorage.getItem('localExpungeVTSettings')); + localResult = JSON.parse(localStorage.getItem('localExpungeVTSettings')); + if (localResult !== undefined && localResult !== '' && localResult !== null) { + vueApp.settings = localResult; + } else { + vueApp.saveSettings(); + } + + chrome.storage.local.get(function (result) { + //test if we have any data + devLog('loading all'); + devLog(JSON.stringify(result)); + if (result.counts !== undefined) { + devLog(result.counts); + vueApp.saved = result.counts; + } + + if (result.responses !== undefined) { + vueApp.responses = result.responses; + } + + callback(); + //this.$nextTick(function () { + //call any vanilla js functions that need to run after vue is all done setting up. + //initAfterVue(); + //}); + }); +} + +export function linesBreaksFromArray(array) { + let string = ''; + let delimiter = '\r\n'; + let i; + for (i = 0; i < array.length; i++) { + if (i > 0) { + string += delimiter; + } + string += array[i]; + } + return string; +} + +export function lowercase(value) { + if (!value) return ''; + value = value.toString(); + return value.charAt(0).toLowerCase() + value.slice(1); +} + +/* + * Creates a filing object from data provided. + * NOTE: will fail without explaination on civil violations b/c this presumes `counts` is a non-empty array + */ +export function makeFilingObject(counts, filingType, county) { + let countsOnThisFiling = counts; + let numCounts = countsOnThisFiling.length; + let docketNums = allDocketNumsObject(countsOnThisFiling); + let numDockets = docketNums.length; + let docketSheetNums = allDocketSheetNumsObject(countsOnThisFiling); + let numDocketSheets = docketSheetNums.length; + let isMultipleCounts = numCounts > 1; + let filingId = filingType + '-' + county + '-' + docketNums[0].num; + + return { + id: filingId, + type: filingType, + title: filingNameFromType(filingType), + county: county, + numCounts: numCounts, + numDockets: numDockets, + numDocketSheets: numDocketSheets, + multipleCounts: isMultipleCounts, + numCountsString: pluralize('Count', numCounts), + numDocketsString: pluralize('Docket', numDockets), + isStipulated: isStipulated(filingType), + isEligible: isEligible(filingType), + docketNums: docketNums, + docketSheetNums: docketSheetNums, + counts: countsOnThisFiling, + }; +} + +export function maxDate() { + let date = dayjs().format("YYYY-MM-DD"); + return date; +} + +function offenseAbbreviationToFull(offenseClass) { + switch (offenseClass) { + case 'mis': + return 'Misdemeanor'; + case 'fel': + return 'Felony'; + default: + return ''; + } +} + +export function openManagePage() { + chrome.tabs.query( + { + active: true, + currentWindow: true, + }, + (tabs) => { + let index = tabs[0].index; + chrome.tabs.create({ + url: chrome.runtime.getURL('./manage-counts.html'), + index: index + 1, + }); + } + ); +} + +export function pluralize(word, num) { + let phrase = num + ' ' + word; + if (num > 1) return phrase + 's'; + return phrase; +} + +export function nl2br(rawStr) { + let breakTag = '
'; + return (rawStr + '').replace( + /([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, + '$1' + breakTag + '$2' + ); +} + +export function openPetitionsPage() { + chrome.tabs.query( + { + active: true, + currentWindow: true, + }, + (tabs) => { + let index = tabs[0].index; + chrome.tabs.create({ + url: chrome.runtime.getURL('./filings.html'), + index: index + 1, + }); + } + ); +} + +export function resetSettings(vueApp, element) { + if (confirm('Are you sure you want to reset setting to the defaults?')) { + localStorage.removeItem(['localExpungeVTSettings']); + vueApp.settings = { + attorney: '', + attorneyAddress: '', + attorneyPhone: '', + footer1: '- Generated by ExpungeVT -', + footer2: '- A Code for BTV Project -', + role: 'AttyConsult', + forVla: false, + }; + } +} + +export function saveCounts(counts) { + if (document.hasFocus()) { + chrome.storage.local.set({ + counts: counts, + }); + } +} + +export function saveHtml(vueApp) { + let dataPojo = { + saved: vueApp.saved, + responses: vueApp.responses, + }; + saveAllCountsToHtml(JSON.stringify(dataPojo)); +} + +export function saveResponses(responses) { + devLog('save responses' + getError()); + if (document.hasFocus()) { + chrome.storage.local.set({ + responses: responses, + }); + } +} + +export function saveSettings(settings) { + settingString = JSON.stringify(settings); + if (document.hasFocus()) { + localStorage.setItem('localExpungeVTSettings', settingString); + } +} + +export function setInitialExpandForTextAreas() { + //sets the default size for all text areas based on their content. + //call this after vue has initialized and displayed + let textAreas = document.getElementsByTagName('textarea'); + for (let index in textAreas) { + let textArea = textAreas[index]; + if (textArea === undefined) return; + autoExpand(textArea); + } +} + +export function sinceNow(value) { + if (!value) return ''; + + let fromTime = dayjs(value).diff(dayjs(), 'milliseconds'); + let duration = dayjs.duration(fromTime); + let years = duration.years() / -1; + let months = duration.months() / -1; + let days = duration.days() / -1; + if (years > 0) { + let Ys = years == 1 ? years + 'y ' : years + 'y '; + let Ms = months == 1 ? months + 'm ' : months + 'm '; + return Ys + Ms; + } else { + if (months > 0) return months == 1 ? months + 'm ' : months + 'm '; + else return days == 1 ? days + 'd ' : days + 'd '; + } +} + +export function slugify(string) { + return string.replace(/\s+/g, '-').toLowerCase(); +} + +export function stringAgeInYearsAtDate(date, dob) { + if (!date) return ''; + if (!dob) return ''; + let fromTime = dayjs(date).diff(dayjs(dob)); + let duration = dayjs.duration(fromTime); + return (duration.asDays() / 365.25).toFixed(0) + ' yo'; +} + +export function toCountyCode(value) { + if (!value) return ''; + return countyCodeFromCounty(value); +} + +export function todayDate() { + date = dayjs().format('MMMM D[, ]YYYY'); + return date; +} + +export function uppercase(value) { + if (!value) return ''; + value = value.toString(); + return value.charAt(0).toUpperCase() + value.slice(1); +} From a960178c7f0c0f8d2bd9880bc6b86929d666b1dd Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Mon, 24 Jul 2023 18:45:01 -0400 Subject: [PATCH 04/14] Added test script target to package.json. --- extensionDirectory/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensionDirectory/package.json b/extensionDirectory/package.json index 67b7806f..5702f8e1 100644 --- a/extensionDirectory/package.json +++ b/extensionDirectory/package.json @@ -28,7 +28,7 @@ "scripts": { "pretty-up": "npx prettier --write \"./**/*.{js,html,css}\"", "pretty-check": "npx prettier --check \"./**/*.{js,html,css}\"", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node ./node_modules/mocha/bin/mocha.js --require mocha-fixtures.mjs --config .mocharc.js **/*.spec.mjs", "build": "node build.js", "build:watch": "echo 'TODO: Setup watch building'", "postbuild": "cp ./package.json ./build/ && npm i --production --prefix=./build" From 76e77702a2d120b69a65d2b6a1e81105fe6241e9 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:13:43 -0400 Subject: [PATCH 05/14] Add Pinia and object-hash as dependencies. Update flag in postbuild lifecycle for omitting development dependencies. --- extensionDirectory/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensionDirectory/package.json b/extensionDirectory/package.json index 5702f8e1..dad943fd 100644 --- a/extensionDirectory/package.json +++ b/extensionDirectory/package.json @@ -10,6 +10,8 @@ "dayjs": "^1.11.9", "gumshoejs": "^5.1.2", "jquery": "^3.6.0", + "object-hash": "^3.0.0", + "pinia": "^2.1.7", "popper.js": "^1.16.1", "smooth-scroll": "^16.1.3", "vue": "^3.1.0" @@ -31,7 +33,7 @@ "test": "node ./node_modules/mocha/bin/mocha.js --require mocha-fixtures.mjs --config .mocharc.js **/*.spec.mjs", "build": "node build.js", "build:watch": "echo 'TODO: Setup watch building'", - "postbuild": "cp ./package.json ./build/ && npm i --production --prefix=./build" + "postbuild": "cp ./package.json ./build/ && npm i --omit=dev --prefix=./build" }, "keywords": [], "author": "", From 03744801a1a1b9f4b6720864f36b89d33fc0fc2a Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:14:13 -0400 Subject: [PATCH 06/14] Add a Pinia store for Expunge data. --- extensionDirectory/store.mjs | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 extensionDirectory/store.mjs diff --git a/extensionDirectory/store.mjs b/extensionDirectory/store.mjs new file mode 100644 index 00000000..2d355a2a --- /dev/null +++ b/extensionDirectory/store.mjs @@ -0,0 +1,37 @@ +import { defineStore } from 'pinia' + +export const useDataStore = defineStore('data', { + state: () => {return { + settings: { + attorney: '', + attorneyAddress: '', + attorneyPhone: '', + footer1: '- Generated by ExpungeVT -', + footer2: '- A Code for BTV Project -', + customNote: '', + role: 'AttyConsult', + forVla: true, + emailConsent: true, + affidavitRequired: false, + groupCounts: false, + groupNoas: false, + termsChecked: false, + }, + saved: { + defName: '', + defAddress: [''], + defDOB: '', + counts: [], + defEmail: '', + }, + responses: {}, + fees: {}, + fines: {}, + countiesContact: {}, + popupHeadline: '', + roleCoverLetterText: {}, + coverLetterContent: {}, + stipDef: {}, + }} +} +) \ No newline at end of file From d6408250e42ccbe0cf2b17cd860d9b643e687b92 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:16:20 -0400 Subject: [PATCH 07/14] Register Pinia for use within the Vue app when 'registering' the manage-counts 'local component'. --- extensionDirectory/manage-counts.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensionDirectory/manage-counts.js b/extensionDirectory/manage-counts.js index aee6f19d..3669edb9 100644 --- a/extensionDirectory/manage-counts.js +++ b/extensionDirectory/manage-counts.js @@ -2,7 +2,11 @@ const jQuery = require('jquery'); window.$ = jQuery; window.jQuery = jQuery; import { createApp } from 'vue'; +import { createPinia } from 'pinia'; import ManageCounts from './components/manage-counts.vue'; -var app = createApp(ManageCounts).mount('#filing-app'); \ No newline at end of file +const app = createApp(ManageCounts); +const pinia = createPinia(); +app.use(pinia); +app.mount('#filing-app'); \ No newline at end of file From 5861ec7eac9ad0dea7c77b41dbc0efeeae428bf4 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:16:51 -0400 Subject: [PATCH 08/14] Register Pinia for use within the Vue app when 'registering' the filings 'local component'. --- extensionDirectory/filings.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensionDirectory/filings.js b/extensionDirectory/filings.js index c46f7b73..8e214b30 100644 --- a/extensionDirectory/filings.js +++ b/extensionDirectory/filings.js @@ -2,7 +2,11 @@ const jQuery = require('jquery'); window.$ = jQuery; window.jQuery = jQuery; import { createApp } from 'vue'; +import { createPinia } from 'pinia'; import Filings from './components/filings.vue'; -var app = createApp(Filings).mount('#filing-app'); \ No newline at end of file +const app = createApp(Filings); +const pinia = createPinia(); +app.use(pinia); +app.mount('#filing-app'); \ No newline at end of file From bac2e68286370706ee21f10845ec4787dc63a2c5 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:23:33 -0400 Subject: [PATCH 09/14] Register Pinia for use within the Vue app when 'registering' the pop-ups 'local component'. --- extensionDirectory/popup.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extensionDirectory/popup.js b/extensionDirectory/popup.js index 2f6b154a..a4ceee1c 100644 --- a/extensionDirectory/popup.js +++ b/extensionDirectory/popup.js @@ -2,6 +2,7 @@ import './popup.css'; import $ from 'jquery'; import dayjs from 'dayjs'; import { createApp } from 'vue'; +import { createPinia } from 'pinia'; import PopupApp from './components/popup.vue'; @@ -335,10 +336,7 @@ function getOdysseyCountInfo(docket, docketUrl) { const countyTitle = docket.find('#roa-header div').get(); county = countyTitle[0].textContent.trim().replace(' Unit', ''); docketSheetNum = docketNum + ' ' + countyCodeFromCounty(county); - console.log('county', county); } - console.log('docketlog', docketNum, county); - // parse each offense let offenseArray = []; caseOffenseTable.find(' tbody > tr').each(function (i) { @@ -712,4 +710,7 @@ let Base64 = { }; //Vue app -let app = createApp(PopupApp).mount('#filing-app'); +const app = createApp(PopupApp) +const pinia = createPinia() +app.use(pinia) +app.mount('#filing-app'); From 32eaea6837027812604e3438271d708901d18e32 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:24:36 -0400 Subject: [PATCH 10/14] Add the Pinia store file /store.mjs to the esbuild file. --- extensionDirectory/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensionDirectory/build.js b/extensionDirectory/build.js index 71a1c194..9252454c 100644 --- a/extensionDirectory/build.js +++ b/extensionDirectory/build.js @@ -3,7 +3,7 @@ const esbuild = require('esbuild'); const copyStaticFiles = require('esbuild-copy-static-files') esbuild.build({ - entryPoints: ['background.js', 'index.js', 'manage-counts.js', 'payload.js', 'filings.js', 'saveFile.js'], + entryPoints: ['background.js', 'store.mjs', 'index.js', 'manage-counts.js', 'payload.js', 'filings.js', 'saveFile.js'], bundle: true, outdir: 'build/', plugins: [vuePlugin(), From f228397ffb1addf080f6d66385502ea2b9bdb718 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:27:47 -0400 Subject: [PATCH 11/14] Add a parameter to the detectChangesInChromeStorage(..) method to designate if the method is registering a listener on storage changes for a Chrome Extension pop-up. Conditionally update the app's state. --- extensionDirectory/utils.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/extensionDirectory/utils.js b/extensionDirectory/utils.js index c2fdf50e..eb1dc801 100644 --- a/extensionDirectory/utils.js +++ b/extensionDirectory/utils.js @@ -1,3 +1,5 @@ + +import hash from 'object-hash'; import Gumshoe from 'gumshoejs'; import dayjs from 'dayjs'; import SmoothScroll from 'smooth-scroll'; @@ -326,7 +328,7 @@ export function deleteCount(vueApp, countId) { vueApp.saved.counts.splice(index, 1); } -export function detectChangesInChromeStorage(app) { +export function detectChangesInChromeStorage(app, isChromeExtensionPopup) { chrome.storage.onChanged.addListener(function (changes, namespace) { let countsChange = changes['counts']; let responsesChange = changes['responses']; @@ -336,8 +338,18 @@ export function detectChangesInChromeStorage(app) { clearAll(); return; } - if (!document.hasFocus() || toRaw(app.saved.counts).length === 0 && Object.keys(toRaw(app.responses)).length === 0) { - app.loadAll(function () {}); + if (!document.hasFocus() ) { + app.loadAll(); + } else if (isChromeExtensionPopup && countsChange !== undefined) { + if (countsChange.oldValue !== undefined) { + let newDigest = hash(countsChange.newValue.counts); + let oldDigest = hash(countsChange.oldValue.counts); + if (newDigest !== oldDigest) { + app.loadAll(); + } + return; + } + app.loadAll(); } }); } From 8ae10c73ed710a6d0baf0611cd8a93205049d9f1 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:28:58 -0400 Subject: [PATCH 12/14] Update the manage-counts Vue SFC to populate state with a Pinia store. --- .../components/manage-counts.vue | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/extensionDirectory/components/manage-counts.vue b/extensionDirectory/components/manage-counts.vue index 50a109a4..fdc7c970 100644 --- a/extensionDirectory/components/manage-counts.vue +++ b/extensionDirectory/components/manage-counts.vue @@ -7,6 +7,7 @@ import SmoothScroll from 'smooth-scroll'; import 'bootstrap'; import 'bootstrap4-toggle'; import { toRaw } from 'vue'; +import { storeToRefs } from 'pinia'; import { confirmDeleteCount, @@ -30,40 +31,23 @@ import { toCountyCode, } from '../utils'; +import { useDataStore } from '../store.mjs'; + export default { // el: '#filing-app', data() { return { - settings: { - attorney: '', - attorneyAddress: '', - attorneyPhone: '', - footer1: '- Generated by ExpungeVT -', - footer2: '- A Code for BTV Project -', - customNote: '', - role: 'AttyConsult', - forVla: true, - emailConsent: true, - affidavitRequired: false, - groupCounts: false, - groupNoas: false, - }, - saved: { - defName: '', - defAddress: [''], - defDOB: '', - counts: [], - defEmail: '', - }, - responses: {}, - fees: {}, - fines: {}, - countiesContact: {}, - popupHeadline: '', - roleCoverLetterText: {}, - coverLetterContent: {}, - stipDef: {}, - }; + settings, + saved, + responses, + fees, + fines, + countiesContact, + popupHeadline, + roleCoverLetterText, + coverLetterContent, + stipDef, + } = storeToRefs(useDataStore()); }, watch: { // Affects "consolidation" checkboxes in filings page header @@ -131,7 +115,7 @@ export default { mounted() { devLog('App mounted!'); this.loadAll(); - detectChangesInChromeStorage(this); + detectChangesInChromeStorage(this, false); handlePrintMacro(this); //This is to make sure dynamically created table are unique across tab in order to avoid errors this.uniqueId = this._uid; From 7313ad6f35bc3638ffbf21b130c8d3af0d1f1445 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:29:35 -0400 Subject: [PATCH 13/14] Update the filings Vue SFC to populate state with a Pinia store. --- extensionDirectory/components/filings.vue | 47 ++++++++--------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/extensionDirectory/components/filings.vue b/extensionDirectory/components/filings.vue index 05699d55..b5a6dab5 100644 --- a/extensionDirectory/components/filings.vue +++ b/extensionDirectory/components/filings.vue @@ -10,6 +10,7 @@ import filingDatedCity from './filing-dated-city.vue'; import filingFooter from './filing-footer.vue'; import filingNav from './filing-nav.vue'; import filingTypeHeading from './filing-type-heading.vue'; +import { storeToRefs } from 'pinia'; import { confirmClearData, @@ -48,6 +49,8 @@ import { uppercase, } from '../utils'; +import { useDataStore } from '../store.mjs'; + export default { // el: '#filing-app', components: { @@ -60,36 +63,17 @@ export default { }, data() { return { - settings: { - attorney: '', - attorneyAddress: '', - attorneyPhone: '', - footer1: '- Generated by ExpungeVT -', - footer2: '- A Code for BTV Project -', - customNote: '', - role: 'AttyConsult', - forVla: false, - emailConsent: true, - affidavitRequired: false, - groupCounts: false, - groupNoas: false, - }, - saved: { - defName: '', - defAddress: [''], - defDOB: '', - counts: [], - defEmail: '', - }, - responses: {}, - fees: {}, - fines: {}, - countiesContact: {}, - popupHeadline: '', - roleCoverLetterText: {}, - coverLetterContent: {}, - stipDef: {}, - }; + settings, + saved, + responses, + fees, + fines, + countiesContact, + popupHeadline, + roleCoverLetterText, + coverLetterContent, + stipDef, + } = storeToRefs(useDataStore()); }, watch: { // Affects "consolidation" checkboxes in filings page header @@ -156,10 +140,9 @@ export default { mounted() { devLog('App mounted!'); this.loadAll(); - detectChangesInChromeStorage(this); + detectChangesInChromeStorage(this, false); handlePrintMacro(this); initAfterVue(); - //This is to make sure dynamically created table are unique across tab in order to avoid errors this.uniqueId = this._uid; }, From 2027fe0051290df02035712a53d7a9af73031537 Mon Sep 17 00:00:00 2001 From: NathanWEdwards Date: Sat, 4 Nov 2023 13:30:10 -0400 Subject: [PATCH 14/14] Update the popup Vue SFC to populate state with a Pinia store. --- extensionDirectory/components/popup.vue | 50 ++++++++----------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/extensionDirectory/components/popup.vue b/extensionDirectory/components/popup.vue index 722a1d2e..65eebc4b 100644 --- a/extensionDirectory/components/popup.vue +++ b/extensionDirectory/components/popup.vue @@ -1,6 +1,7 @@