Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import './index';

import { Summary } from '../../../storygen/Summary';
import { getMeta } from '../../../storygen/getMeta';
import { getStory } from '../../../storygen/getStory';

const summary: Summary = {
href: 'https://demo.api/hapi/stores/0',
parent: 'https://demo.api/hapi/stores',
nucleon: true,
localName: 'foxy-data-retention-settings-form',
translatable: true,
configurable: {},
};

export default getMeta(summary);

export const Playground = getStory({ ...summary, code: true });
export const Empty = getStory(summary);
export const Error = getStory(summary);
export const Busy = getStory(summary);

Empty.args.href = '';
Error.args.href = 'https://demo.api/virtual/empty?status=404';
Busy.args.href = 'https://demo.api/virtual/stall';
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import './index';

import { expect, fixture, html } from '@open-wc/testing';
import { InternalForm } from '../../internal/InternalForm/InternalForm';
import { DataRetentionSettingsForm as Form } from './DataRetentionSettingsForm';

describe('DataRetentionSettingsForm', () => {
it('imports and registers foxy-internal-summary-control', () => {
expect(customElements.get('foxy-internal-summary-control')).to.exist;
});

it('imports and registers foxy-internal-switch-control', () => {
expect(customElements.get('foxy-internal-switch-control')).to.exist;
});

it('imports and registers foxy-internal-number-control', () => {
expect(customElements.get('foxy-internal-number-control')).to.exist;
});

it('imports and registers foxy-internal-form', () => {
expect(customElements.get('foxy-internal-form')).to.exist;
});

it('registers as foxy-data-retention-settings-form', () => {
expect(customElements.get('foxy-data-retention-settings-form')).to.equal(Form);
});

it('extends InternalForm', () => {
expect(new Form()).to.be.instanceOf(InternalForm);
});

it('has a default i18n namespace of "data-retention-settings-form"', () => {
expect(Form).to.have.property('defaultNS', 'data-retention-settings-form');
});

describe('v8n', () => {
it('allows an unset auto_anonymize_days', () => {
const element = new Form();
expect(element.errors).to.not.include('auto-anonymize-days:v8n_too_small');
expect(element.errors).to.not.include('auto-anonymize-days:v8n_required');
});

it('produces "auto-anonymize-days:v8n_too_small" when below 90', () => {
const element = new Form();
element.edit({ data_retention: { auto_anonymize: false, auto_anonymize_days: 89 } });
expect(element.errors).to.include('auto-anonymize-days:v8n_too_small');

element.edit({ data_retention: { auto_anonymize: false, auto_anonymize_days: 90 } });
expect(element.errors).to.not.include('auto-anonymize-days:v8n_too_small');
});

it('requires days when auto_anonymize is enabled', () => {
const element = new Form();
element.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: null } });
expect(element.errors).to.include('auto-anonymize-days:v8n_required');

element.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: 365 } });
expect(element.errors).to.not.include('auto-anonymize-days:v8n_required');
});
});

describe('hiddenSelector', () => {
it('always hides delete and timestamps', async () => {
const element = await fixture<Form>(
html`<foxy-data-retention-settings-form></foxy-data-retention-settings-form>`
);

expect(element.hiddenSelector.matches('delete', true)).to.be.true;
expect(element.hiddenSelector.matches('timestamps', true)).to.be.true;
});

it('hides the days field when auto_anonymize is off', async () => {
const element = await fixture<Form>(
html`<foxy-data-retention-settings-form></foxy-data-retention-settings-form>`
);

element.edit({ data_retention: { auto_anonymize: false, auto_anonymize_days: null } });
expect(element.hiddenSelector.matches('general:auto-anonymize-days', true)).to.be.true;
});

it('shows the days field when auto_anonymize is on', async () => {
const element = await fixture<Form>(
html`<foxy-data-retention-settings-form></foxy-data-retention-settings-form>`
);

element.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: 365 } });
expect(element.hiddenSelector.matches('general:auto-anonymize-days', true)).to.be.false;
});
});

it('renders a switch control for auto_anonymize', async () => {
const element = await fixture<Form>(
html`<foxy-data-retention-settings-form></foxy-data-retention-settings-form>`
);

const control = element.renderRoot.querySelector(
'foxy-internal-switch-control[infer="auto-anonymize"]'
);

expect(control).to.exist;
});

