Commit e26c093a authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '357216-local-storage-sync-serialize-by-default' into 'master'

Serialize and deserialize by default for LocalStorageSync component

See merge request gitlab-org/gitlab!84159
parents 724e4858 d808cf4d
...@@ -57,6 +57,7 @@ export default { ...@@ -57,6 +57,7 @@ export default {
:value="sortDirection" :value="sortDirection"
:storage-key="storageKey" :storage-key="storageKey"
:persist="persistSortOrder" :persist="persistSortOrder"
as-string
@input="setDiscussionSortDirection({ direction: $event })" @input="setDiscussionSortDirection({ direction: $event })"
/> />
<gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile"> <gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile">
......
...@@ -99,7 +99,6 @@ export default { ...@@ -99,7 +99,6 @@ export default {
<local-storage-sync <local-storage-sync
storage-key="package_registry_list_sorting" storage-key="package_registry_list_sorting"
:value="sorting" :value="sorting"
as-json
@input="updateSorting" @input="updateSorting"
> >
<url-sync> <url-sync>
......
...@@ -273,6 +273,7 @@ export default { ...@@ -273,6 +273,7 @@ export default {
<local-storage-sync <local-storage-sync
:storage-key="$options.viewTypeKey" :storage-key="$options.viewTypeKey"
:value="currentViewType" :value="currentViewType"
as-string
@input="updateViewType" @input="updateViewType"
> >
<graph-view-selector <graph-view-selector
......
...@@ -144,7 +144,6 @@ export default { ...@@ -144,7 +144,6 @@ export default {
<local-storage-sync <local-storage-sync
v-model="autoDevopsEnabledAlertDismissedProjects" v-model="autoDevopsEnabledAlertDismissedProjects"
:storage-key="$options.autoDevopsEnabledAlertStorageKey" :storage-key="$options.autoDevopsEnabledAlertStorageKey"
as-json
/> />
<user-callout-dismisser <user-callout-dismisser
......
...@@ -112,7 +112,6 @@ export default { ...@@ -112,7 +112,6 @@ export default {
v-model="mergeRequestMeta" v-model="mergeRequestMeta"
:storage-key="$options.storageKey" :storage-key="$options.storageKey"
:clear="clearStorage" :clear="clearStorage"
as-json
/> />
<edit-meta-controls <edit-meta-controls
ref="editMetaControls" ref="editMetaControls"
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
<template> <template>
<div v-show="showAlert"> <div v-show="showAlert">
<local-storage-sync v-model="isDismissed" :storage-key="storageKey" as-json /> <local-storage-sync v-model="isDismissed" :storage-key="storageKey" />
<gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert"> <gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert">
<slot></slot> <slot></slot>
</gl-alert> </gl-alert>
......
<script> <script>
import { isEqual } from 'lodash'; import { isEqual, isString } from 'lodash';
/**
* This component will save and restore a value to and from localStorage.
* The value will be saved only when the value changes; the initial value won't be saved.
*
* By default, the value will be saved using JSON.stringify(), and retrieved back using JSON.parse().
*
* If you would like to save the raw string instead, you may set the 'asString' prop to true, though be aware that this is a
* legacy prop to maintain backwards compatibility.
*
* For new components saving data for the first time, it's recommended to not use 'asString' even if you're saving a string; it will still be
* saved and restored properly using JSON.stringify()/JSON.parse().
*/
export default { export default {
props: { props: {
storageKey: { storageKey: {
...@@ -12,7 +24,7 @@ export default { ...@@ -12,7 +24,7 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
asJson: { asString: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
...@@ -30,6 +42,8 @@ export default { ...@@ -30,6 +42,8 @@ export default {
}, },
watch: { watch: {
value(newVal) { value(newVal) {
if (!this.persist) return;
this.saveValue(this.serialize(newVal)); this.saveValue(this.serialize(newVal));
}, },
clear(newVal) { clear(newVal) {
...@@ -67,15 +81,22 @@ export default { ...@@ -67,15 +81,22 @@ export default {
} }
}, },
saveValue(val) { saveValue(val) {
if (!this.persist) return;
localStorage.setItem(this.storageKey, val); localStorage.setItem(this.storageKey, val);
}, },
serialize(val) { serialize(val) {
return this.asJson ? JSON.stringify(val) : val; if (!isString(val) && this.asString) {
// eslint-disable-next-line no-console
console.warn(
`[gitlab] LocalStorageSync is saving`,
val,
`to the key "${this.storageKey}", but it is not a string and the 'asString' prop is true. This will save and restore the stringified value rather than the original value. If this is not intended, please remove or set the 'asString' prop to false.`,
);
}
return this.asString ? val : JSON.stringify(val);
}, },
deserialize(val) { deserialize(val) {
return this.asJson ? JSON.parse(val) : val; return this.asString ? val : JSON.parse(val);
}, },
}, },
render() { render() {
......
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
</script> </script>
<template> <template>
<local-storage-sync :storage-key="storageKey" :value="selected" @input="setSelected"> <local-storage-sync :storage-key="storageKey" :value="selected" as-string @input="setSelected">
<gl-dropdown :text="dropdownText" lazy> <gl-dropdown :text="dropdownText" lazy>
<gl-dropdown-item <gl-dropdown-item
v-for="option in parsedOptions" v-for="option in parsedOptions"
......
...@@ -314,6 +314,7 @@ export default { ...@@ -314,6 +314,7 @@ export default {
<local-storage-sync <local-storage-sync
storage-key="gl-web-ide-button-selected" storage-key="gl-web-ide-button-selected"
:value="selection" :value="selection"
as-string
@input="select" @input="select"
/> />
<gl-modal <gl-modal
......
...@@ -157,8 +157,8 @@ export default { ...@@ -157,8 +157,8 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" as-json /> <local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" />
<local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" as-json /> <local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" />
<h4>{{ tableHeader }}</h4> <h4>{{ tableHeader }}</h4>
<gl-table <gl-table
:fields="tableHeaderFields" :fields="tableHeaderFields"
......
...@@ -140,8 +140,8 @@ export default { ...@@ -140,8 +140,8 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" as-json /> <local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" />
<local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" as-json /> <local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" />
<gl-table <gl-table
:fields="tableHeaderFields" :fields="tableHeaderFields"
:items="enabledNamespaces" :items="enabledNamespaces"
......
...@@ -74,7 +74,6 @@ export default { ...@@ -74,7 +74,6 @@ export default {
<local-storage-sync <local-storage-sync
v-model="userManuallyCollapsed" v-model="userManuallyCollapsed"
:storage-key="$options.MR_APPROVALS_PROMO_DISMISSED" :storage-key="$options.MR_APPROVALS_PROMO_DISMISSED"
as-json
/> />
<template v-if="isReady"> <template v-if="isReady">
<p class="gl-mb-0 gl-text-gray-500"> <p class="gl-mb-0 gl-text-gray-500">
......
<script> <script>
import { GlToggle } from '@gitlab/ui'; import { GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default { export default {
...@@ -20,9 +19,6 @@ export default { ...@@ -20,9 +19,6 @@ export default {
onToggle(val) { onToggle(val) {
this.setShowLabels(val); this.setShowLabels(val);
}, },
onStorageUpdate(val) {
this.setShowLabels(parseBoolean(val));
},
}, },
}; };
</script> </script>
...@@ -30,9 +26,9 @@ export default { ...@@ -30,9 +26,9 @@ export default {
<template> <template>
<div class="board-labels-toggle-wrapper gl-display-flex gl-align-items-center gl-ml-3"> <div class="board-labels-toggle-wrapper gl-display-flex gl-align-items-center gl-ml-3">
<local-storage-sync <local-storage-sync
:value="isShowingLabels"
storage-key="gl-show-board-labels" storage-key="gl-show-board-labels"
:value="JSON.stringify(isShowingLabels)" @input="setShowLabels"
@input="onStorageUpdate"
/> />
<gl-toggle <gl-toggle
:value="isShowingLabels" :value="isShowingLabels"
......
...@@ -324,7 +324,6 @@ export default { ...@@ -324,7 +324,6 @@ export default {
<gl-form novalidate @submit.prevent="onSubmit()"> <gl-form novalidate @submit.prevent="onSubmit()">
<local-storage-sync <local-storage-sync
v-if="!isEdit" v-if="!isEdit"
as-json
:storage-key="storageKey" :storage-key="storageKey"
:clear="clearStorage" :clear="clearStorage"
:value="formFieldValues" :value="formFieldValues"
......
<script> <script>
import { difference } from 'lodash'; import { difference } from 'lodash';
import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils'; import { getCookie, setCookie } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { translateScannerNames } from '~/security_configuration/utils'; import { translateScannerNames } from '~/security_configuration/utils';
...@@ -86,8 +86,8 @@ export default { ...@@ -86,8 +86,8 @@ export default {
setCookie(this.$options.autoFixUserCalloutCookieName, 'true'); setCookie(this.$options.autoFixUserCalloutCookieName, 'true');
this.shouldShowAutoFixUserCallout = false; this.shouldShowAutoFixUserCallout = false;
}, },
setScannerAlertDismissed(value) { dismissScannerAlert() {
this.scannerAlertDismissed = parseBoolean(value); this.scannerAlertDismissed = true;
}, },
}, },
projectVulnerabilitiesQuery, projectVulnerabilitiesQuery,
...@@ -101,15 +101,14 @@ export default { ...@@ -101,15 +101,14 @@ export default {
<div v-else> <div v-else>
<local-storage-sync <local-storage-sync
:value="String(scannerAlertDismissed)" v-model="scannerAlertDismissed"
:storage-key="$options.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY" :storage-key="$options.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY"
@input="setScannerAlertDismissed"
/> />
<security-scanner-alert <security-scanner-alert
v-if="shouldShowScannersAlert" v-if="shouldShowScannersAlert"
:not-enabled-scanners="notEnabledSecurityScanners" :not-enabled-scanners="notEnabledSecurityScanners"
:no-pipeline-run-scanners="noPipelineRunSecurityScanners" :no-pipeline-run-scanners="noPipelineRunSecurityScanners"
@dismiss="setScannerAlertDismissed('true')" @dismiss="dismissScannerAlert"
/> />
<auto-fix-user-callout <auto-fix-user-callout
......
...@@ -262,7 +262,6 @@ export default { ...@@ -262,7 +262,6 @@ export default {
<local-storage-sync <local-storage-sync
v-if="shouldShowPageSizeSelector" v-if="shouldShowPageSizeSelector"
v-model="pageSize" v-model="pageSize"
as-json
:storage-key="$options.PAGE_SIZE_STORAGE_KEY" :storage-key="$options.PAGE_SIZE_STORAGE_KEY"
> >
<page-size-selector v-model="pageSize" class="gl-absolute gl-right-0" /> <page-size-selector v-model="pageSize" class="gl-absolute gl-right-0" />
......
...@@ -83,7 +83,7 @@ export default { ...@@ -83,7 +83,7 @@ export default {
</script> </script>
<template> <template>
<local-storage-sync v-model="surveyShowDate" :storage-key="storageKey"> <local-storage-sync v-model="surveyShowDate" :storage-key="storageKey" as-string>
<gl-banner <gl-banner
v-if="shouldShowSurvey" v-if="shouldShowSurvey"
:title="title" :title="title"
......
...@@ -373,10 +373,7 @@ describe('Vulnerability list GraphQL component', () => { ...@@ -373,10 +373,7 @@ describe('Vulnerability list GraphQL component', () => {
findPageSizeSelector().vm.$emit('input', pageSize); findPageSizeSelector().vm.$emit('input', pageSize);
await nextTick(); await nextTick();
expect(findLocalStorageSync().props()).toMatchObject({ expect(findLocalStorageSync().props('value')).toBe(pageSize);
asJson: true,
value: pageSize,
});
}); });
}); });
}); });
......
...@@ -15,6 +15,7 @@ describe('Shared Survey Banner component', () => { ...@@ -15,6 +15,7 @@ describe('Shared Survey Banner component', () => {
let wrapper; let wrapper;
const findGlBanner = () => wrapper.findComponent(GlBanner); const findGlBanner = () => wrapper.findComponent(GlBanner);
const findAskLaterButton = () => wrapper.findByTestId('ask-later-button'); const findAskLaterButton = () => wrapper.findByTestId('ask-later-button');
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const getOffsetDateString = (days) => { const getOffsetDateString = (days) => {
const date = new Date(); const date = new Date();
...@@ -60,6 +61,7 @@ describe('Shared Survey Banner component', () => { ...@@ -60,6 +61,7 @@ describe('Shared Survey Banner component', () => {
expect(findGlBanner().html()).toContain(description); expect(findGlBanner().html()).toContain(description);
expect(findAskLaterButton().exists()).toBe(true); expect(findAskLaterButton().exists()).toBe(true);
expect(findLocalStorageSync().props('asString')).toBe(true);
expect(findGlBanner().props()).toMatchObject({ expect(findGlBanner().props()).toMatchObject({
title, title,
buttonText, buttonText,
......
...@@ -38,8 +38,8 @@ describe('Sort Discussion component', () => { ...@@ -38,8 +38,8 @@ describe('Sort Discussion component', () => {
createComponent(); createComponent();
}); });
it('has local storage sync', () => { it('has local storage sync with the correct props', () => {
expect(findLocalStorageSync().exists()).toBe(true); expect(findLocalStorageSync().props('asString')).toBe(true);
}); });
it('calls setDiscussionSortDirection when update is emitted', () => { it('calls setDiscussionSortDirection when update is emitted', () => {
......
...@@ -73,7 +73,6 @@ describe('Package Search', () => { ...@@ -73,7 +73,6 @@ describe('Package Search', () => {
mountComponent(); mountComponent();
expect(findLocalStorageSync().props()).toMatchObject({ expect(findLocalStorageSync().props()).toMatchObject({
asJson: true,
storageKey: 'package_registry_list_sorting', storageKey: 'package_registry_list_sorting',
value: { value: {
orderBy: LIST_KEY_CREATED_AT, orderBy: LIST_KEY_CREATED_AT,
......
...@@ -30,6 +30,7 @@ import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; ...@@ -30,6 +30,7 @@ import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils'; import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
import * as sentryUtils from '~/pipelines/utils'; import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data'; import { mockRunningPipelineHeaderData } from '../mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data'; import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
...@@ -55,6 +56,7 @@ describe('Pipeline graph wrapper', () => { ...@@ -55,6 +56,7 @@ describe('Pipeline graph wrapper', () => {
wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]'); wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
const getViewSelector = () => wrapper.find(GraphViewSelector); const getViewSelector = () => wrapper.find(GraphViewSelector);
const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert); const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const createComponent = ({ const createComponent = ({
apolloProvider, apolloProvider,
...@@ -376,6 +378,10 @@ describe('Pipeline graph wrapper', () => { ...@@ -376,6 +378,10 @@ describe('Pipeline graph wrapper', () => {
localStorage.clear(); localStorage.clear();
}); });
it('sets the asString prop on the LocalStorageSync component', () => {
expect(getLocalStorageSync().props('asString')).toBe(true);
});
it('reads the view type from localStorage when available', () => { it('reads the view type from localStorage when available', () => {
const viewSelectorNeedsSegment = wrapper const viewSelectorNeedsSegment = wrapper
.find(GlButtonGroup) .find(GlButtonGroup)
......
...@@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => { ...@@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => {
}); });
describe('local storage sync', () => { describe('local storage sync', () => {
it('uses the local storage sync component', () => { it('uses the local storage sync component with the correct props', () => {
createComponent(); createComponent();
expect(findLocalStorageSync().exists()).toBe(true); expect(findLocalStorageSync().props('asString')).toBe(true);
}); });
it('passes the right props', () => { it('passes the right props', () => {
......
...@@ -261,7 +261,10 @@ describe('Web IDE link component', () => { ...@@ -261,7 +261,10 @@ describe('Web IDE link component', () => {
}); });
it('should update local storage when selection changes', async () => { it('should update local storage when selection changes', async () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key); expect(findLocalStorageSync().props()).toMatchObject({
asString: true,
value: ACTION_WEB_IDE.key,
});
findActionsButton().vm.$emit('select', ACTION_GITPOD.key); findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment