Skip to content

Commit 939c465

Browse files
committed
feat: add foxy-store-transaction-folder-card element
1 parent b2ab7f9 commit 939c465

File tree

9 files changed

+332
-0
lines changed

9 files changed

+332
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import './index';
2+
3+
import { Summary } from '../../../storygen/Summary';
4+
import { getMeta } from '../../../storygen/getMeta';
5+
import { getStory } from '../../../storygen/getStory';
6+
7+
const summary: Summary = {
8+
href: 'https://demo.api/hapi/transaction_folders/0',
9+
parent: 'https://demo.api/hapi/transaction_folders',
10+
nucleon: true,
11+
localName: 'foxy-store-transaction-folder-card',
12+
translatable: true,
13+
};
14+
15+
export default getMeta(summary);
16+
17+
export const Playground = getStory({ ...summary, code: true });
18+
export const Empty = getStory(summary);
19+
export const Error = getStory(summary);
20+
export const Busy = getStory(summary);
21+
22+
Empty.args.href = '';
23+
Error.args.href = 'https://demo.api/virtual/empty?status=404';
24+
Busy.args.href = 'https://demo.api/virtual/stall';
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { NucleonElement } from '../NucleonElement/NucleonElement';
2+
import type { FetchEvent } from '../NucleonElement/FetchEvent';
3+
import type { Data } from './types';
4+
5+
import './index';
6+
7+
import { StoreTransactionFolderCard as Card } from './StoreTransactionFolderCard';
8+
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
9+
import { InternalCard } from '../../internal/InternalCard/InternalCard';
10+
import { createRouter } from '../../../server/index';
11+
import { getTestData } from '../../../testgen/getTestData';
12+
import { getByKey } from '../../../testgen/getByKey';
13+
14+
async function waitForIdle(element: Card) {
15+
await waitUntil(
16+
() => {
17+
const loaders = element.renderRoot.querySelectorAll<NucleonElement<any>>('foxy-nucleon');
18+
return [...loaders].every(loader => loader.in('idle'));
19+
},
20+
'',
21+
{ timeout: 5000 }
22+
);
23+
}
24+
25+
describe('StoreTransactionFolderCard', () => {
26+
const OriginalResizeObserver = window.ResizeObserver;
27+
28+
// @ts-expect-error disabling ResizeObserver because it errors in test env
29+
before(() => (window.ResizeObserver = undefined));
30+
after(() => (window.ResizeObserver = OriginalResizeObserver));
31+
32+
it('defines foxy-nucleon', () => {
33+
const localName = 'foxy-nucleon';
34+
expect(customElements.get(localName)).to.exist;
35+
});
36+
37+
it('defines foxy-i18n', () => {
38+
const localName = 'foxy-i18n';
39+
expect(customElements.get(localName)).to.exist;
40+
});
41+
42+
it('defines itself as foxy-store-transaction-folder-card', () => {
43+
const localName = 'foxy-store-transaction-folder-card';
44+
expect(customElements.get(localName)).to.equal(Card);
45+
});
46+
47+
it('has a static property "countRefreshInterval"', () => {
48+
expect(Card).to.have.property('countRefreshInterval', 600000);
49+
});
50+
51+
it('extends InternalCard', () => {
52+
expect(new Card()).to.be.instanceOf(InternalCard);
53+
});
54+
55+
it('has a default i18n namespace "store-transaction-folder-card"', () => {
56+
expect(Card).to.have.property('defaultNS', 'store-transaction-folder-card');
57+
expect(new Card()).to.have.property('ns', 'store-transaction-folder-card');
58+
});
59+
60+
it('has a reactive property "getCountLoaderURL"', async () => {
61+
const def = { attribute: false };
62+
expect(Card).to.have.deep.nested.property('properties.getCountLoaderURL', def);
63+
64+
const layout = html`<foxy-store-transaction-folder-card></foxy-store-transaction-folder-card>`;
65+
const element = await fixture<Card>(layout);
66+
expect(element).to.have.deep.property('getCountLoaderURL', null);
67+
});
68+
69+
it('renders folder name when loaded', async () => {
70+
const layout = html`<foxy-store-transaction-folder-card></foxy-store-transaction-folder-card>`;
71+
const element = await fixture<Card>(layout);
72+
73+
expect(element.renderRoot).to.not.include.text('Test');
74+
75+
const folder = await getTestData<Data>('./hapi/transaction_folders/0');
76+
folder.name = 'Test';
77+
element.data = folder;
78+
await element.requestUpdate();
79+
80+
expect(element.renderRoot).to.include.text('Test');
81+
});
82+
83+
it('renders "no name" when folder name is empty', async () => {
84+
const layout = html`<foxy-store-transaction-folder-card></foxy-store-transaction-folder-card>`;
85+
const element = await fixture<Card>(layout);
86+
87+
expect(await getByKey(element, 'no_name')).to.exist;
88+
89+
const folder = await getTestData<Data>('./hapi/transaction_folders/0');
90+
folder.name = '';
91+
element.data = folder;
92+
await element.requestUpdate();
93+
94+
expect(await getByKey(element, 'no_name')).to.exist;
95+
96+
folder.name = 'Test';
97+
element.data = folder;
98+
await element.requestUpdate();
99+
100+
expect(await getByKey(element, 'no_name')).to.not.exist;
101+
});
102+
103+
it('renders a total count', async () => {
104+
const router = createRouter();
105+
const layout = html`
106+
<foxy-store-transaction-folder-card
107+
href="https://demo.api/hapi/transaction_folders/0"
108+
@fetch=${(evt: FetchEvent) => router.handleEvent(evt)}
109+
>
110+
</foxy-store-transaction-folder-card>
111+
`;
112+
113+
const element = await fixture<Card>(layout);
114+
await waitUntil(() => !!element.data);
115+
await waitForIdle(element);
116+
await element.requestUpdate();
117+
expect(element.renderRoot).to.include.text('1');
118+
119+
element.getCountLoaderURL = (defaultValue: URL) => {
120+
defaultValue.searchParams.set('is_test', 'false');
121+
return defaultValue;
122+
};
123+
element.requestUpdate();
124+
await waitForIdle(element);
125+
expect(element.renderRoot).to.include.text('0');
126+
});
127+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import type { PropertyDeclarations } from 'lit-element';
2+
import type { NucleonElement } from '../NucleonElement/NucleonElement';
3+
import type { TemplateResult } from 'lit-html';
4+
import type { Resource } from '@foxy.io/sdk/core';
5+
import type { Rels } from '@foxy.io/sdk/backend';
6+
import type { Data } from './types';
7+
8+
import { TranslatableMixin } from '../../../mixins/translatable';
9+
import { getResourceId } from '@foxy.io/sdk/core';
10+
import { InternalCard } from '../../internal/InternalCard/InternalCard';
11+
import { ifDefined } from 'lit-html/directives/if-defined';
12+
import { html, svg } from 'lit-html';
13+
import { classMap } from '../../../utils/class-map';
14+
15+
const NS = 'store-transaction-folder-card';
16+
const Base = TranslatableMixin(InternalCard, NS);
17+
18+
/**
19+
* Card element representing a `fx:store_transaction_folder` resource.
20+
*
21+
* @element foxy-store-transaction-folder-card
22+
* @since 1.46.0
23+
*/
24+
export class StoreTransactionFolderCard extends Base<Data> {
25+
static readonly countRefreshInterval: number = 600000;
26+
27+
static get properties(): PropertyDeclarations {
28+
return {
29+
...super.properties,
30+
getCountLoaderURL: { attribute: false },
31+
};
32+
}
33+
34+
getCountLoaderURL: ((defaultValue: URL) => URL) | null = null;
35+
36+
private readonly __countLoaderId = 'countLoader';
37+
38+
private readonly __storeLoaderId = 'storeLoader';
39+
40+
private __refreshTimeout: NodeJS.Timeout | null = null;
41+
42+
disconnectedCallback(): void {
43+
super.disconnectedCallback();
44+
if (typeof this.__refreshTimeout === 'number') clearTimeout(this.__refreshTimeout);
45+
}
46+
47+
updated(changes: Map<keyof this, unknown>): void {
48+
super.updated(changes);
49+
if (typeof this.__refreshTimeout !== 'number') {
50+
const constructor = this.constructor as typeof StoreTransactionFolderCard;
51+
const interval = constructor.countRefreshInterval;
52+
this.__refreshTimeout = setTimeout(() => this.__getCountLoader()?.refresh(), interval);
53+
}
54+
}
55+
56+
render(): TemplateResult {
57+
const count = this.__getCountLoader()?.data?.total_items;
58+
const store = this.__getStoreLoader()?.data;
59+
let countUrl: URL | undefined;
60+
61+
try {
62+
if (this.data && store) {
63+
countUrl = new URL(store._links['fx:transactions'].href);
64+
countUrl.searchParams.set('folder_id', String(getResourceId(this.data._links.self.href)));
65+
countUrl.searchParams.set('limit', '1');
66+
countUrl = this.getCountLoaderURL?.(countUrl) ?? countUrl;
67+
}
68+
} catch {
69+
countUrl = undefined;
70+
}
71+
72+
const colors: Record<string, string> = {
73+
'red': 'text-folder-red',
74+
'red_pale': 'text-folder-red-pale',
75+
'green': 'text-folder-green',
76+
'green_pale': 'text-folder-green-pale',
77+
'blue': 'text-folder-blue',
78+
'blue_pale': 'text-folder-blue-pale',
79+
'orange': 'text-folder-orange',
80+
'orange_pale': 'text-folder-orange-pale',
81+
'violet': 'text-folder-violet',
82+
'violet_pale': 'text-folder-violet-pale',
83+
'': 'text-body',
84+
};
85+
86+
const currentColor = this.form.color && this.form.color in colors ? this.form.color : '';
87+
88+
return html`
89+
<foxy-nucleon
90+
infer=""
91+
href=${ifDefined(this.data?._links['fx:store'].href)}
92+
id=${this.__storeLoaderId}
93+
@update=${() => this.requestUpdate()}
94+
>
95+
</foxy-nucleon>
96+
97+
<foxy-nucleon
98+
infer=""
99+
href=${ifDefined(countUrl?.toString())}
100+
id=${this.__countLoaderId}
101+
@update=${() => this.requestUpdate()}
102+
>
103+
</foxy-nucleon>
104+
105+
<div
106+
class=${classMap({
107+
'transition-colors flex items-center gap-s font-medium text-m leading-s rounded-s': true,
108+
'bg-contrast-5': !this.in('fail') && !this.data,
109+
'bg-error-10': this.in('fail'),
110+
})}
111+
>
112+
<span
113+
class=${classMap({
114+
'transition-opacity': true,
115+
'opacity-0': !this.data,
116+
[colors[currentColor]]: true,
117+
})}
118+
>
119+
${svg`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" style="width: 1em; height: 1em;"><path d="M2 3.5A1.5 1.5 0 0 1 3.5 2h2.879a1.5 1.5 0 0 1 1.06.44l1.122 1.12A1.5 1.5 0 0 0 9.62 4H12.5A1.5 1.5 0 0 1 14 5.5v1.401a2.986 2.986 0 0 0-1.5-.401h-9c-.546 0-1.059.146-1.5.401V3.5ZM2 9.5v3A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-3A1.5 1.5 0 0 0 12.5 8h-9A1.5 1.5 0 0 0 2 9.5Z" /></svg>`}
120+
</span>
121+
122+
<span
123+
class=${classMap({
124+
'transition-opacity truncate min-w-0': true,
125+
'opacity-0': !this.data,
126+
})}
127+
>
128+
${this.data?.name || html`<foxy-i18n infer="" key="no_name"></foxy-i18n>`}
129+
</span>
130+
131+
<span
132+
class=${classMap({
133+
'transition-opacity bg-contrast-5 px-xs rounded text-xs': true,
134+
'opacity-0': !this.data || typeof count !== 'number',
135+
})}
136+
>
137+
${count ?? 0}
138+
</span>
139+
</div>
140+
`;
141+
}
142+
143+
private __getCountLoader() {
144+
type AnyCollection = NucleonElement<Resource<Rels.Attributes>>;
145+
type Loader = Omit<AnyCollection, '_embedded'> & { _embedded: unknown };
146+
return this.renderRoot.querySelector<Loader>(`#${this.__countLoaderId}`);
147+
}
148+
149+
private __getStoreLoader() {
150+
type Loader = NucleonElement<Resource<Rels.Store>>;
151+
return this.renderRoot.querySelector<Loader>(`#${this.__storeLoaderId}`);
152+
}
153+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import '../../internal/InternalCard/index';
2+
3+
import '../NucleonElement/index';
4+
import '../I18n/index';
5+
6+
import { StoreTransactionFolderCard } from './StoreTransactionFolderCard';
7+
8+
customElements.define('foxy-store-transaction-folder-card', StoreTransactionFolderCard);
9+
10+
export { StoreTransactionFolderCard };
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { Resource } from '@foxy.io/sdk/core';
2+
import type { Rels } from '@foxy.io/sdk/backend';
3+
4+
export type Data = Resource<Rels.StoreTransactionFolder>;

src/elements/public/index.defined.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export { Spinner } from './Spinner/index';
9494
export { StoreCard } from './StoreCard/index';
9595
export { StoreForm } from './StoreForm/index';
9696
export { StoreShippingMethodForm } from './StoreShippingMethodForm/index';
97+
export { StoreTransactionFolderCard } from './StoreTransactionFolderCard/index';
9798
export { StoreTransactionFolderForm } from './StoreTransactionFolderForm/index';
9899
export { SubscriptionCard } from './SubscriptionCard/index';
99100
export { SubscriptionForm } from './SubscriptionForm/index';

src/elements/public/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export { Spinner } from './Spinner/Spinner';
9494
export { StoreCard } from './StoreCard/StoreCard';
9595
export { StoreForm } from './StoreForm/StoreForm';
9696
export { StoreShippingMethodForm } from './StoreShippingMethodForm/StoreShippingMethodForm';
97+
export { StoreTransactionFolderCard } from './StoreTransactionFolderCard/StoreTransactionFolderCard';
9798
export { StoreTransactionFolderForm } from './StoreTransactionFolderForm/StoreTransactionFolderForm';
9899
export { SubscriptionCard } from './SubscriptionCard/SubscriptionCard';
99100
export { SubscriptionForm } from './SubscriptionForm/SubscriptionForm';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"no_name": "New folder",
3+
"spinner": {
4+
"loading_busy": "Loading",
5+
"loading_empty": "No data",
6+
"loading_error": "Unknown error"
7+
}
8+
}

web-test-runner.groups.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,10 @@ export const groups = [
599599
name: 'foxy-store-shipping-method-form',
600600
files: './src/elements/public/StoreShippingMethodForm/**/*.test.ts',
601601
},
602+
{
603+
name: 'foxy-store-transaction-folder-card',
604+
files: './src/elements/public/StoreTransactionFolderCard/**/*.test.ts',
605+
},
602606
{
603607
name: 'foxy-store-transaction-folder-form',
604608
files: './src/elements/public/StoreTransactionFolderForm/**/*.test.ts',

0 commit comments

Comments
 (0)