Skip to content

Commit 1a69fa8

Browse files
authored
Add UI component builder (#71)
* Add ui component builder * fix import
1 parent 96908ff commit 1a69fa8

14 files changed

+375
-0
lines changed

.changeset/plenty-crabs-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@team-plain/typescript-sdk': minor
3+
---
4+
5+
Add `uiComponent` builder so you can more easily create ui components for use with the API.

src/examples/createThread.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { PlainClient } from '../client';
2+
import { ComponentTextColor, ComponentTextSize } from '../graphql/types';
3+
import { uiComponent } from '../ui-components';
4+
5+
export async function createThreadA() {
6+
const client = new PlainClient({ apiKey: 'XXX' });
7+
8+
const res = await client.createThread({
9+
title: 'Contact form',
10+
customerIdentifier: {
11+
customerId: 'c_123',
12+
},
13+
components: [
14+
{
15+
componentText: {
16+
text: 'hello world',
17+
},
18+
},
19+
{
20+
componentText: {
21+
text: 'hello world',
22+
textColor: ComponentTextColor.Error,
23+
},
24+
},
25+
{
26+
componentText: {
27+
text: 'hello world',
28+
textSize: ComponentTextSize.M,
29+
},
30+
},
31+
],
32+
});
33+
34+
if (res.error) {
35+
console.error(res.error);
36+
} else {
37+
console.log(`Thread created with id=${res.data.thread.id}`);
38+
}
39+
}
40+
41+
export async function createThreadB() {
42+
const client = new PlainClient({ apiKey: 'XXX' });
43+
44+
const res = await client.createThread({
45+
title: 'Contact form',
46+
customerIdentifier: {
47+
customerId: 'c_123',
48+
},
49+
components: [
50+
uiComponent.text({ text: 'hello world' }),
51+
uiComponent.text({ text: 'hello world', color: 'ERROR' }),
52+
uiComponent.text({ text: 'hello world', size: 'M' }),
53+
],
54+
});
55+
56+
if (res.error) {
57+
console.error(res.error);
58+
} else {
59+
console.log(`Thread created with id=${res.data.thread.id}`);
60+
}
61+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
export { PlainClient } from './client';
44

5+
export { uiComponent } from './ui-components';
6+
57
export {
68
// Enums
79
AttachmentType,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ComponentBadgeColor, type ComponentInput } from '../graphql/types';
2+
3+
type Color = `${ComponentBadgeColor}`;
4+
5+
function colorToEnum(color?: Color): ComponentBadgeColor | null {
6+
if (!color) {
7+
return null;
8+
}
9+
const map: Record<Color, ComponentBadgeColor> = {
10+
BLUE: ComponentBadgeColor.Blue,
11+
GREEN: ComponentBadgeColor.Green,
12+
GREY: ComponentBadgeColor.Grey,
13+
RED: ComponentBadgeColor.Red,
14+
YELLOW: ComponentBadgeColor.Yellow,
15+
};
16+
return map[color];
17+
}
18+
19+
/**
20+
* Returns a ComponentInput which can be used with the API.
21+
*/
22+
export function badgeComponent(args: { label: string; color?: Color }): ComponentInput {
23+
return {
24+
componentBadge: {
25+
badgeLabel: args.label,
26+
badgeColor: colorToEnum(args.color),
27+
},
28+
};
29+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type ComponentInput } from '../graphql/types';
2+
3+
/**
4+
* Returns a ComponentInput which can be used with the API.
5+
*/
6+
export function containerComponent(args: { content: ComponentInput[] }): ComponentInput {
7+
return {
8+
componentContainer: {
9+
containerContent: args.content,
10+
},
11+
};
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { type ComponentInput } from '../graphql/types';
2+
3+
/**
4+
* Returns a ComponentInput which can be used with the API.
5+
*/
6+
export function copyButtonComponent(args: { value: string; tooltip?: string }): ComponentInput {
7+
return {
8+
componentCopyButton: {
9+
copyButtonValue: args.value,
10+
copyButtonTooltipLabel: args.tooltip,
11+
},
12+
};
13+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ComponentDividerSpacingSize, type ComponentInput } from '../graphql/types';
2+
3+
type Size = `${ComponentDividerSpacingSize}`;
4+
5+
function spacingSizeToEnum(size?: Size): ComponentDividerSpacingSize | null {
6+
if (!size) {
7+
return null;
8+
}
9+
const map: Record<Size, ComponentDividerSpacingSize> = {
10+
XL: ComponentDividerSpacingSize.Xl,
11+
L: ComponentDividerSpacingSize.L,
12+
M: ComponentDividerSpacingSize.M,
13+
S: ComponentDividerSpacingSize.S,
14+
XS: ComponentDividerSpacingSize.Xs,
15+
};
16+
return map[size];
17+
}
18+
19+
/**
20+
* Returns a ComponentInput which can be used with the API.
21+
*/
22+
export function dividerComponent(args: { spacingSize?: Size }): ComponentInput {
23+
return {
24+
componentDivider: {
25+
dividerSpacingSize: spacingSizeToEnum(args.spacingSize),
26+
},
27+
};
28+
}

src/ui-components/index.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, test } from 'vitest';
2+
3+
import { ComponentBadgeColor, ComponentTextColor, ComponentTextSize } from '../graphql/types';
4+
import { uiComponent } from '.';
5+
6+
describe('ui components builder', () => {
7+
test('basic example', () => {
8+
expect([
9+
uiComponent.text({ text: 'hello world' }),
10+
uiComponent.text({ text: 'hello world', color: 'MUTED', size: 'S' }),
11+
]).toEqual([
12+
{
13+
componentText: {
14+
text: 'hello world',
15+
textColor: null,
16+
textSize: null,
17+
},
18+
},
19+
{
20+
componentText: {
21+
text: 'hello world',
22+
textColor: ComponentTextColor.Muted,
23+
textSize: ComponentTextSize.S,
24+
},
25+
},
26+
]);
27+
});
28+
29+
test('container component', () => {
30+
expect([
31+
uiComponent.container({
32+
content: [
33+
uiComponent.text({ text: 'hello world' }),
34+
uiComponent.badge({ label: 'success', color: 'GREEN' }),
35+
],
36+
}),
37+
]).toEqual([
38+
{
39+
componentContainer: {
40+
containerContent: [
41+
{
42+
componentText: {
43+
text: 'hello world',
44+
textColor: null,
45+
textSize: null,
46+
},
47+
},
48+
{
49+
componentBadge: {
50+
badgeLabel: 'success',
51+
badgeColor: ComponentBadgeColor.Green,
52+
},
53+
},
54+
],
55+
},
56+
},
57+
]);
58+
});
59+
});

src/ui-components/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { badgeComponent } from './badgeComponent';
2+
import { containerComponent } from './containerComponent';
3+
import { copyButtonComponent } from './copyButtonComponent';
4+
import { dividerComponent } from './dividerComponent';
5+
import { linkButtonComponent } from './linkButtonComponent';
6+
import { plainTextComponent } from './plainTextComponent';
7+
import { rowComponent } from './rowComponent';
8+
import { spacerComponent } from './spacerComponent';
9+
import { textComponent } from './textComponent';
10+
11+
export const uiComponent = {
12+
badge: badgeComponent,
13+
container: containerComponent,
14+
copyButton: copyButtonComponent,
15+
divider: dividerComponent,
16+
linkButton: linkButtonComponent,
17+
plainText: plainTextComponent,
18+
row: rowComponent,
19+
spacer: spacerComponent,
20+
text: textComponent,
21+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { type ComponentInput } from '../graphql/types';
2+
3+
/**
4+
* Returns a ComponentInput which can be used with the API.
5+
*/
6+
export function linkButtonComponent(args: { label: string; url: string }): ComponentInput {
7+
return {
8+
componentLinkButton: {
9+
linkButtonLabel: args.label,
10+
linkButtonUrl: args.url,
11+
},
12+
};
13+
}

0 commit comments

Comments
 (0)