Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL param support #3028

Draft
wants to merge 10 commits into
base: next
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/lib/sdk/src/utils/svelte/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ import { addBasePath as _addBasePath } from './addBasePath.js';
import { config } from '$evidence/config';
/** @type {(path: string) => string} */
export const addBasePath = (path) => _addBasePath(path, config);
export { hydrateFromUrlParam, updateUrlParam } from './useUrlParams.js';
31 changes: 31 additions & 0 deletions packages/lib/sdk/src/utils/svelte/storybookURLWatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export let displayedStoryURL = window.location.href; // The string that updates

const updateURL = () => {
const newURL = window.location.href;
if (newURL !== displayedStoryURL) {
displayedStoryURL = newURL; // Update the variable

// Try forcing Storybook to recognize the change
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.src = iframe.src; // Force reload

Check failure on line 11 in packages/lib/sdk/src/utils/svelte/storybookURLWatcher.js

View workflow job for this annotation

GitHub Actions / Check project linting

'iframe.src' is assigned to itself
}
}
};

export const initStorybookURLWatcher = () => {
const pushState = history.pushState;
const replaceState = history.replaceState;

history.pushState = function () {
pushState.apply(history, arguments);
updateURL();
};

history.replaceState = function () {
replaceState.apply(history, arguments);
updateURL();
};

window.addEventListener('popstate', updateURL);
};
91 changes: 91 additions & 0 deletions packages/lib/sdk/src/utils/svelte/useUrlParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// @ts-expect-error
import { browser } from '$app/environment';

/**
* Hydrates a value using a URL search parameter.
* @param {string} key
* @param {(value: string | number | null) => unknown} hydrate
*/
export function hydrateFromUrlParam(key, hydrate) {
if (browser) {
const url = new URL(window.location.href);
let _value = parseUrlValue(url.searchParams.get(key));
console.log('hydrateFromUrlParam', key, _value);
hydrate?.(_value);
}
}

/** @type {ReturnType<typeof setTimeout>} */
let timeout;
/**
* Updates the URL search parameter.
* @param {string} key
* @param {string | null} value
*/
export function updateUrlParam(key, value, debounceDelay = null) {
console.log('updateUrlParam', key, value);
if (browser) {
const url = new URL(window.location.href);

const updateUrl = () => {
if (value !== null) {
url.searchParams.set(key, encodeUrlValue(value));
} else {
url.searchParams.delete(key);
}

history.replaceState(null, '', `?${url.searchParams.toString()}`);
};

if (debounceDelay !== null) {
clearTimeout(timeout);
timeout = setTimeout(updateUrl, debounceDelay);
} else {
updateUrl();
}
}
}

/**
* Encodes a value for a URL parameter.
* @param {any} value
* @returns {string}
*/
function encodeUrlValue(value) {
// Convert value to a JSON string
const jsonString = JSON.stringify(value);

// Base64 encode it (btoa only works with strings)
const base64Encoded = btoa(jsonString);

// Encode for safe URL usage
return base64Encoded;
}

/**
* Parses a value retrieved from a URL parameter.
* @param {string | null} value
* @returns {any}
*/
function parseUrlValue(value) {
if (value === null) return null;

let parsed;

// Try to decode as Base64 and parse as JSON
try {
const base64Decoded = atob(value);
parsed = JSON.parse(base64Decoded);
console.log('parsed', parsed);
// Return the parsed object if it's a valid object or array
if ((typeof parsed === 'object' || typeof parsed === 'boolean') && parsed !== null) {
return parsed;
}
} catch {
// If Base64 decoding or JSON parsing fails, simply return the value as is
console.log('Error decoding or parsing');
}

// Return the value as is (it could be a primitive value or non-Base64 string)
return parsed || value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,45 @@
import ButtonGroupItem from './ButtonGroupItem.svelte';
import { Query } from '@evidence-dev/sdk/usql';
import { query } from '@evidence-dev/universal-sql/client-duckdb';
import { setContext } from 'svelte';
import { readable } from 'svelte/store';

import { getInputContext } from '@evidence-dev/sdk/utils/svelte';
// From layout.js
const inputStore = getInputContext();

setContext('page-ctx', { page: readable({ data: {} }), url: new URL('http://localhost:3000') });

const data = Query.create(`select * from hashtags`, query);

let storyIframeURL = '';

const updateURL = () => {
storyIframeURL = window.location.href;

// Try forcing Storybook to recognize the change
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.src = iframe.src; // Force reload
}
};

(function () {
const pushState = history.pushState;
const replaceState = history.replaceState;

history.pushState = function () {
pushState.apply(history, arguments);
updateURL();
};

history.replaceState = function () {
replaceState.apply(history, arguments);
updateURL();
};

window.addEventListener('popstate', updateURL);
})();
</script>

<Template let:args>
Expand Down Expand Up @@ -242,3 +275,80 @@
</ButtonGroup>
</div>
</Story>
<Story name="URL Params Hard Coded Entries" let:args>
<div class="mb-8">
<ButtonGroup {...args}>
<ButtonGroupItem valueLabel="Num 1" value={1} />
<ButtonGroupItem valueLabel="Num 2" value={2} />
<ButtonGroupItem valueLabel="Num 3" value={3} default />
<ButtonGroupItem valueLabel="Num 4" value={4} />
<ButtonGroupItem valueLabel="String 4" value={'4'} />
</ButtonGroup>
</div>

Current Value: {$inputStore[args.name]}
<div>URL: {storyIframeURL}</div>
<button
class="mt-4 p-1 border bg-info/60 hover:bg-info/40 active:bg-info/20 rounded-md text-sm

