Skip to content

Commit c3e94ef

Browse files
committed
feat(foxy-transaction): add support for transaction folders
1 parent ced0218 commit c3e94ef

File tree

8 files changed

+282
-28
lines changed

8 files changed

+282
-28
lines changed

src/elements/public/Transaction/Transaction.stories.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getMeta } from '../../../storygen/getMeta';
55
import { getStory } from '../../../storygen/getStory';
66

77
const summary: Summary = {
8-
href: 'https://demo.api/hapi/transactions/0?zoom=applied_taxes,discounts,shipments,gift_card_code_logs:gift_card,gift_card_code_logs:gift_card_code',
8+
href: 'https://demo.api/hapi/transactions/0?zoom=folder,applied_taxes,discounts,shipments,gift_card_code_logs:gift_card,gift_card_code_logs:gift_card_code',
99
parent: 'https://demo.api/hapi/transactions',
1010
nucleon: true,
1111
localName: 'foxy-transaction',
@@ -22,7 +22,7 @@ export const Error = getStory(summary);
2222
export const Busy = getStory(summary);
2323

2424
Readonly.args.href =
25-
'https://demo.api/hapi/transactions/1?zoom=applied_taxes,discounts,shipments,applied_gift_card_codes:gift_card';
25+
'https://demo.api/hapi/transactions/1?zoom=folder,applied_taxes,discounts,shipments,gift_card_code_logs:gift_card,gift_card_code_logs:gift_card_code';
2626

2727
Empty.args.href = '';
2828
Error.args.href = 'https://demo.api/virtual/empty?status=404';

src/elements/public/Transaction/Transaction.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,20 @@ describe('Transaction', () => {
624624
expect(element.headerSubtitleBadges).to.not.deep.include({ key: 'archived' });
625625
});
626626

627+
it('renders folder badge in subtitle if assigned', async () => {
628+
const router = createRouter();
629+
const element = await fixture<Transaction>(html`
630+
<foxy-transaction
631+
href="https://demo.api/hapi/transactions/0?zoom=folder,applied_taxes,discounts,shipments,applied_gift_card_codes:gift_card"
632+
@fetch=${(evt: FetchEvent) => router.handleEvent(evt)}
633+
>
634+
</foxy-transaction>
635+
`);
636+
637+
await waitUntil(() => element.in({ idle: 'snapshot' }));
638+
expect(element.headerSubtitleBadges[0]).to.have.property('text', 'Pending');
639+
});
640+
627641
it('uses display_id as ID copied by Copy ID button', async () => {
628642
const router = createRouter();
629643
const element = await fixture<Transaction>(html`

src/elements/public/Transaction/Transaction.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { PropertyDeclarations, TemplateResult } from 'lit-element';
22
import type { NucleonElement } from '../NucleonElement/NucleonElement';
33
import type { Resource } from '@foxy.io/sdk/core';
4+
import type { Badge } from '../../internal/InternalForm/types';
45
import type { Data } from './types';
56
import type { Rels } from '@foxy.io/sdk/backend';
67

@@ -9,7 +10,7 @@ import { TranslatableMixin } from '../../../mixins/translatable';
910
import { ResponsiveMixin } from '../../../mixins/responsive';
1011
import { InternalForm } from '../../internal/InternalForm/InternalForm';
1112
import { ifDefined } from 'lit-html/directives/if-defined';
12-
import { html } from 'lit-element';
13+
import { html, svg } from 'lit-element';
1314

1415
const NS = 'transaction';
1516
const Base = ResponsiveMixin(TranslatableMixin(InternalForm, NS));
@@ -189,10 +190,36 @@ export class Transaction extends Base<Data> {
189190
};
190191
}
191192

192-
get headerSubtitleBadges(): { key: string }[] {
193+
get headerSubtitleBadges(): Badge[] {
193194
const badges = super.headerSubtitleBadges;
194-
if (this.data?.is_test) badges.push({ key: 'test' });
195+
const folder = this.data?._embedded?.['fx:folder'];
196+
197+
if (folder?.name) {
198+
const folderColor = folder.color ?? '';
199+
const colors: Record<string, string> = {
200+
'red': 'bg-folder-red text-white',
201+
'red_pale': 'bg-folder-red-pale text-black',
202+
'green': 'bg-folder-green text-white',
203+
'green_pale': 'bg-folder-green-pale text-black',
204+
'blue': 'bg-folder-blue text-white',
205+
'blue_pale': 'bg-folder-blue-pale text-black',
206+
'orange': 'bg-folder-orange text-white',
207+
'orange_pale': 'bg-folder-orange-pale text-black',
208+
'violet': 'bg-folder-violet text-white',
209+
'violet_pale': 'bg-folder-violet-pale text-black',
210+
'': 'bg-contrast-5 text-body',
211+
};
212+
213+
badges.push({
214+
class: colors[folderColor in colors ? folderColor : ''],
215+
icon: svg`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" style="width: 1em; height: 1em;"><path d="M3.75 3A1.75 1.75 0 0 0 2 4.75v3.26a3.235 3.235 0 0 1 1.75-.51h12.5c.644 0 1.245.188 1.75.51V6.75A1.75 1.75 0 0 0 16.25 5h-4.836a.25.25 0 0 1-.177-.073L9.823 3.513A1.75 1.75 0 0 0 8.586 3H3.75ZM3.75 9A1.75 1.75 0 0 0 2 10.75v4.5c0 .966.784 1.75 1.75 1.75h12.5A1.75 1.75 0 0 0 18 15.25v-4.5A1.75 1.75 0 0 0 16.25 9H3.75Z" /></svg>`,
216+
text: folder.name,
217+
});
218+
}
219+
195220
if (this.data?.hide_transaction) badges.push({ key: 'archived' });
221+
if (this.data?.is_test) badges.push({ key: 'test' });
222+
196223
return badges;
197224
}
198225

