Skip to content

Commit a65c8ba

Browse files
Feat/tag type colors backend (#9565)
Adds backend color support for tag types
1 parent 0542fef commit a65c8ba

8 files changed

+145
-5
lines changed

src/lib/features/tag-type/tag-type-store-type.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface ITagType {
44
name: string;
55
description?: string;
66
icon?: string | null;
7+
color?: string | null;
78
}
89

910
export interface ITagTypeStore extends Store<ITagType, string> {

src/lib/features/tag-type/tag-type-store.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import NotFoundError from '../../error/notfound-error';
66
import type { ITagType, ITagTypeStore } from './tag-type-store-type';
77
import type { Db } from '../../db/db';
88

9-
const COLUMNS = ['name', 'description', 'icon'];
9+
const COLUMNS = ['name', 'description', 'icon', 'color'];
1010
const TABLE = 'tag_types';
1111

1212
interface ITagTypeTable {
1313
name: string;
1414
description?: string;
1515
icon?: string;
16+
color?: string;
1617
}
1718

1819
export default class TagTypeStore implements ITagTypeStore {
@@ -96,9 +97,16 @@ export default class TagTypeStore implements ITagTypeStore {
9697
return [];
9798
}
9899

99-
async updateTagType({ name, description, icon }: ITagType): Promise<void> {
100+
async updateTagType({
101+
name,
102+
description,
103+
icon,
104+
color,
105+
}: ITagType): Promise<void> {
100106
const stopTimer = this.timer('updateTagType');
101-
await this.db(TABLE).where({ name }).update({ description, icon });
107+
await this.db(TABLE)
108+
.where({ name })
109+
.update({ description, icon, color });
102110
stopTimer();
103111
}
104112

@@ -109,6 +117,7 @@ export default class TagTypeStore implements ITagTypeStore {
109117
name: row.name,
110118
description: row.description,
111119
icon: row.icon,
120+
color: row.color,
112121
};
113122
}
114123
}

src/lib/features/tag-type/tag-type.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,19 @@ class TagTypeController extends Controller {
212212
req: IAuthRequest<{ name: string }, unknown, UpdateTagTypeSchema>,
213213
res: Response,
214214
): Promise<void> {
215-
const { description, icon } = req.body;
215+
const { description, icon, color } = req.body;
216216
const { name } = req.params;
217217

218218
await this.tagTypeService.transactional((service) =>
219-
service.updateTagType({ name, description, icon }, req.audit),
219+
service.updateTagType(
220+
{
221+
name,
222+
description,
223+
icon,
224+
color: color as string | null | undefined,
225+
},
226+
req.audit,
227+
),
220228
);
221229
res.status(200).end();
222230
}

src/lib/features/tag-type/tag-types.e2e.test.ts

+103
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,25 @@ test('Can create a new tag type', async () => {
7979
});
8080
});
8181

82+
test('Can create a new tag type with color', async () => {
83+
await app.request
84+
.post('/api/admin/tag-types')
85+
.send({
86+
name: 'colored-tag',
87+
description: 'A tag type with a color',
88+
icon: 'icon',
89+
color: '#FF5733',
90+
})
91+
.expect(201);
92+
return app.request
93+
.get('/api/admin/tag-types/colored-tag')
94+
.expect('Content-Type', /json/)
95+
.expect(200)
96+
.expect((res) => {
97+
expect(res.body.tagType.color).toBe('#FF5733');
98+
});
99+
});
100+
82101
test('Invalid tag types gets rejected', async () => {
83102
await app.request
84103
.post('/api/admin/tag-types')
@@ -96,6 +115,20 @@ test('Invalid tag types gets rejected', async () => {
96115
});
97116
});
98117

