-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MWPW-139819 Redirect Formatter PoC (#166)
* linter * Needs a lot of cleanup, but working as expected currently * Added some functionality, going to ask for Malik to look at it, and get some dev feedback, then continue * Adding functionality, styles, still need to add additional tests and error handling * Added tests, some cleanup and styles * Small cleanup items in prep for code review * CR changes * CR changes * Additional clariications for usability * Update blocks/redirects-formatter/redirects-formatter.js Co-authored-by: Megan Thomas <[email protected]> * CR changes * Update blocks/redirects-formatter/redirects-formatter.js Co-authored-by: Megan Thomas <[email protected]> --------- Co-authored-by: Megan Thomas <[email protected]>
- Loading branch information
1 parent
3c52072
commit 6377b20
Showing
6 changed files
with
374 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
|
||
:root { | ||
--notch-size: 12px; | ||
} | ||
|
||
h1 { | ||
margin: 20px; | ||
width: fit-content; | ||
padding-bottom: 10px; | ||
border-bottom: 2px solid var(--text-color); | ||
} | ||
|
||
textarea { | ||
white-space: pre; | ||
min-height: 200px; | ||
height: fit-content; | ||
} | ||
|
||
button { | ||
width: fit-content; | ||
grid-column: 4; | ||
margin: 16px 0; | ||
outline: transparent solid 0; | ||
transition: outline 300ms; | ||
border: none; | ||
height: 36px; | ||
font-family: adobe-clean, sans-serif; | ||
color: var(--color-white); | ||
background: #FF1593; | ||
font-size: 16px; | ||
font-weight: 700; | ||
padding: 0 18px; | ||
clip-path: polygon(0% 0%, var(--notch-size) 0%, calc(100% - var(--notch-size)) 0%, 100% var(--notch-size), 100% calc(100% - var(--notch-size)), 100% 100%, 0% 100%, 0% 100%); | ||
} | ||
|
||
button:hover { | ||
background-color: #d07; | ||
} | ||
|
||
label { | ||
margin: 16px 0; | ||
align-self: center; | ||
} | ||
|
||
.redirects-formatter, | ||
.redirects-container { | ||
display: flex; | ||
flex-direction: column; | ||
margin: 0 20px 20px; | ||
} | ||
|
||
.instructions { | ||
margin: 0 20px; | ||
} | ||
|
||
.checkbox-container { | ||
display: flex; | ||
flex-wrap: wrap; | ||
background: #efefef; | ||
grid-column: 1/3; | ||
border: 1px solid black; | ||
} | ||
|
||
.checkbox-wrapper { | ||
padding: 5px; | ||
min-width: 100px; | ||
display: flex; | ||
} | ||
|
||
.redirects-container section { | ||
display: grid; | ||
grid-template-columns: 1fr 1fr; | ||
margin-bottom: 20px; | ||
} | ||
|
||
.redirects-container section textarea { | ||
grid-column: 1/3; | ||
} | ||
|
||
.redirects-container section button { | ||
grid-column-start: 2; | ||
justify-self: end; | ||
} | ||
|
||
.error { | ||
margin: 10px 20px; | ||
min-height: 30px; | ||
color: red; | ||
} | ||
|
||
.error-border { | ||
border: 1px solid red; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { getLibs } from '../../scripts/utils.js'; | ||
|
||
export const SELECT_ALL_REGIONS = 'Select All Regions'; | ||
export const DESELECT_ALL_REGIONS = 'De-select All Regions'; | ||
export const NO_LOCALE_ERROR = 'No locales selected from list'; | ||
const INPUT_LABEL_TEXT = 'Paste source and destination URLs here:'; | ||
const OUTPUT_LABEL_TEXT = 'Localized results appear here:'; | ||
const PROCESS_TEXT = 'Process redirects'; | ||
const COPY_TO_CLIPBOARD = 'Copy to clipboard'; | ||
const INSTRUCTIONS_TEXT = 'Select the locales you require by checking the checkboxes. Paste URLs copied from an excel sheet' | ||
+ ' into the first input. Press "Process Redirects" to generate localized URLs to paste into redirects.xlsx. To copy your URLS,' | ||
+ ' press "Copy to clipboard" or select them with the cursor manually.'; | ||
|
||
async function createLocaleCheckboxes(prefixGroup) { | ||
const { createTag } = await import(`${getLibs()}/utils/utils.js`); | ||
|
||
return Object.keys(prefixGroup).map((key) => { | ||
const { prefix } = prefixGroup[key]; | ||
const currLocale = prefix === '' ? 'en' : prefix; | ||
if (currLocale === 'langstore') return undefined; | ||
const checkbox = createTag('input', { class: 'locale-checkbox', type: 'checkbox', id: `${currLocale}`, name: `${currLocale}` }); | ||
const label = createTag('label', { class: 'locale-label', for: `${currLocale}` }, currLocale); | ||
|
||
return createTag('div', { class: 'checkbox-wrapper' }, [checkbox, label]); | ||
}); | ||
} | ||
|
||
export function parseUrlString(input) { | ||
const pairs = input.split('\n'); | ||
|
||
return pairs.reduce((rdx, pairString) => { | ||
const pair = pairString.split('\t'); | ||
rdx.push(pair); | ||
return rdx; | ||
}, []); | ||
} | ||
|
||
function handleError(e, eSection) { | ||
const errorElem = document.querySelector('.error'); | ||
setTimeout(() => { | ||
errorElem.innerText = ''; | ||
eSection.classList.remove('error-border'); | ||
}, 2000); | ||
errorElem.innerText = e; | ||
eSection.classList.add('error-border'); | ||
} | ||
|
||
export function generateRedirectList(urls, locales, handler) { | ||
const inputSection = document.querySelector('.redirects-text-area'); | ||
const checkboxSection = document.querySelector('.checkbox-container'); | ||
const errorMessage = 'Invalid URL. URLs must start with "https://" e.g: "https://business.adobe.com"'; | ||
|
||
return urls.reduce((rdx, urlPair) => { | ||
if (!locales.length) handler(NO_LOCALE_ERROR, checkboxSection); | ||
|
||
locales.forEach((locale) => { | ||
let from; | ||
let to; | ||
try { | ||
from = new URL(urlPair[0]); | ||
} catch (e) { | ||
handler(errorMessage, inputSection); | ||
return; | ||
} | ||
try { | ||
to = new URL(urlPair[1]); | ||
} catch (e) { | ||
handler(errorMessage, inputSection); | ||
return; | ||
} | ||
const fromPath = from.pathname.split('.html')[0]; | ||
rdx.push([`/${locale}${fromPath}`, `${to.origin}/${locale}${to.pathname}`]); | ||
}); | ||
return rdx; | ||
}, []); | ||
} | ||
|
||
export function stringifyListForExcel(urls) { | ||
return urls.reduce((rdx, url) => `${rdx}${url[0]}\t${url[1]}\n`, ''); | ||
} | ||
|
||
export default async function init(el) { | ||
const { createTag } = await import(`${getLibs()}/utils/utils.js`); | ||
const xlPath = './locale-config.json'; | ||
const resp = await fetch(xlPath); | ||
if (!resp.ok) return; | ||
const { data } = await resp.json(); | ||
|
||
const redirectsContainer = createTag('section', { class: 'redirects-container' }); | ||
const header = createTag('h1', null, 'Redirect Formatting Tool'); | ||
const instructions = createTag('p', { class: 'instructions' }, INSTRUCTIONS_TEXT); | ||
const errorSection = createTag('p', { class: 'error' }); | ||
|
||
// Checkboxes | ||
const checkBoxesHeader = createTag('p', { class: 'cb-label' }); | ||
checkBoxesHeader.innerText = 'Select Locales'; | ||
const checkBoxes = await createLocaleCheckboxes(data); | ||
const checkBoxesContainer = createTag('div', { class: 'checkbox-container' }, checkBoxes); | ||
const selectAllCB = createTag('button', { class: 'select-all-cb' }, SELECT_ALL_REGIONS); | ||
const checkBoxesArea = createTag('section', { class: 'cb-area' }, [checkBoxesHeader, selectAllCB, checkBoxesContainer]); | ||
|
||
// Text input area | ||
const inputAreaContainer = createTag('section', { class: 'input-container' }); | ||
const textAreaInput = createTag('textarea', { class: 'redirects-text-area', id: 'redirects-input', name: 'redirects-input' }); | ||
const taiLabel = createTag('label', { class: 'io-label', for: 'redirects-input' }, INPUT_LABEL_TEXT); | ||
const submitButton = createTag('button', { class: 'process-redirects' }, PROCESS_TEXT); | ||
inputAreaContainer.append(taiLabel, submitButton, textAreaInput); | ||
|
||
// Text output Area | ||
const outputAreaContainer = createTag('section', { class: 'output-container' }); | ||
const textAreaOutput = createTag('textarea', { class: 'redirects-text-area', id: 'redirects-output', name: 'redirects-output', readonly: true }); | ||
const taoLabel = createTag('label', { class: 'io-label', for: 'redirects-output' }, OUTPUT_LABEL_TEXT); | ||
const copyButton = createTag('button', { class: 'copy' }, COPY_TO_CLIPBOARD); | ||
outputAreaContainer.append(taoLabel, copyButton, textAreaOutput); | ||
|
||
// Event listeners | ||
selectAllCB.addEventListener('click', () => { | ||
const allNotSelected = selectAllCB.innerText === SELECT_ALL_REGIONS; | ||
|
||
document.querySelectorAll('.locale-checkbox').forEach((cb) => { | ||
cb.checked = allNotSelected; | ||
}); | ||
|
||
selectAllCB.innerText = allNotSelected ? DESELECT_ALL_REGIONS : SELECT_ALL_REGIONS; | ||
}); | ||
|
||
submitButton.addEventListener('click', () => { | ||
const locales = [...document.querySelectorAll("[type='checkbox']")].reduce((rdx, cb) => { | ||
if (cb.checked) { | ||
rdx.push(cb.id); | ||
} | ||
return rdx; | ||
}, []); | ||
|
||
const parsedInput = parseUrlString(textAreaInput.value); | ||
const redirList = generateRedirectList(parsedInput, locales, handleError); | ||
const outputString = stringifyListForExcel(redirList); | ||
|
||
textAreaOutput.value = outputString; | ||
}); | ||
|
||
copyButton.addEventListener('click', () => { | ||
if (!navigator?.clipboard) return; | ||
const redirects = textAreaOutput.value; | ||
navigator.clipboard.writeText(redirects).then( | ||
() => { | ||
copyButton.innerText = 'Copied'; | ||
setTimeout(() => { | ||
copyButton.innerText = COPY_TO_CLIPBOARD; | ||
}, 1500); | ||
}, | ||
() => { | ||
copyButton.innerText = 'Error!'; | ||
setTimeout(() => { | ||
copyButton.innerText = COPY_TO_CLIPBOARD; | ||
}, 1500); | ||
}, | ||
); | ||
}); | ||
|
||
redirectsContainer.append(checkBoxesArea, inputAreaContainer, outputAreaContainer); | ||
el.append(header, instructions, errorSection, redirectsContainer); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"total":6,"offset":0,"limit":6,"data":[{"prefix":"ar"},{"prefix":"gt"},{"prefix":"ru"},{"prefix":"ck"},{"prefix":"cn"},{"prefix":"si"}],":type":"sheet"} |
3 changes: 3 additions & 0 deletions
3
test/blocks/redirects-formatter/mocks/redirects-formatter.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<div> | ||
<div class="redirects-formatter"></div> | ||
</div> |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
113 changes: 113 additions & 0 deletions
113
test/blocks/redirects-formatter/redirects-formatter.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { readFile } from '@web/test-runner-commands'; | ||
import { expect } from '@esm-bundle/chai'; | ||
import sinon from 'sinon'; | ||
import { setLibs } from '../../../scripts/utils.js'; | ||
|
||
const { | ||
default: init, | ||
parseUrlString, | ||
generateRedirectList, | ||
stringifyListForExcel, | ||
SELECT_ALL_REGIONS, | ||
DESELECT_ALL_REGIONS, | ||
NO_LOCALE_ERROR, | ||
} = await import('../../../blocks/redirects-formatter/redirects-formatter.js'); | ||
const { default: textAreaString } = await import('./mocks/textAreaValues.js'); | ||
|
||
setLibs('libs'); | ||
|
||
describe('Redirects Formatter', () => { | ||
const ogFetch = window.fetch; | ||
|
||
beforeEach(async () => { | ||
document.body.innerHTML = await readFile({ path: './mocks/redirects-formatter.html' }); | ||
|
||
const block = document.querySelector('.redirects-formatter'); | ||
|
||
sinon.stub(window, 'fetch'); | ||
const fetchText = await readFile({ path: './mocks/locale-config.json' }); | ||
const res = new window.Response(fetchText, { status: 200 }); | ||
window.fetch.returns(Promise.resolve(res)); | ||
|
||
await init(block); | ||
}); | ||
|
||
afterEach(async () => { | ||
window.fetch = ogFetch; | ||
}); | ||
|
||
it('correctly parses values from the input', () => { | ||
const parsedInput = parseUrlString(textAreaString); | ||
const firstPair = parsedInput[0]; | ||
const lastPair = parsedInput[2]; | ||
expect(firstPair[0]).to.equal('https://business.adobe.com/products/experience-manager/sites/experience-fragments.html'); | ||
expect(firstPair[1]).to.equal('https://business.adobe.com/products/experience-manager/sites/omnichannel-experiences.html'); | ||
expect(lastPair[0]).to.equal('https://business.adobe.com/products/experience-manager/sites/out-of-the-box-components.html'); | ||
expect(lastPair[1]).to.equal('https://business.adobe.com/products/experience-manager/sites/developer-tools.html'); | ||
}); | ||
|
||
it('outputs localized urls', () => { | ||
const parsedInput = parseUrlString(textAreaString); | ||
const locales = ['ar', 'au', 'uk']; | ||
|
||
const redir = generateRedirectList(parsedInput, locales); | ||
expect(redir[0][0]).to.equal('/ar/products/experience-manager/sites/experience-fragments'); | ||
expect(redir.length).to.equal(9); | ||
}); | ||
|
||
it('provides a string formatted for pasting into excel', () => { | ||
const parsedInput = parseUrlString(textAreaString); | ||
const locales = ['ar', 'au', 'uk']; | ||
const redir = generateRedirectList(parsedInput, locales); | ||
const stringList = stringifyListForExcel(redir); | ||
|
||
expect(typeof stringList).to.equal('string'); | ||
expect(stringList.substring(0, 4)).to.equal('/ar/'); | ||
expect(stringList.substring((stringList.length - 6), stringList.length)).to.equal('.html\n'); | ||
}); | ||
|
||
it('selects/deselects all the checkboxes on click', async () => { | ||
const checkBoxes = document.querySelectorAll('.locale-checkbox'); | ||
expect([...checkBoxes].every((cb) => !cb.checked)).to.be.true; | ||
|
||
const selectAllButton = document.querySelector('button'); | ||
selectAllButton.click(); | ||
|
||
expect([...checkBoxes].every((cb) => cb.checked)).to.be.true; | ||
expect(selectAllButton.innerText).to.equal(DESELECT_ALL_REGIONS); | ||
|
||
selectAllButton.click(); | ||
expect([...checkBoxes].every((cb) => !cb.checked)).to.be.true; | ||
expect(selectAllButton.innerText).to.equal(SELECT_ALL_REGIONS); | ||
}); | ||
|
||
it('informs the user of an error if no locales are selected', async () => { | ||
const checkBoxes = document.querySelectorAll('.locale-checkbox'); | ||
expect([...checkBoxes].every((cb) => !cb.checked)).to.be.true; | ||
|
||
const processButton = document.querySelector('.process-redirects'); | ||
const errorMessage = document.querySelector('.error'); | ||
const checkBoxContainer = document.querySelector('.checkbox-container'); | ||
processButton.click(); | ||
expect(errorMessage.innerHTML).to.equal(NO_LOCALE_ERROR); | ||
expect(checkBoxContainer.classList.contains('error-border')).to.be.true; | ||
}); | ||
|
||
it('informs the user of an error if an incorrect url is passed in to the input', async () => { | ||
const input = document.querySelector('.redirects-text-area'); | ||
const processButton = document.querySelector('.process-redirects'); | ||
const errorMessage = document.querySelector('.error'); | ||
const selectAllCB = document.querySelector('.select-all-cb'); | ||
const correct = 'https://www.adobe.com/resource\thttps://www.adobe.com'; | ||
const incorrect = '/resource\thttps://www.adobe.com'; | ||
|
||
selectAllCB.click(); | ||
input.value = correct; | ||
processButton.click(); | ||
expect(input.classList.contains('error-border')).to.be.false; | ||
input.value = incorrect; | ||
processButton.click(); | ||
expect(errorMessage.innerHTML.length > 0).to.be.true; | ||
expect(input.classList.contains('error-border')).to.be.true; | ||
}); | ||
}); |