@@ -201,8 +228,10 @@ export class Transaction extends Base<Data> {
201228
}
202229

203230
renderHeaderActions(): TemplateResult {
231+
const foldersHref = this.__storeLoader?.data?._links['fx:transaction_folders'].href;
232+
204233
return html`
205-
<foxy-internal-transaction-actions-control infer="actions">
234+
<foxy-internal-transaction-actions-control folders=${ifDefined(foldersHref)} infer="actions">
206235
</foxy-internal-transaction-actions-control>
207236
`;
208237
}

src/elements/public/Transaction/internal/InternalTransactionActionsControl/InternalTransactionActionsControl.test.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { NucleonElement } from '../../../NucleonElement/NucleonElement';
2+
13
import '../../../NucleonElement/index';
24

35
import { expect, fixture, waitUntil } from '@open-wc/testing';
@@ -7,12 +9,23 @@ import { Transaction } from '../../Transaction';
79
import { html } from 'lit-html';
810
import { createRouter } from '../../../../../server/index';
911
import { FetchEvent } from '../../../NucleonElement/FetchEvent';
10-
import { stub } from 'sinon';
12+
import { spy, stub } from 'sinon';
1113

1214
import unset from 'lodash-es/unset';
1315
import set from 'lodash-es/set';
1416
import { BooleanSelector } from '@foxy.io/sdk/core';
1517

18+
async function waitForIdle(element: InternalTransactionActionsControl) {
19+
await waitUntil(
20+
() => {
21+
const loaders = element.renderRoot.querySelectorAll<NucleonElement<any>>('foxy-nucleon');
22+
return [...loaders].every(loader => loader.in('idle'));
23+
},
24+
'',
25+
{ timeout: 5000 }
26+
);
27+
}
28+
1629
describe('Transaction', () => {
1730
describe('InternalTransactionActionsControl', () => {
1831
it('imports and defines foxy-internal-control', () => {
@@ -353,5 +366,119 @@ describe('Transaction', () => {
353366
await control.requestUpdate();
354367
expect(button).to.have.attribute('disabled');
355368
});
369+
370+
it('renders folder selector', async () => {
371+
const router = createRouter();
372+
const wrapper = await fixture<Transaction>(html`
373+
<foxy-nucleon
374+
href="https://demo.api/hapi/transactions/0"
375+
@fetch=${(evt: FetchEvent) => router.handleEvent(evt)}
376+
>
377+
<foxy-internal-transaction-actions-control
378+
infer="actions"
379+
folders="https://demo.api/hapi/transaction_folders"
380+
>
381+
</foxy-internal-transaction-actions-control>
382+
</foxy-nucleon>
383+
`);
384+
385+
await waitUntil(() => wrapper.in({ idle: 'snapshot' }));
386+
const control = wrapper.firstElementChild as InternalTransactionActionsControl;
387+
await waitForIdle(control);
388+
389+
const labelText = control.renderRoot.querySelector(
390+
'foxy-i18n[infer="folder"][key="caption"]'
391+
);
392+
393+
const label = labelText?.closest('label');
394+
const select = label?.querySelector('select');
395+
396+
expect(labelText).to.exist;
397+
expect(label).to.exist;
398+
expect(select).to.exist;
399+
400+
expect(select?.options).to.have.lengthOf(3);
401+
expect(select?.options[0].value).to.equal('');
402+
expect(select?.options[0]).to.include.text('folder.option_none');
403+
expect(select?.options[0]).to.not.have.attribute('selected');
404+
405+
expect(select?.options[1].value).to.equal('https://demo.api/hapi/transaction_folders/0');
406+
expect(select?.options[1]).to.include.text('Pending');
407+
expect(select?.options[1]).to.have.attribute('selected');
408+
409+
expect(select?.options[2].value).to.equal('https://demo.api/hapi/transaction_folders/1');
410+
expect(select?.options[2]).to.include.text('Shipped');
411+
expect(select?.options[2]).to.not.have.attribute('selected');
412+
413+
const editMethod = spy(wrapper, 'edit');
414+
const submitMethod = spy(wrapper, 'submit');
415+
416+
select!.options[2].selected = true;
417+
select!.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
418+
expect(editMethod).to.have.been.calledOnceWith({ folder_uri: select!.options[2].value });
419+
expect(submitMethod).to.have.been.calledOnceWith(false);
420+
});
421+
422+
it('disables folder selector when control is disabled', async () => {
423+
const router = createRouter();
424+
const wrapper = await fixture<Transaction>(html`
425+
<foxy-nucleon
426+
href="https://demo.api/hapi/transactions/0"
427+
@fetch=${(evt: FetchEvent) => router.handleEvent(evt)}
428+
>
429+
<foxy-internal-transaction-actions-control
430+
infer="actions"
431+
folders="https://demo.api/hapi/transaction_folders"
432+
>
433+
</foxy-internal-transaction-actions-control>
434+
</foxy-nucleon>
435+
`);
436+
437+
await waitUntil(() => wrapper.in({ idle: 'snapshot' }));
438+
const control = wrapper.firstElementChild as InternalTransactionActionsControl;
439+
440+
await waitForIdle(control);
441+
const select = control.renderRoot
442+
.querySelector('foxy-i18n[infer="folder"][key="caption"]')
443+
?.closest('label')
444+
?.querySelector('select');
445+
446+
expect(select).to.not.have.attribute('disabled');
447+
448+
control.disabled = true;
449+
await control.requestUpdate();
450+
expect(select).to.have.attribute('disabled');
451+
});
452+
453+
it('makes folder selector readonly when control is readonly', async () => {
454+
const router = createRouter();
455+
const wrapper = await fixture<Transaction>(html`
456+
<foxy-nucleon
457+
href="https://demo.api/hapi/transactions/0"
458+
@fetch=${(evt: FetchEvent) => router.handleEvent(evt)}
459+
>
460+
<foxy-internal-transaction-actions-control
461+
infer="actions"
462+
folders="https://demo.api/hapi/transaction_folders"
463+
>
464+
</foxy-internal-transaction-actions-control>
465+
</foxy-nucleon>
466+
`);
467+
468+
await waitUntil(() => wrapper.in({ idle: 'snapshot' }));
469+
const control = wrapper.firstElementChild as InternalTransactionActionsControl;
470+
471+
await waitForIdle(control);
472+
const select = control.renderRoot
473+
.querySelector('foxy-i18n[infer="folder"][key="caption"]')
474+
?.closest('label')
475+
?.querySelector('select');
476+
477+
expect(select).to.not.have.attribute('readonly');
478+
479+
control.readonly = true;
480+
await control.requestUpdate();
481+
expect(select).to.have.attribute('readonly');
482+
});
356483
});
357484
});

src/elements/public/Transaction/internal/InternalTransactionActionsControl/InternalTransactionActionsControl.ts

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1-
import { html, TemplateResult } from 'lit-html';
2-
import { ifDefined } from 'lit-html/directives/if-defined';
1+
import type { PropertyDeclarations, TemplateResult } from 'lit-element';
2+
import type { NucleonElement } from '../../../NucleonElement/NucleonElement';
3+
import type { Transaction } from '../../Transaction';
4+
import type { Resource } from '@foxy.io/sdk/core';
5+
36
import { InternalControl } from '../../../../internal/InternalControl/InternalControl';
4-
import { Transaction } from '../../Transaction';
7+
import { ifDefined } from 'lit-html/directives/if-defined';
8+
import { html, svg } from 'lit-html';
9+
import { Rels } from '@foxy.io/sdk/backend';
510

611
export class InternalTransactionActionsControl extends InternalControl {
7-
renderControl(): TemplateResult {
8-
const host = this.nucleon as Transaction | null;
12+
static get properties(): PropertyDeclarations {
13+
return {
14+
...super.properties,
15+
folders: {},
16+
};
17+
}
18+
19+
folders: string | null = null;
920

21+
renderControl(): TemplateResult {
1022
return html`
1123
<div class="flex flex-wrap gap-x-m gap-y-xs">
1224
${this.nucleon?.data?._links['fx:capture'] ? this.__renderCaptureAction() : ''}
@@ -15,21 +27,8 @@ export class InternalTransactionActionsControl extends InternalControl {
1527
${this.nucleon?.data?._links['fx:send_emails'] ? this.__renderSendEmailsAction() : ''}
1628
${this.nucleon?.data?._links['fx:subscription'] ? this.__renderSubscriptionAction() : ''}
1729
${this.nucleon?.data?._links['fx:receipt'] ? this.__renderReceiptAction() : ''}
18-
19-
<vaadin-button
20-
theme="tertiary-inline"
21-
?disabled=${this.disabledSelector.matches('archive', true)}
22-
@click=${() => {
23-
host?.edit({ hide_transaction: !host?.form.hide_transaction });
24-
host?.submit();
25-
}}
26-
>
27-
<foxy-i18n
28-
infer="archive"
29-
key="caption_${host?.form.hide_transaction ? 'unarchive' : 'archive'}"
30-
>
31-
</foxy-i18n>
32-
</vaadin-button>
30+
${this.__renderArchiveAction()}
31+
${this.folders ? this.__renderFolderSelector(this.folders) : ''}
3332
</div>
3433
`;
3534
}
@@ -110,4 +109,82 @@ export class InternalTransactionActionsControl extends InternalControl {
110109
</a>
111110
`;
112111
}
112+
113+
private __renderArchiveAction() {
114+
const host = this.nucleon as Transaction | null;
115+
116+
return html`
117+
<vaadin-button
118+
theme="tertiary-inline"
119+
?disabled=${this.disabledSelector.matches('archive', true)}
120+
@click=${() => {
121+
host?.edit({ hide_transaction: !host?.form.hide_transaction });
122+
host?.submit();
123+
}}
124+
>
125+
<foxy-i18n
126+
infer="archive"
127+
key="caption_${host?.form.hide_transaction ? 'unarchive' : 'archive'}"
128+
>
129+
</foxy-i18n>
130+
</vaadin-button>
131+
`;
132+
}
133+
134+
private __renderFolderSelector(foldersHref: string) {
135+
type FoldersLoader = NucleonElement<Resource<Rels.StoreTransactionFolders>>;
136+
137+
const foldersLoader = this.renderRoot.querySelector<FoldersLoader>('#foldersLoader');
138+
const folders = Array.from(foldersLoader?.data?._embedded?.['fx:transaction_folders'] ?? []);
139+
const host = this.nucleon as Transaction | null;
140+
141+
return html`
142+
<foxy-nucleon
143+
class="hidden"
144+
infer=""
145+
href=${foldersHref}
146+
id="foldersLoader"
147+
@update=${() => this.requestUpdate()}
148+
>
149+
</foxy-nucleon>
150+
151+
<label
152+
class="group relative rounded focus-within-ring-2 focus-within-ring-primary-50"
153+
?hidden=${folders.length === 0}
154+
>
155+
<span
156+
class="inline-flex items-center gap-xs relative transition-opacity group-hover-opacity-80"
157+
>
158+
<foxy-i18n class="font-medium text-primary" infer="folder" key="caption"></foxy-i18n>
159+
${svg`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="text-primary transform scale-125" style="width: 1em; height: 1em"><path fill-rule="evenodd" d="M10.53 3.47a.75.75 0 0 0-1.06 0L6.22 6.72a.75.75 0 0 0 1.06 1.06L10 5.06l2.72 2.72a.75.75 0 1 0 1.06-1.06l-3.25-3.25Zm-4.31 9.81 3.25 3.25a.75.75 0 0 0 1.06 0l3.25-3.25a.75.75 0 1 0-1.06-1.06L10 14.94l-2.72-2.72a.75.75 0 0 0-1.06 1.06Z" clip-rule="evenodd" /></svg>`}
160+
</span>
161+
162+
<select
163+
class="absolute inset-0 opacity-0 cursor-pointer"
164+
?disabled=${this.disabled}
165+
?readonly=${this.readonly}
166+
@change=${(evt: Event) => {
167+
host?.edit({ folder_uri: (evt.currentTarget as HTMLSelectElement).value });
168+
host?.submit(false);
169+
}}
170+
>
171+
<option value="" ?selected=${!host?.form.folder_uri} ?disabled=${!host?.form.folder_uri}>
172+
${this.t('folder.option_none')}
173+
</option>
174+
175+
${folders
176+
.sort((a, b) => a.sort_order - b.sort_order)
177+
.map(folder => {
178+
const folderHref = folder._links.self.href;
179+
const isSelected = host?.form.folder_uri === folderHref;
180+
return html`
181+
<option value=${folderHref} ?selected=${isSelected} ?disabled=${isSelected}>
182+
${folder.name}
183+
</option>
184+
`;
185+
})}
186+
</select>
187+
</label>
188+
`;
189+
}
113190
}

0 commit comments

Comments
 (0)