it('renders a number control for auto_anonymize_days when enabled', async () => {
const element = await fixture<Form>(
html`<foxy-data-retention-settings-form></foxy-data-retention-settings-form>`
);

element.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: 365 } });
await element.requestUpdate();

const control = element.renderRoot.querySelector(
'foxy-internal-number-control[infer="auto-anonymize-days"]'
);

expect(control).to.exist;
expect(control).to.have.attribute('min', '90');
});

it('reads and writes auto_anonymize via the store data_retention field', async () => {
const element = await fixture<Form>(
html`<foxy-data-retention-settings-form></foxy-data-retention-settings-form>`
);

element.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: 120 } });
await element.requestUpdate();

const control = element.renderRoot.querySelector(
'foxy-internal-switch-control[infer="auto-anonymize"]'
) as HTMLElement & { getValue: () => unknown; setValue: (v: unknown) => void };

expect(control.getValue()).to.equal(true);

control.setValue(false);
expect(element.form.data_retention).to.deep.equal({
auto_anonymize: false,
auto_anonymize_days: 120,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { TemplateResult } from 'lit-html';
import type { NucleonV8N } from '../NucleonElement/types';
import type { Data, ParsedDataRetention } from './types';

import { TranslatableMixin } from '../../../mixins/translatable';
import { BooleanSelector } from '@foxy.io/sdk/core';
import { InternalForm } from '../../internal/InternalForm/InternalForm';
import { html } from 'lit-html';

const NS = 'data-retention-settings-form';
const Base = TranslatableMixin(InternalForm, NS);

/**
* Form element for editing a store's data retention settings.
*
* Data retention lives in the `data_retention` field of the `fx:store`
* resource, so this form binds to a store and edits that nested object: an
* auto-anonymization toggle and a "days of inactivity" value with a hard
* 90-day minimum (enforced client-side and by the API).
*
* @element foxy-data-retention-settings-form
* @since 1.52.0
*/
export class DataRetentionSettingsForm extends Base<Data> {
static get v8n(): NucleonV8N<Data> {
return [
({ data_retention: dr }) => {
const v = dr?.auto_anonymize_days;
return (
v === null ||
v === undefined ||
(Number.isInteger(v) && v >= 90) ||
'auto-anonymize-days:v8n_too_small'
);
},
({ data_retention: dr }) => {
const v = dr?.auto_anonymize_days;
return (
!dr?.auto_anonymize ||
(typeof v === 'number' && v >= 90) ||
'auto-anonymize-days:v8n_required'
);
},
];
}

private readonly __getAutoAnonymize = (): boolean => {
return this.__getDataRetention().auto_anonymize;
};

private readonly __setAutoAnonymize = (newValue: boolean): void => {
this.edit({ data_retention: { ...this.__getDataRetention(), auto_anonymize: newValue } });
};

private readonly __getAutoAnonymizeDays = (): number | null => {
return this.__getDataRetention().auto_anonymize_days;
};

private readonly __setAutoAnonymizeDays = (newValue: number): void => {
this.edit({ data_retention: { ...this.__getDataRetention(), auto_anonymize_days: newValue } });
};

get hiddenSelector(): BooleanSelector {
// No DELETE route (this edits a store field) and it carries no timestamps.
const alwaysMatch = ['delete', 'timestamps', super.hiddenSelector.toString()];
// The "days" field only applies when auto-anonymization is on.
if (!this.form.data_retention?.auto_anonymize)
alwaysMatch.unshift('general:auto-anonymize-days');
return new BooleanSelector(alwaysMatch.join(' ').trim());
}

renderBody(): TemplateResult {
return html`
<foxy-internal-summary-control infer="general">
<foxy-internal-switch-control
infer="auto-anonymize"
layout="summary-item"
.getValue=${this.__getAutoAnonymize}
.setValue=${this.__setAutoAnonymize}
>
</foxy-internal-switch-control>

<foxy-internal-number-control
layout="summary-item"
infer="auto-anonymize-days"
min="90"
step="1"
.getValue=${this.__getAutoAnonymizeDays}
.setValue=${this.__setAutoAnonymizeDays}
>
</foxy-internal-number-control>
</foxy-internal-summary-control>

${super.renderBody()}
`;
}

private __getDataRetention(): ParsedDataRetention {
const dr = this.form.data_retention;
return {
auto_anonymize: dr?.auto_anonymize ?? false,
auto_anonymize_days: dr?.auto_anonymize_days ?? null,
};
}
}
10 changes: 10 additions & 0 deletions src/elements/public/DataRetentionSettingsForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import '../../internal/InternalSummaryControl/index';
import '../../internal/InternalSwitchControl/index';
import '../../internal/InternalNumberControl/index';
import '../../internal/InternalForm/index';

import { DataRetentionSettingsForm } from './DataRetentionSettingsForm';

customElements.define('foxy-data-retention-settings-form', DataRetentionSettingsForm);

export { DataRetentionSettingsForm };
11 changes: 11 additions & 0 deletions src/elements/public/DataRetentionSettingsForm/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Resource } from '@foxy.io/sdk/core';
import type { Rels } from '@foxy.io/sdk/backend';

// This form edits the `data_retention` field of a store, so it binds to the
// store resource itself.
export type Data = Resource<Rels.Store>;

export type ParsedDataRetention = {
auto_anonymize: boolean;
auto_anonymize_days: number | null;
};
1 change: 1 addition & 0 deletions src/elements/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export { CustomerPortalSettingsForm } from './CustomerPortalSettingsForm/Custome
export { CustomersTable } from './CustomersTable/CustomersTable';
export { CustomFieldCard } from './CustomFieldCard/CustomFieldCard';
export { CustomFieldForm } from './CustomFieldForm/CustomFieldForm';
export { DataRetentionSettingsForm } from './DataRetentionSettingsForm/DataRetentionSettingsForm';
export { DiscountBuilder } from './DiscountBuilder/DiscountBuilder';
export { DiscountCard } from './DiscountCard/DiscountCard';
export { DiscountDetailCard } from './DiscountDetailCard/DiscountDetailCard';
Expand Down
9 changes: 9 additions & 0 deletions src/server/hapi/createDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,15 @@ export const createDataset: () => Dataset = () => ({
},
],

data_retention_settings: [
{
id: 0,
store_id: 0,
auto_anonymize: true,
auto_anonymize_days: 365,
},
],

customer_portal_settings: [
{
id: 0,
Expand Down
4 changes: 4 additions & 0 deletions src/server/hapi/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ export const links: Links = {
'fx:store': { href: `./stores/${store_id}` },
}),

data_retention_settings: ({ store_id }) => ({
'fx:store': { href: `./stores/${store_id}` },
}),

taxes: ({ id, store_id }) => ({
'fx:store': { href: `./stores/${store_id}` },
'fx:tax_item_categories': { href: `./tax_item_categories?tax_id=${id}` },
Expand Down
52 changes: 52 additions & 0 deletions src/static/translations/data-retention-settings-form/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"header": {
"title_existing": "Data retention",
"title_new": "Data retention",
"subtitle": "Automatic anonymization of inactive customers",
"copy-id": {
"failed_to_copy": "Failed to copy",
"click_to_copy": "Copy ID",
"copying": "Copying...",
"done": "Copied to clipboard"
},
"copy-json": {
"failed_to_copy": "Failed to copy",
"click_to_copy": "Copy source as JSON",
"copying": "Copying...",
"done": "Copied to clipboard"
}
},
"general": {
"label": "",
"helper_text": "",
"auto-anonymize": {
"label": "Auto-anonymize inactive customers",
"helper_text": "When enabled, customers with no activity for the configured number of days are automatically and irreversibly anonymized."
},
"auto-anonymize-days": {
"label": "Days of inactivity",
"placeholder": "365",
"helper_text": "Minimum 90 days. Customers inactive this long will be anonymized.",
"v8n_required": "Enter the number of inactive days (at least 90).",
"v8n_too_small": "Must be at least 90 days."
}
},
"timestamps": {
"date_created": "Created on",
"date_modified": "Last updated on",
"date": "{{value, date}}"
},
"delete": {
"delete": "Delete",
"cancel": "Cancel",
"delete_prompt": "Are you sure you'd like to reset these settings?"
},
"undo": { "caption": "Undo" },
"submit": { "caption": "Save changes" },
"create": { "caption": "Create" },
"spinner": {
"refresh": "Refresh",
"loading_busy": "Loading",
"loading_error": "Unknown error"
}
}