diff --git a/.gitignore b/.gitignore index 544e3985..8e4c47f9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ pids *.pid *.seed *.pid.lock +screenshots/ # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -71,4 +72,7 @@ extensionDirectory-build/index.js extensionDirectory-build/payload.js # chrome store files -build.zip \ No newline at end of file +build.zip + +# Puppeteer store files +.cache/ \ No newline at end of file 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/.mocharc.js b/extensionDirectory/.mocharc.js new file mode 100644 index 00000000..099acc01 --- /dev/null +++ b/extensionDirectory/.mocharc.js @@ -0,0 +1,4 @@ +module.exports = { + 'watch-files': ['**/*spec.js'], + 'timeout': 0, +} \ No newline at end of file diff --git a/extensionDirectory/.puppeteerrc.cjs b/extensionDirectory/.puppeteerrc.cjs new file mode 100644 index 00000000..bfb3194d --- /dev/null +++ b/extensionDirectory/.puppeteerrc.cjs @@ -0,0 +1,5 @@ +const {join} = require('path'); + +module.exports = { + cacheDirectory: join(__dirname, '.cache', 'puppeteer'), +}; \ No newline at end of file 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(), diff --git a/extensionDirectory/components.js b/extensionDirectory/components.js deleted file mode 100644 index fbf0d165..00000000 --- a/extensionDirectory/components.js +++ /dev/null @@ -1,279 +0,0 @@ -//Vue Components -import Vue from 'vue/dist/vue'; -import moment from 'moment'; - -Vue.component('docket-caption', { - template: `
-
-

STATE OF VERMONT,

-

Respondent

-

v.

-

{{name}},

-

Petitioner

-
-
- `, - props: ['name'], -}); - -Vue.component('filing-nav', { - template: `
-
    - -
    - -
-
- `, - filters: { - navTitleFilter(txt) { - const trimStip = txt.startsWith('Stipulated ') - ? txt.substring(11) + ' (Stip)' - : txt; - const trimPetion = trimStip.startsWith('Petition to ') - ? trimStip.substring(12) - : trimStip; - return trimPetion; - }, - /** - * The subtitle for each petition in the nav varies depending on several factors. - * @todo Finish selecting the best subtitles after all other PRs are merged in - * @param {object} filing All the info for the current filing - */ - petitionCountFilter(filing) { - // NoA subtitle - if (filing.type == 'NoA') { - // default (ungrouped): "####-##-## (N Counts)" - if (!this.app.groupCounts && !this.app.groupNoas) { - return `${filing.docketNums[0].num} (${filing.numCountsString})`; - } - // two cases, same text: "N Dockets (N Counts)" - // 1. groupNoas only - // 2. groupCounts & groupNoas - else { - return `${filing.docketNums.length} Dockets (${filing.numCountsString})`; - } - } - // Petition subtitles - else { - // default (ungrouped): "N Counts" (or blank if counts == 1) - if (!this.app.groupCounts && !this.app.groupNoas) { - return filing.counts.length > 1 - ? `${filing.counts.length} Counts` - : ''; - } - // groupCounts: "####-##-##" - else if (this.app.groupNoas && !this.app.groupCounts) { - return `${filing.docketNums[0].num}`; - } - // groupCounts: "N Dockets (N Counts)" - else if (this.app.groupNoas && this.app.groupCounts) { - return `${filing.docketNums.length} Dockets (${filing.numCountsString})`; - } - } - }, - }, - props: ['filings'], -}); - -Vue.component('filing-footer', { - template: `
-

Stipulated and agreed this         day of                             , 20      .

-
-

State's Attorney/Attorney General

