Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ tests/*
!tests/frontend-compat.test.mjs
!tests/block-selection.test.mjs
!tests/datepicker-compat.test.mjs
!tests/preview-url.test.mjs
!tests/vite-compat.test.mjs
docs
vendor
Expand Down
9 changes: 4 additions & 5 deletions _dev/src/components/Iframe.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ let currentPrettyBlocksId = computed(() => currentBlock.id_prettyblocks)
*/
onMounted(() => {
prettyBlocksContext.setIframe()
if (new URL(window.location.href).searchParams.has('startup_url')) {
prettyBlocksContext.pushUrl(prettyBlocksContext.psContext.current_url)
}
})

watch(currentBlock, (newVal, oldValue) => {
Expand All @@ -51,11 +54,7 @@ const reloadIframe = () => {
}


let filteredURL = ref(prettyBlocksContext.updateFilteredURL(ajax_urls.startup_url))

watch(prettyBlocksContext.psContext, () => {
filteredURL.value = prettyBlocksContext.updateFilteredURL(prettyBlocksContext.psContext.current_url)
})
const filteredURL = computed(() => prettyBlocksContext.psContext.current_url)
</script>

<template>
Expand Down
6 changes: 4 additions & 2 deletions _dev/src/components/form/ShopSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ const props = defineProps({

const changetItem = (item) => {
props.modelValue.shop_name = item.name
const { id_lang, id_shop } = prettyBlocksContext.psContext

prettyBlocksContext.$patch({
psContext: {
id_lang: item.id_lang,
id_shop: item.id_shop,
shop_name: item.name,
current_url: item.base_url + (item.base_url.includes('?prettyblocks=1') ? '' : '?prettyblocks=1'),
href: item.base_url,
}
})
prettyBlocksContext.changeUrl(item.base_url)
prettyBlocksContext.changeUrl(item.base_url, {
reloadSame: id_lang !== item.id_lang || id_shop !== item.id_shop,
})
}

const emit = defineEmits(['update:modelValue.shop_name'])
Expand Down
22 changes: 6 additions & 16 deletions _dev/src/scripts/eventHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,12 @@ export const eventHandler = async (event) => {
})
}

if(event.data.type == 'setNewUrl' || event.data.type == 'setForceNewUrl')
{
let context = event.data.params.context
let custom_url = event.data.params.url
let force_reload = event.data.type == 'setForceNewUrl' ? true : false


prettyBlocksContext.$patch({
psContext: {
...prettyBlocksContext.psContext,
...context,
current_url: custom_url
}
});
prettyBlocksContext.changeUrl(custom_url)
if(force_reload) {
if (event.data.type == 'setNewUrl' || event.data.type == 'setForceNewUrl') {
const { context, url } = event.data.params
const { id_lang, id_shop, shop_name, href } = context
prettyBlocksContext.$patch({ psContext: { id_lang, id_shop, shop_name, href } })
const navigated = prettyBlocksContext.changeUrl(url)
if (event.data.type == 'setForceNewUrl' && navigated) {
window.location.reload()
}
}
Expand Down
50 changes: 23 additions & 27 deletions _dev/src/store/pinia.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { eventHandler } from '../scripts/eventHandler'
import { createToaster } from "@meforma/vue-toaster";
import { HttpClient } from '../services/HttpClient';
import { acceptsBlockRefresh, blockSelectionAfterRefresh, sameBlockContext, snapshotBlockContext } from './blockSelection.mjs';
import { withPrettyBlocksMode } from './previewUrl.mjs';
const toaster = createToaster({
position: "top",
});
Expand Down Expand Up @@ -47,7 +48,9 @@ export const usePrettyBlocksContext = defineStore('prettyBlocksContext', {
id_lang: 1,
id_shop: 1,
shop_name: null,
current_url: ajax_urls.startup_url + (ajax_urls.startup_url.includes('?prettyblocks=1') ? '' : '?prettyblocks=1'),
current_url: withPrettyBlocksMode(ajax_urls.startup_url, ajax_urls.base_url)
?? withPrettyBlocksMode(ajax_urls.base_url)
?? 'about:blank',
href: ajax_urls.startup_url,
},
iframe: {
Expand Down Expand Up @@ -196,16 +199,25 @@ export const usePrettyBlocksContext = defineStore('prettyBlocksContext', {
let message = { type: eventType, data: data };
this.iframe.domElement.contentWindow.postMessage(message, "*");
},
changeUrl(url) {
changeUrl(url, { reloadSame = false } = {}) {
const previewUrl = this.updateFilteredURL(url)
if (!previewUrl) {
return false
}
this.pushUrl(previewUrl)
if (previewUrl === this.psContext.current_url) {
if (reloadSame) {
this.reloadIframe()
}
return false
}
this.$patch((state) => {
state.psContext.current_url = this.updateFilteredURL(url)
state.psContext.current_url = previewUrl
})
// Update the current window URL with startup_url parameter
this.pushUrl(url)
this.showLoader()
this.setIframe()
this.emit('urlChanged', url)

return true
},
showLoader() {
this.$patch((state) => {
Expand All @@ -224,29 +236,13 @@ export const usePrettyBlocksContext = defineStore('prettyBlocksContext', {
currentUrl.searchParams.delete('id');
currentUrl.searchParams.delete('endpoint');

currentUrl.searchParams.set('startup_url', this.updateFilteredURL(url));
window.history.replaceState({}, '', currentUrl.toString());
currentUrl.searchParams.set('startup_url', url);
if (currentUrl.href !== window.location.href) {
window.history.replaceState({}, '', currentUrl.href);
}
},
updateFilteredURL(url) {
if (url.startsWith('/')) {

if (ajax_urls.base_url.endsWith('/')) {
url = ajax_urls.base_url + url.substring(1);
} else {
url = ajax_urls.base_url + url;
}
}

let hashIndex = url.indexOf('#');
if (hashIndex !== -1) {
url = url.substring(0, hashIndex) + '?prettyblocks=1' + url.substring(hashIndex);
} else if (!url.includes('?')) {
url += '?prettyblocks=1';
} else if (!url.includes('prettyblocks')) {
url += '&prettyblocks=1';
}

return url;
return withPrettyBlocksMode(url, ajax_urls.base_url, this.psContext.href);
},
reloadIframe(currentSrc = false) {
if (this.iframe.domElement) {
Expand Down
29 changes: 29 additions & 0 deletions _dev/src/store/previewUrl.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function withPrettyBlocksMode(value, baseUrl, currentUrl = baseUrl) {
if (typeof value !== 'string' || !value.trim()) {
return null;
}

try {
const shop = baseUrl ? new URL(baseUrl) : undefined;
const base = currentUrl ? new URL(currentUrl, shop) : shop;
const url = new URL(value, base);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return null;
}

// Keep the shop-relative /path convention from #261 on subdirectory shops,
// without adding the shop directory twice to an already prefixed URL.
if (shop && url.origin === shop.origin && /^\/(?!\/)/.test(value)) {
const directory = shop.pathname.replace(/\/$/, '');
if (directory && url.pathname !== directory && !url.pathname.startsWith(directory + '/')) {
url.pathname = directory + url.pathname;
}
}

// Combination fragments must stay intact; the edit flag belongs to the query.
url.searchParams.set('prettyblocks', '1');
return url.href;
} catch {
return null;
}
}
6 changes: 4 additions & 2 deletions docker/release.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
&& corepack yarn install --frozen-lockfile --non-interactive

COPY _dev ./_dev
COPY tests/block-selection.test.mjs tests/datepicker-compat.test.mjs tests/vite-compat.test.mjs ./tests/
RUN node --test tests/block-selection.test.mjs tests/datepicker-compat.test.mjs tests/vite-compat.test.mjs \
COPY tests/block-selection.test.mjs tests/datepicker-compat.test.mjs \
tests/preview-url.test.mjs tests/vite-compat.test.mjs ./tests/
RUN node --test tests/block-selection.test.mjs tests/datepicker-compat.test.mjs \
tests/preview-url.test.mjs tests/vite-compat.test.mjs \
&& cd _dev && corepack yarn build


Expand Down
79 changes: 79 additions & 0 deletions tests/preview-url.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { withPrettyBlocksMode } from '../_dev/src/store/previewUrl.mjs';

const shop = 'https://shop.example.test/store/';
const product = shop + 'fr/1-1-t-shirt.html';
const combination = '#/1-taille-s/8-couleur-blanc';

test('puts edit mode before a product combination fragment and preserves the query', () => {
const input = product + '?campaign=summer&tag=a%26b' + combination;
const output = new URL(withPrettyBlocksMode(input, shop));

assert.equal(output.pathname, '/store/fr/1-1-t-shirt.html');
assert.equal(output.hash, combination);
assert.deepEqual([...output.searchParams], [
['campaign', 'summer'], ['tag', 'a&b'], ['prettyblocks', '1'],
]);
});

test('adding edit mode to a combination URL is idempotent across iframe reloads', () => {
const expected = product + '?prettyblocks=1' + combination;
assert.equal(withPrettyBlocksMode(product + combination, shop), expected);
assert.equal(withPrettyBlocksMode(expected, shop), expected);
});

test('replaces disabled, duplicated and previously corrupted edit flags', () => {
for (const query of [
'?prettyblocks=0',
'?prettyblocks=0&prettyblocks=1',
'?prettyblocks=1?prettyblocks=1?prettyblocks=1',
]) {
const output = new URL(withPrettyBlocksMode(product + query + combination, shop));
assert.deepEqual(output.searchParams.getAll('prettyblocks'), ['1']);
assert.equal(output.hash, combination);
}
});

test('another query key or value containing prettyblocks is not the edit flag', () => {
const output = new URL(withPrettyBlocksMode(product + '?q=prettyblocks&notprettyblocks=0', shop));
assert.equal(output.searchParams.get('q'), 'prettyblocks');
assert.equal(output.searchParams.get('notprettyblocks'), '0');
assert.deepEqual(output.searchParams.getAll('prettyblocks'), ['1']);
});

test('resolves relative links, query-only links and fragments against the current preview', () => {
assert.equal(withPrettyBlocksMode('2-t-shirt.html' + combination, shop, product),
shop + 'fr/2-t-shirt.html?prettyblocks=1' + combination);
assert.equal(withPrettyBlocksMode('?id=2' + combination, shop, product),
product + '?id=2&prettyblocks=1' + combination);
assert.equal(withPrettyBlocksMode(combination, shop, product + '?id=2'),
product + '?id=2&prettyblocks=1' + combination);
});

test('keeps subdirectory and multishop URLs on their intended path and origin', () => {
const expected = product + '?prettyblocks=1' + combination;
assert.equal(withPrettyBlocksMode('fr/1-1-t-shirt.html' + combination, shop), expected);
assert.equal(withPrettyBlocksMode('/fr/1-1-t-shirt.html' + combination, shop), expected);
assert.equal(withPrettyBlocksMode('/store/fr/1-1-t-shirt.html' + combination, shop), expected);
assert.equal(withPrettyBlocksMode('//other.example.test/t-shirt.html' + combination, shop),
'https://other.example.test/t-shirt.html?prettyblocks=1' + combination);
assert.equal(withPrettyBlocksMode('https://other.example.test/t-shirt.html' + combination, shop),
'https://other.example.test/t-shirt.html?prettyblocks=1' + combination);
});

test('does not create an editor URL from empty, malformed or non-web targets', () => {
for (const input of ['', null, 'http://[invalid', 'javascript:alert(1)', 'mailto:shop@example.test']) {
assert.equal(withPrettyBlocksMode(input, shop), null);
}
});

test('a back-office startup_url round trip retains the query and combination', () => {
const editor = new URL(shop + 'admin/modules/prettyblocks/');
const preview = withPrettyBlocksMode(product + '?id=1' + combination, shop);
editor.searchParams.set('startup_url', preview);

const reopened = new URL(editor.href).searchParams.get('startup_url');
assert.equal(reopened, product + '?id=1&prettyblocks=1' + combination);
assert.equal(withPrettyBlocksMode(reopened, shop), reopened);
});