1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-17 01:17:29 +02:00

Feat/tag type colors backend (#9565)

Adds backend color support for tag types
This commit is contained in:
Fredrik Strand Oseberg 2025-03-18 15:27:41 +01:00 committed by GitHub
parent 0542fef5d8
commit a65c8baf56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 145 additions and 5 deletions

View File

@ -4,6 +4,7 @@ export interface ITagType {
name: string;
description?: string;
icon?: string | null;
color?: string | null;
}
export interface ITagTypeStore extends Store<ITagType, string> {

View File

@ -6,13 +6,14 @@ import NotFoundError from '../../error/notfound-error';
import type { ITagType, ITagTypeStore } from './tag-type-store-type';
import type { Db } from '../../db/db';
const COLUMNS = ['name', 'description', 'icon'];
const COLUMNS = ['name', 'description', 'icon', 'color'];
const TABLE = 'tag_types';
interface ITagTypeTable {
name: string;
description?: string;
icon?: string;
color?: string;
}
export default class TagTypeStore implements ITagTypeStore {
@ -96,9 +97,16 @@ export default class TagTypeStore implements ITagTypeStore {
return [];
}
async updateTagType({ name, description, icon }: ITagType): Promise<void> {
async updateTagType({
name,
description,
icon,
color,
}: ITagType): Promise<void> {
const stopTimer = this.timer('updateTagType');
await this.db(TABLE).where({ name }).update({ description, icon });
await this.db(TABLE)
.where({ name })
.update({ description, icon, color });
stopTimer();
}
@ -109,6 +117,7 @@ export default class TagTypeStore implements ITagTypeStore {
name: row.name,
description: row.description,
icon: row.icon,
color: row.color,
};
}
}

View File

@ -212,11 +212,19 @@ class TagTypeController extends Controller {
req: IAuthRequest<{ name: string }, unknown, UpdateTagTypeSchema>,
res: Response,
): Promise<void> {
const { description, icon } = req.body;
const { description, icon, color } = req.body;
const { name } = req.params;
await this.tagTypeService.transactional((service) =>
service.updateTagType({ name, description, icon }, req.audit),
service.updateTagType(
{
name,
description,
icon,
color: color as string | null | undefined,
},
req.audit,
),
);
res.status(200).end();
}

View File

@ -79,6 +79,25 @@ test('Can create a new tag type', async () => {
});
});
test('Can create a new tag type with color', async () => {
await app.request
.post('/api/admin/tag-types')
.send({
name: 'colored-tag',
description: 'A tag type with a color',
icon: 'icon',
color: '#FF5733',
})
.expect(201);
return app.request
.get('/api/admin/tag-types/colored-tag')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.tagType.color).toBe('#FF5733');
});
});
test('Invalid tag types gets rejected', async () => {
await app.request
.post('/api/admin/tag-types')
@ -96,6 +115,20 @@ test('Invalid tag types gets rejected', async () => {
});
});
test('Tag type with invalid color format gets rejected', async () => {
const res = await app.request
.post('/api/admin/tag-types')
.send({
name: 'invalid-color-tag',
description: 'A tag with invalid color',
color: 'not-a-color',
})
.set('Content-Type', 'application/json')
.expect(400);
expect(res.body.details[0].message).toMatch(/color/);
});
test('Can update a tag types description and icon', async () => {
await app.request.get('/api/admin/tag-types/simple').expect(200);
await app.request
@ -113,6 +146,32 @@ test('Can update a tag types description and icon', async () => {
expect(res.body.tagType.icon).toBe('$');
});
});
test('Can update a tag type color', async () => {
await app.request
.post('/api/admin/tag-types')
.send({
name: 'color-update-tag',
description: 'A tag type to test color updates',
color: '#FFFFFF',
})
.expect(201);
await app.request
.put('/api/admin/tag-types/color-update-tag')
.send({
color: '#00FF00',
})
.expect(200);
const res = await app.request
.get('/api/admin/tag-types/color-update-tag')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body.tagType.color).toBe('#00FF00');
});
test('Numbers are coerced to strings for icons and descriptions', async () => {
await app.request.get('/api/admin/tag-types/simple').expect(200);
await app.request
@ -139,6 +198,34 @@ test('Validation of tag-types returns 200 for valid tag-types', async () => {
});
});
test('Validation of tag-types with valid color is successful', async () => {
const res = await app.request
.post('/api/admin/tag-types/validate')
.send({
name: 'color-validation',
description: 'A tag type with a valid color',
color: '#123ABC',
})
.set('Content-Type', 'application/json')
.expect(200);
expect(res.body.valid).toBe(true);
});
test('Validation of tag-types with invalid color format is unsuccessful', async () => {
const res = await app.request
.post('/api/admin/tag-types/validate')
.send({
name: 'invalid-color-validation',
description: 'A tag type with an invalid color',
color: 'not-a-color',
})
.set('Content-Type', 'application/json')
.expect(400);
expect(res.body.details[0].message).toMatch(/color/);
});
test('Validation of tag types allows numbers for description and icons because of coercion', async () => {
await app.request
.post('/api/admin/tag-types/validate')
@ -216,3 +303,19 @@ test('Only required argument should be name', async () => {
expect(res.body.name).toBe(name);
});
});
test('Creating a tag type with null color is allowed', async () => {
const name = 'null-color-tag';
const res = await app.request
.post('/api/admin/tag-types')
.send({
name,
description: 'A tag with null color',
color: null,
})
.set('Content-Type', 'application/json')
.expect(201);
expect(res.body.name).toBe(name);
expect(res.body.color).toBe(null);
});

View File

@ -23,6 +23,13 @@ export const tagTypeSchema = {
description: 'The icon of the tag type.',
example: 'not-really-used',
},
color: {
type: 'string',
nullable: true,
description: 'The hexadecimal color code for the tag type.',
example: '#FFFFFF',
pattern: '^#[0-9A-Fa-f]{6}$',
},
},
components: {},
} as const;

View File

@ -9,11 +9,13 @@ test('tagTypesSchema', () => {
name: 'simple',
description: 'Used to simplify filtering of features',
icon: '#',
color: '#FF0000',
},
{
name: 'hashtag',
description: '',
icon: null,
color: null,
},
],
};

View File

@ -15,6 +15,12 @@ export const updateTagTypeSchema = {
description: 'The icon of the tag type.',
example: 'not-really-used',
},
color: {
type: 'string',
description: 'The hexadecimal color code for the tag type.',
example: '#FFFFFF',
pattern: '^#[0-9A-Fa-f]{6}$',
},
},
components: {},
} as const;

View File

@ -6,6 +6,10 @@ export const tagTypeSchema = Joi.object()
name: customJoi.isUrlFriendly().min(2).max(50).required(),
description: Joi.string().allow(''),
icon: Joi.string().allow(null).allow(''),
color: Joi.string()
.pattern(/^#[0-9A-Fa-f]{6}$/)
.allow(null)
.allow(''),
})
.options({
allowUnknown: false,