118+
test('Tag type with invalid color format gets rejected', async () => {
119+
const res = await app.request
120+
.post('/api/admin/tag-types')
121+
.send({
122+
name: 'invalid-color-tag',
123+
description: 'A tag with invalid color',
124+
color: 'not-a-color',
125+
})
126+
.set('Content-Type', 'application/json')
127+
.expect(400);
128+
129+
expect(res.body.details[0].message).toMatch(/color/);
130+
});
131+
99132
test('Can update a tag types description and icon', async () => {
100133
await app.request.get('/api/admin/tag-types/simple').expect(200);
101134
await app.request
@@ -113,6 +146,32 @@ test('Can update a tag types description and icon', async () => {
113146
expect(res.body.tagType.icon).toBe('$');
114147
});
115148
});
149+
150+
test('Can update a tag type color', async () => {
151+
await app.request
152+
.post('/api/admin/tag-types')
153+
.send({
154+
name: 'color-update-tag',
155+
description: 'A tag type to test color updates',
156+
color: '#FFFFFF',
157+
})
158+
.expect(201);
159+
160+
await app.request
161+
.put('/api/admin/tag-types/color-update-tag')
162+
.send({
163+
color: '#00FF00',
164+
})
165+
.expect(200);
166+
167+
const res = await app.request
168+
.get('/api/admin/tag-types/color-update-tag')
169+
.expect('Content-Type', /json/)
170+
.expect(200);
171+
172+
expect(res.body.tagType.color).toBe('#00FF00');
173+
});
174+
116175
test('Numbers are coerced to strings for icons and descriptions', async () => {
117176
await app.request.get('/api/admin/tag-types/simple').expect(200);
118177
await app.request
@@ -139,6 +198,34 @@ test('Validation of tag-types returns 200 for valid tag-types', async () => {
139198
});
140199
});
141200

201+
test('Validation of tag-types with valid color is successful', async () => {
202+
const res = await app.request
203+
.post('/api/admin/tag-types/validate')
204+
.send({
205+
name: 'color-validation',
206+
description: 'A tag type with a valid color',
207+
color: '#123ABC',
208+
})
209+
.set('Content-Type', 'application/json')
210+
.expect(200);
211+
212+
expect(res.body.valid).toBe(true);
213+
});
214+
215+
test('Validation of tag-types with invalid color format is unsuccessful', async () => {
216+
const res = await app.request
217+
.post('/api/admin/tag-types/validate')
218+
.send({
219+
name: 'invalid-color-validation',
220+
description: 'A tag type with an invalid color',
221+
color: 'not-a-color',
222+
})
223+
.set('Content-Type', 'application/json')
224+
.expect(400);
225+
226+
expect(res.body.details[0].message).toMatch(/color/);
227+
});
228+
142229
test('Validation of tag types allows numbers for description and icons because of coercion', async () => {
143230
await app.request
144231
.post('/api/admin/tag-types/validate')
@@ -216,3 +303,19 @@ test('Only required argument should be name', async () => {
216303
expect(res.body.name).toBe(name);
217304
});
218305
});
306+
307+
test('Creating a tag type with null color is allowed', async () => {
308+
const name = 'null-color-tag';
309+
const res = await app.request
310+
.post('/api/admin/tag-types')
311+
.send({
312+
name,
313+
description: 'A tag with null color',
314+
color: null,
315+
})
316+
.set('Content-Type', 'application/json')
317+
.expect(201);
318+
319+
expect(res.body.name).toBe(name);
320+
expect(res.body.color).toBe(null);
321+
});

src/lib/openapi/spec/tag-type-schema.ts

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ export const tagTypeSchema = {
2323
description: 'The icon of the tag type.',
2424
example: 'not-really-used',
2525
},
26+
color: {
27+
type: 'string',
28+
nullable: true,
29+
description: 'The hexadecimal color code for the tag type.',
30+
example: '#FFFFFF',
31+
pattern: '^#[0-9A-Fa-f]{6}$',
32+
},
2633
},
2734
components: {},
2835
} as const;

src/lib/openapi/spec/tag-types-schema.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ test('tagTypesSchema', () => {
99
name: 'simple',
1010
description: 'Used to simplify filtering of features',
1111
icon: '#',
12+
color: '#FF0000',
1213
},
1314
{
1415
name: 'hashtag',
1516
description: '',
1617
icon: null,
18+
color: null,
1719
},
1820
],
1921
};

src/lib/openapi/spec/update-tag-type-schema.ts

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export const updateTagTypeSchema = {
1515
description: 'The icon of the tag type.',
1616
example: 'not-really-used',
1717
},
18+
color: {
19+
type: 'string',
20+
description: 'The hexadecimal color code for the tag type.',
21+
example: '#FFFFFF',
22+
pattern: '^#[0-9A-Fa-f]{6}$',
23+
},
1824
},
1925
components: {},
2026
} as const;

src/lib/services/tag-type-schema.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export const tagTypeSchema = Joi.object()
66
name: customJoi.isUrlFriendly().min(2).max(50).required(),
77
description: Joi.string().allow(''),
88
icon: Joi.string().allow(null).allow(''),
9+
color: Joi.string()
10+
.pattern(/^#[0-9A-Fa-f]{6}$/)
11+
.allow(null)
12+
.allow(''),
913
})
1014
.options({
1115
allowUnknown: false,

0 commit comments

Comments
 (0)