-
-
- - `, - 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 old mode 100755 new mode 100644 index 9668db59..b5a6dab5 --- a/extensionDirectory/components/filings.vue +++ b/extensionDirectory/components/filings.vue @@ -1,2749 +1,2167 @@ - - - + + + diff --git a/extensionDirectory/components/manage-counts.vue b/extensionDirectory/components/manage-counts.vue old mode 100755 new mode 100644 index df1efd7f..fdc7c970 --- a/extensionDirectory/components/manage-counts.vue +++ b/extensionDirectory/components/manage-counts.vue @@ -1,622 +1,519 @@ -filingNav - - \ No newline at end of file +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 @@ @@ -484,6 +346,7 @@ export default {

Name:

@@ -493,7 +356,9 @@ export default {
@@ -509,6 +374,7 @@ export default {
@@ -667,6 +533,8 @@ export default {
Offense Date:
 Not Entered + {{ dateFormatSimple(count.allegedOffenseDate) }} {{ dateFormatSimple(count.allegedOffenseDate) }} ({{ stringAgeInYearsAtDate( @@ -679,6 +547,8 @@ export default {
Arrest/Citation Date:
 Not Entered + {{ dateFormatSimple(count.arrestCitationDate) }} {{ dateFormatSimple(count.arrestCitationDate) }} ({{ stringAgeInYearsAtDate( @@ -691,6 +561,8 @@ export default {
Disposition Date:
 Pending + {{ dateFormatSimple(count.dispositionDate) }} {{ dateFormatSimple(count.dispositionDate) }} ({{ stringAgeInYearsAtDate(count.dispositionDate, saved.defDOB) diff --git a/extensionDirectory/csv.js b/extensionDirectory/csv.js index 0be40b2c..56123505 100644 --- a/extensionDirectory/csv.js +++ b/extensionDirectory/csv.js @@ -1,5 +1,5 @@ function convertArrayOfObjectsToCSV(args) { - var result, ctr, keys, columnDelimiter, lineDelimiter, contentQuoting, data; + let result, ctr, keys, columnDelimiter, lineDelimiter, contentQuoting, data; data = args.data || null; if (data == null || !data.length) { @@ -32,14 +32,14 @@ function convertArrayOfObjectsToCSV(args) { } function downloadCSV(args) { - var data_array = args.data_array; - var csv = convertArrayOfObjectsToCSV({ + let data_array = args.data_array; + let csv = convertArrayOfObjectsToCSV({ data: data_array, }); if (csv == null) return; - var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); - var url = URL.createObjectURL(blob); + let blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + let url = URL.createObjectURL(blob); chrome.downloads.download({ url: url, filename: args.filename, 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 diff --git a/extensionDirectory/images/ACT_logo_color.png b/extensionDirectory/images/ACT_logo_color.png old mode 100755 new mode 100644 diff --git a/extensionDirectory/images/icon_128x.png b/extensionDirectory/images/icon_128.png similarity index 100% rename from extensionDirectory/images/icon_128x.png rename to extensionDirectory/images/icon_128.png diff --git a/extensionDirectory/images/icon_16x.png b/extensionDirectory/images/icon_16.png similarity index 100% rename from extensionDirectory/images/icon_16x.png rename to extensionDirectory/images/icon_16.png diff --git a/extensionDirectory/images/icon_32x.png b/extensionDirectory/images/icon_32.png similarity index 100% rename from extensionDirectory/images/icon_32x.png rename to extensionDirectory/images/icon_32.png diff --git a/extensionDirectory/images/icon_48x.png b/extensionDirectory/images/icon_48.png similarity index 100% rename from extensionDirectory/images/icon_48x.png rename to extensionDirectory/images/icon_48.png diff --git a/extensionDirectory/jsconfig.json b/extensionDirectory/jsconfig.json old mode 100755 new mode 100644 index cd04420a..752e89d7 --- a/extensionDirectory/jsconfig.json +++ b/extensionDirectory/jsconfig.json @@ -1,9 +1,9 @@ -{ - "allowJs": true, - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./*"] - } - } +{ + "allowJs": true, + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + } } \ No newline at end of file diff --git a/extensionDirectory/manage-counts.js b/extensionDirectory/manage-counts.js old mode 100755 new mode 100644 index d1e40d18..3669edb9 --- a/extensionDirectory/manage-counts.js +++ b/extensionDirectory/manage-counts.js @@ -1,8 +1,12 @@ -const jQuery = require('jquery'); -window.$ = jQuery; -window.jQuery = jQuery; -import { createApp } from 'vue'; - -import ManageCounts from './components/manage-counts.vue'; - -var app = createApp(ManageCounts).mount('#filing-app'); \ No newline at end of file +const jQuery = require('jquery'); +window.$ = jQuery; +window.jQuery = jQuery; +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; + +import ManageCounts from './components/manage-counts.vue'; + +const app = createApp(ManageCounts); +const pinia = createPinia(); +app.use(pinia); +app.mount('#filing-app'); \ No newline at end of file diff --git a/extensionDirectory/mocha-fixtures.mjs b/extensionDirectory/mocha-fixtures.mjs new file mode 100644 index 00000000..ce41e85a --- /dev/null +++ b/extensionDirectory/mocha-fixtures.mjs @@ -0,0 +1,11 @@ +import { mkdirSync, access, constants } from 'fs'; + +const screenshots = './screenshots' + +export async function mochaGlobalSetup() { + access(screenshots, constants.F_OK, (err) => { + if (err) { + mkdirSync(screenshots); + } + }) +} \ No newline at end of file diff --git a/extensionDirectory/package.json b/extensionDirectory/package.json index 619df0d3..dad943fd 100644 --- a/extensionDirectory/package.json +++ b/extensionDirectory/package.json @@ -7,28 +7,33 @@ "@fortawesome/fontawesome-free": "^5.13.0", "bootstrap": "^4.4.1", "bootstrap4-toggle": "^3.6.1", + "dayjs": "^1.11.9", "gumshoejs": "^5.1.2", "jquery": "^3.6.0", - "moment": "^2.29.4", + "object-hash": "^3.0.0", + "pinia": "^2.1.7", "popper.js": "^1.16.1", "smooth-scroll": "^16.1.3", "vue": "^3.1.0" }, "devDependencies": { "@vue/compiler-sfc": "^3.2.37", + "chai": "^4.3.7", "esbuild": "^0.17.5", "esbuild-copy-static-files": "^0.1.0", "esbuild-plugin-vue3": "^0.3.0", + "mocha": "^10.2.0", "npm-run-all": "^4.1.5", - "prettier": "^2.0.5" + "prettier": "^2.0.5", + "puppeteer": "^20.9.0" }, "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" + "postbuild": "cp ./package.json ./build/ && npm i --omit=dev --prefix=./build" }, "keywords": [], "author": "", diff --git a/extensionDirectory/payload.js b/extensionDirectory/payload.js index a55abfa3..adec7ecf 100644 --- a/extensionDirectory/payload.js +++ b/extensionDirectory/payload.js @@ -30,7 +30,7 @@ if (docketData.url.startsWith('file')) { if (title === 'ExpungeVT Case Record') { docketData.domain = 'expungeVtRecord'; } else { - var answer = window.confirm( + let answer = window.confirm( 'This does not look like a case file. Are you sure you want to proceed?' ); if (answer) { diff --git a/extensionDirectory/popup.js b/extensionDirectory/popup.js index 7b49fe0f..a4ceee1c 100644 --- a/extensionDirectory/popup.js +++ b/extensionDirectory/popup.js @@ -1,7 +1,8 @@ import './popup.css'; import $ from 'jquery'; -import moment from 'moment'; +import dayjs from 'dayjs'; import { createApp } from 'vue'; +import { createPinia } from 'pinia'; import PopupApp from './components/popup.vue'; @@ -124,13 +125,13 @@ function appendDataWithConfirmation(newData, oldData) { return oldData; } - var returnData = oldData; - var newCounts = newData.counts; - var totalNumMatchingExistingCounts = 0; + let returnData = oldData; + let newCounts = newData.counts; + let totalNumMatchingExistingCounts = 0; for (count in newCounts) { - var currentCount = newCounts[count]; + let currentCount = newCounts[count]; devLog(currentCount.uid); - var numMatchingExistingCounts = oldData.counts.filter( + let numMatchingExistingCounts = oldData.counts.filter( (count) => count.uid === currentCount.uid ).length; if (numMatchingExistingCounts == 0) { @@ -148,8 +149,8 @@ function appendDataWithConfirmation(newData, oldData) { return returnData; function isSamePetitioner() { - var oldName = oldData['defName']; - var newName = newData['defName']; + let oldName = oldData['defName']; + let newName = newData['defName']; if (oldName != newName) { return confirm( `"The name on the counts you are trying to add is ${newName}, which is not the same as ${oldName}. Are you sure you want to continue?` @@ -202,7 +203,7 @@ class PetitionerCount { // guid is a globally unique string this.guid = guid; // eg: "3abef45f-187d-b0e4-9e2c-969c158acded" - // a variety of docket info + // a letiety of docket info this.docketSheetNum = docketSheetNum; // eg: "1899-5-12 Cncr" this.docketCounty = docketCounty; // eg: "Cncr" this.docketNum = docketNum; // eg: "1899-5-12" @@ -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) { @@ -514,7 +512,7 @@ function devLog(data) { * @param {string} date A date in the format 'MM/DD/YYYY' */ function formatDate(date) { - return moment(date, 'MM/DD/YYYY').format('YYYY-MM-DD'); + return dayjs(date, 'MM/DD/YYYY').format('YYYY-MM-DD'); } /** @@ -522,7 +520,7 @@ function formatDate(date) { * @param {string} offenseDisposition The text of the disposition decision */ function isDismissed(offenseDisposition) { - var dispositionNormalized = offenseDisposition.toLowerCase().trim(); + let dispositionNormalized = offenseDisposition.toLowerCase().trim(); if (dispositionNormalized.toLowerCase().substr(0, 12) === 'dismissed by') { return true; } else { @@ -538,7 +536,7 @@ function isFelOrMisd(element) { } function nthIndex(str, subStr, n) { - var L = str.length, + let L = str.length, i = -1; while (n-- && i++ < L) { i = str.indexOf(subStr, i); @@ -612,13 +610,13 @@ function guid() { ); } -var Base64 = { +let Base64 = { _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef' + 'ghijklmnopqrstuvwxyz0123456789+/=', encode: function (e) { - var t = ''; - var n, r, i, s, o, u, a; - var f = 0; + let t = ''; + let n, r, i, s, o, u, a; + let f = 0; e = Base64._utf8_encode(e); while (f < e.length) { n = e.charCodeAt(f++); @@ -643,10 +641,10 @@ var Base64 = { return t; }, decode: function (e) { - var t = ''; - var n, r, i; - var s, o, u, a; - var f = 0; + let t = ''; + let n, r, i; + let s, o, u, a; + let f = 0; e = e.replace(/[^A-Za-z0-9\+\/\=]/g, ''); while (f < e.length) { s = this._keyStr.indexOf(e.charAt(f++)); @@ -669,9 +667,9 @@ var Base64 = { }, _utf8_encode: function (e) { e = e.replace(/\r\n/g, '\n'); - var t = ''; - for (var n = 0; n < e.length; n++) { - var r = e.charCodeAt(n); + let t = ''; + for (let n = 0; n < e.length; n++) { + let r = e.charCodeAt(n); if (r < 128) { t += String.fromCharCode(r); } else if (r > 127 && r < 2048) { @@ -686,9 +684,9 @@ var Base64 = { return t; }, _utf8_decode: function (e) { - var t = ''; - var n = 0; - var r = (c1 = c2 = 0); + let t = ''; + let n = 0; + let r = (c1 = c2 = 0); while (n < e.length) { r = e.charCodeAt(n); if (r < 128) { @@ -712,4 +710,7 @@ var Base64 = { }; //Vue app -var app = createApp(PopupApp).mount('#filing-app'); +const app = createApp(PopupApp) +const pinia = createPinia() +app.use(pinia) +app.mount('#filing-app'); diff --git a/extensionDirectory/saveFile.js b/extensionDirectory/saveFile.js index 023094ea..90603da6 100644 --- a/extensionDirectory/saveFile.js +++ b/extensionDirectory/saveFile.js @@ -1,4 +1,4 @@ -import moment from 'moment'; +import dayjs from 'dayjs'; function saveAllCountsToHtml(savedData) { @@ -13,10 +13,9 @@ function saveAllCountsToHtml(savedData) { defInitials += n[0] }) - let now = moment().format('YYYY-MM-DD'); + let now = dayjs().format('YYYY-MM-DD'); let obfuscatedSavedData = Base64.encode(savedData); let fileName = now + '-' + defInitials; - console.log(fileName) let htmlString = ` diff --git a/extensionDirectory/static/filings.html b/extensionDirectory/static/filings.html index f85d8613..7cefdbd8 100644 --- a/extensionDirectory/static/filings.html +++ b/extensionDirectory/static/filings.html @@ -31,8 +31,8 @@ href="./node_modules/@fortawesome/fontawesome-free/css/all.css" /> - - + + 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 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 old mode 100755 new mode 100644 index 4250c59f..eb1dc801 --- a/extensionDirectory/utils.js +++ b/extensionDirectory/utils.js @@ -1,163 +1,796 @@ -import Gumshoe from 'gumshoejs' -import SmoothScroll from 'smooth-scroll'; - -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 - var computed = window.getComputedStyle(field); - - // Calculate the height - var 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 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]; -} - -/** - * 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 (self.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 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 - var 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() { - var 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 - ); -} - -export function setInitialExpandForTextAreas() { - //sets the default size for all text areas based on their content. - //call this after vue has initialized and displayed - var textAreas = document.getElementsByTagName('textarea'); - for (var index in textAreas) { - var textArea = textAreas[index]; - if (textArea === undefined) return; - autoExpand(textArea); - } -} - -export function toCountyCode(value) { - if (!value) return ''; - return countyCodeFromCounty(value); -} \ No newline at end of file + +import hash from 'object-hash'; +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, isChromeExtensionPopup) { + 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(); + } 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(); + } + }); +} + +/** + * 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); +}