Variables: Url sync for MultiValueVariable (#59371)

* Initial take on variable url sync

* Url sync is working

* Fixing test
This commit is contained in:
Torkel Ödegaard
2022-12-01 09:48:26 +01:00
committed by GitHub
parent 8e929163a8
commit 5281cff94d
6 changed files with 131 additions and 23 deletions
+1 -2
View File
@@ -151,14 +151,13 @@ export function isSceneObject(obj: any): obj is SceneObject {
return obj.useState !== undefined;
}
/** These functions are still just temporary until this get's refined */
export interface SceneObjectWithUrlSync<TState> extends SceneObject {
getUrlState(state: TState): SceneObjectUrlValues;
updateFromUrl(values: SceneObjectUrlValues): void;
}
export interface SceneObjectUrlSyncHandler<TState> {
getKeys(): Set<string>;
getKeys(): string[];
getUrlState(state: TState): SceneObjectUrlValues;
updateFromUrl(values: SceneObjectUrlValues): void;
}
@@ -6,17 +6,17 @@ import {
} from '../core/types';
interface SceneObjectUrlSyncConfigOptions {
keys?: string[];
keys: string[];
}
export class SceneObjectUrlSyncConfig<TState extends SceneObjectState> implements SceneObjectUrlSyncHandler<TState> {
private _keys: Set<string>;
private _keys: string[];
public constructor(private _sceneObject: SceneObjectWithUrlSync<TState>, _options: SceneObjectUrlSyncConfigOptions) {
this._keys = new Set(_options.keys);
this._keys = _options.keys;
}
public getKeys(): Set<string> {
public getKeys(): string[] {
return this._keys;
}
@@ -17,9 +17,7 @@ interface TestObjectState extends SceneLayoutChildState {
}
class TestObj extends SceneObjectBase<TestObjectState> {
protected _urlSync = new SceneObjectUrlSyncConfig(this, {
keys: ['name', 'array'],
});
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['name', 'array'] });
public getUrlState(state: TestObjectState) {
return { name: state.name, array: state.array };
@@ -74,12 +72,6 @@ describe('UrlSyncManager', () => {
// Should not update url
expect(locationUpdates.length).toBe(1);
// When clearing url (via go back)
locationService.getHistory().goBack();
// Should restore to initial state
expect(obj.state.name).toBe('test');
});
});
@@ -104,7 +96,8 @@ describe('UrlSyncManager', () => {
expect(obj.state.name).toBe('test2');
// When relevant key is cleared (say go back)
locationService.partial({ name: null });
locationService.getHistory().goBack();
// Should revert to initial state
expect(obj.state.name).toBe('test');
@@ -63,7 +63,7 @@ export class UrlSyncManager {
}
if (Object.keys(mappedUpdated).length > 0) {
locationService.partial(mappedUpdated, false);
locationService.partial(mappedUpdated, true);
}
}
};
@@ -153,4 +153,64 @@ describe('MultiValueVariable', () => {
expect(variable.getValue()).toEqual(['1', '2']);
});
});
describe('Url syncing', () => {
it('getUrlState should return single value state if value is single value', async () => {
const variable = new ExampleVariable({
name: 'test',
options: [],
optionsToReturn: [],
value: '1',
text: 'A',
});
expect(variable.urlSync?.getUrlState(variable.state)).toEqual({ ['var-test']: '1' });
});
it('getUrlState should return string array if value is string array', async () => {
const variable = new ExampleVariable({
name: 'test',
options: [],
optionsToReturn: [],
value: ['1', '2'],
text: ['A', 'B'],
});
expect(variable.urlSync?.getUrlState(variable.state)).toEqual({ ['var-test']: ['1', '2'] });
});
it('fromUrlState should update value for single value', async () => {
const variable = new ExampleVariable({
name: 'test',
options: [
{ label: 'A', value: '1' },
{ label: 'B', value: '2' },
],
optionsToReturn: [],
value: '1',
text: 'A',
});
variable.urlSync?.updateFromUrl({ ['var-test']: '2' });
expect(variable.state.value).toEqual('2');
expect(variable.state.text).toEqual('B');
});
it('fromUrlState should update value for array value', async () => {
const variable = new ExampleVariable({
name: 'test',
options: [
{ label: 'A', value: '1' },
{ label: 'B', value: '2' },
],
optionsToReturn: [],
value: '1',
text: 'A',
});
variable.urlSync?.updateFromUrl({ ['var-test']: ['2', '1'] });
expect(variable.state.value).toEqual(['2', '1']);
expect(variable.state.text).toEqual(['B', 'A']);
});
});
});
@@ -4,7 +4,7 @@ import { map, Observable } from 'rxjs';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
import { SceneObjectBase } from '../../core/SceneObjectBase';
import { SceneObject } from '../../core/types';
import { SceneObject, SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '../../core/types';
import {
SceneVariable,
SceneVariableValueChangedEvent,
@@ -12,6 +12,7 @@ import {
ValidateAndUpdateResult,
VariableValue,
VariableValueOption,
VariableValueSingle,
} from '../types';
export interface MultiValueVariableState extends SceneVariableState {
@@ -29,6 +30,8 @@ export abstract class MultiValueVariable<TState extends MultiValueVariableState
extends SceneObjectBase<TState>
implements SceneVariable<TState>
{
protected _urlSync: SceneObjectUrlSyncHandler<TState> = new MultiValueUrlSyncHandler(this);
/**
* The source of value options.
*/
@@ -124,20 +127,38 @@ export abstract class MultiValueVariable<TState extends MultiValueVariableState
return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
}
private setStateAndPublishValueChangedEvent(state: Partial<MultiValueVariableState>) {
this.setStateHelper(state);
}
/**
* Change the value and publish SceneVariableValueChangedEvent event
*/
public changeValueTo(value: VariableValue, text?: VariableValue) {
if (value !== this.state.value || text !== this.state.text) {
this.setStateAndPublishValueChangedEvent({ value, text, loading: false });
if (!text) {
if (Array.isArray(value)) {
text = value.map((v) => this.findLabelTextForValue(v));
} else {
text = this.findLabelTextForValue(value);
}
}
this.setStateHelper({ value, text, loading: false });
this.publishEvent(new SceneVariableValueChangedEvent(this), true);
}
}
private findLabelTextForValue(value: VariableValueSingle): VariableValueSingle {
const option = this.state.options.find((x) => x.value === value);
if (option) {
return option.label;
}
const optionByLabel = this.state.options.find((x) => x.label === value);
if (optionByLabel) {
return optionByLabel.label;
}
return value;
}
/**
* This helper function is to counter the contravariance of setState
*/
@@ -146,3 +167,38 @@ export abstract class MultiValueVariable<TState extends MultiValueVariableState
test.setState(state);
}
}
export class MultiValueUrlSyncHandler<TState extends MultiValueVariableState = MultiValueVariableState>
implements SceneObjectUrlSyncHandler<TState>
{
public constructor(private _sceneObject: MultiValueVariable<TState>) {}
private getKey(): string {
return `var-${this._sceneObject.state.name}`;
}
public getKeys(): string[] {
return [this.getKey()];
}
public getUrlState(state: TState): SceneObjectUrlValues {
let urlValue: string | string[] | null = null;
let value = state.value;
if (Array.isArray(value)) {
urlValue = value.map(String);
} else {
urlValue = String(value);
}
return { [this.getKey()]: urlValue };
}
public updateFromUrl(values: SceneObjectUrlValues): void {
const urlValue = values[this.getKey()];
if (urlValue != null) {
this._sceneObject.changeValueTo(urlValue);
}
}
}