Skip to content

Commit

Permalink
MWPW-139819 Redirect Formatter PoC (#166)
Browse files Browse the repository at this point in the history
* 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
JasonHowellSlavin and meganthecoder authored Jan 25, 2024
1 parent 3c52072 commit 6377b20
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 0 deletions.
93 changes: 93 additions & 0 deletions blocks/redirects-formatter/redirects-formatter.css
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;
}
163 changes: 163 additions & 0 deletions blocks/redirects-formatter/redirects-formatter.js
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);
}
1 change: 1 addition & 0 deletions test/blocks/redirects-formatter/mocks/locale-config.json
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"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<div class="redirects-formatter"></div>
</div>
1 change: 1 addition & 0 deletions test/blocks/redirects-formatter/mocks/textAreaValues.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 113 additions & 0 deletions test/blocks/redirects-formatter/redirects-formatter.test.js
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;
});
});

0 comments on commit 6377b20

Please sign in to comment.