"
on:click={() => window.open(storyIframeURL, '_blank')}>Go to URL</button
>
</Story>

<Story
name="URL Params Query-Based Entries - Text"
let:args
args={{
data: 'hashtags',
value: 'id',
label: 'tag'
}}
>
<div class="mb-8">
<ButtonGroup {...args} defaultValue={1} />
</div>

Current Value: {$inputStore[args.name]}
<div class="mt-4">URL: {storyIframeURL}</div>
</Story>
<Story
name="URL params multiple components"
let:args
args={{
data: 'hashtags',
value: 'id',
label: 'tag'
}}
>
<div class="mb-8">
<ButtonGroup {...args} defaultValue={1} name="buttonGroup_A" />
</div>

Current Value: {$inputStore['buttonGroup_A']}
<div>URL: {storyIframeURL}</div>
<button
class="mt-4 p-1 border bg-info/60 hover:bg-info/40 active:bg-info/20 rounded-md text-sm

"
on:click={() => window.open(storyIframeURL, '_blank')}>Go to URL</button
>

<div class="mb-8">
<ButtonGroup {...args} data={undefined} name="buttonGroup_B">
<ButtonGroupItem valueLabel="Option 1" value={1} default />
<ButtonGroupItem valueLabel="Option 2" value={2} />
<ButtonGroupItem valueLabel="Option 3" value={3} />
<ButtonGroupItem valueLabel="Option 4" value={4} />
</ButtonGroup>
</div>

Current Value: {$inputStore['buttonGroup_B']}
<div class="mt-4">URL: {storyIframeURL}</div>
<button
class="mt-4 p-1 border bg-info/60 hover:bg-info/40 active:bg-info/20 rounded-md text-sm

"
on:click={() => window.open(storyIframeURL, '_blank')}>Go to URL</button
>
</Story>
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
</script>

<script>
import { page } from '$app/stores';
import { hydrateFromUrlParam, updateUrlParam } from '@evidence-dev/sdk/utils/svelte';
import { presets, setButtonGroupContext } from './lib.js';
import { writable, readonly } from 'svelte/store';
import { getInputContext } from '@evidence-dev/sdk/utils/svelte';
import { setContext } from 'svelte';
import { buildReactiveInputQuery } from '@evidence-dev/component-utilities/buildQuery';
import Info from '../../../unsorted/ui/Info.svelte';
import ButtonGroupItem from './ButtonGroupItem.svelte';
import { page } from '$app/stores';
import HiddenInPrint from '../shared/HiddenInPrint.svelte';
import QueryLoad from '$lib/atoms/query-load/QueryLoad.svelte';
import { getThemeStores } from '../../../themes/themes.js';
Expand All @@ -37,6 +38,7 @@

/** @type {string | undefined} */
export let defaultValue = undefined;
$: console.log(defaultValue, 'defaultValue');

setContext('button-display', display);

Expand All @@ -50,11 +52,19 @@

const valueStore = writable(null);

hydrateFromUrlParam(name, (v) => {
if (v) {
defaultValue = v;
}
});
setContext('button-group-defaultValue', writable(defaultValue));

// TODO: Use getInputSetter instead
setButtonGroupContext((v) => {
$valueStore = v;
// the assignment to $inputs is necessary to trigger the change on SSR
$inputs[name] = v?.value ?? null;
//
updateUrlParam(name, v?.value);
}, readonly(valueStore));

/////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
/** @type {string} */
export let defaultValue;

let buttonGroupDefaultValue = getContext('button-group-defaultValue');

let display = getContext('button-display');

const { update, value: currentValue } = getButtonGroupContext();
Expand All @@ -30,11 +32,9 @@
let _default = false;
export { _default as default };

if (_default) {
if ($buttonGroupDefaultValue === value || defaultValue === value) {
update({ valueLabel, value });
}

if (defaultValue === value) {
} else if (_default && !$buttonGroupDefaultValue) {
update({ valueLabel, value });
}
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@
import Checkbox from './Checkbox.svelte';
import { getInputContext } from '@evidence-dev/sdk/utils/svelte';
const inputStore = getInputContext();

let storyIframeURL = '';

const updateURL = () => {
storyIframeURL = window.location.href;

// Try forcing Storybook to recognize the change
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.src = iframe.src; // Force reload
}
};

(function () {
const pushState = history.pushState;
const replaceState = history.replaceState;

history.pushState = function () {
pushState.apply(history, arguments);
updateURL();
};

history.replaceState = function () {
replaceState.apply(history, arguments);
updateURL();
};

window.addEventListener('popstate', updateURL);
})();
</script>

<Story name="Base" let:args>
Expand All @@ -40,3 +69,17 @@
<Story name="Checkbox w/ Title undefined" let:args>
<Checkbox defaultValue="true" name="undefinedTitle" {...args} />
</Story>
<Story name="URL Params" let:args>
<Checkbox title="string true" defaultValue="true" name="string_true_url" {...args} />
<p>{$inputStore.string_true}</p>
<Checkbox title="boolean true" checked={true} name="boolean_true_url" {...args} />
<p>{$inputStore.boolean_true}</p>

<div class="mt-4">URL: {storyIframeURL}</div>
<button
class="mt-4 p-1 border bg-info/60 hover:bg-info/40 active:bg-info/20 rounded-md text-sm

"
on:click={() => window.open(storyIframeURL, '_blank')}>Go to URL</button
>
</Story>
Loading
Loading