mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
Feat/tag type colors backend (#9565)
Adds backend color support for tag types
This commit is contained in:
parent
0542fef5d8
commit
a65c8baf56
@ -4,6 +4,7 @@ export interface ITagType {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
|
color?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITagTypeStore extends Store<ITagType, string> {
|
export interface ITagTypeStore extends Store<ITagType, string> {
|
||||||
|
@ -6,13 +6,14 @@ import NotFoundError from '../../error/notfound-error';
|
|||||||
import type { ITagType, ITagTypeStore } from './tag-type-store-type';
|
import type { ITagType, ITagTypeStore } from './tag-type-store-type';
|
||||||
import type { Db } from '../../db/db';
|
import type { Db } from '../../db/db';
|
||||||
|
|
||||||
const COLUMNS = ['name', 'description', 'icon'];
|
const COLUMNS = ['name', 'description', 'icon', 'color'];
|
||||||
const TABLE = 'tag_types';
|
const TABLE = 'tag_types';
|
||||||
|
|
||||||
interface ITagTypeTable {
|
interface ITagTypeTable {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TagTypeStore implements ITagTypeStore {
|
export default class TagTypeStore implements ITagTypeStore {
|
||||||
@ -96,9 +97,16 @@ export default class TagTypeStore implements ITagTypeStore {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateTagType({ name, description, icon }: ITagType): Promise<void> {
|
async updateTagType({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
color,
|
||||||
|
}: ITagType): Promise<void> {
|
||||||
const stopTimer = this.timer('updateTagType');
|
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();
|
stopTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +117,7 @@ export default class TagTypeStore implements ITagTypeStore {
|
|||||||
name: row.name,
|
name: row.name,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
icon: row.icon,
|
icon: row.icon,
|
||||||
|
color: row.color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,11 +212,19 @@ class TagTypeController extends Controller {
|
|||||||
req: IAuthRequest<{ name: string }, unknown, UpdateTagTypeSchema>,
|
req: IAuthRequest<{ name: string }, unknown, UpdateTagTypeSchema>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { description, icon } = req.body;
|
const { description, icon, color } = req.body;
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
|
|
||||||
await this.tagTypeService.transactional((service) =>
|
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();
|
res.status(200).end();
|
||||||
}
|
}
|
||||||
|
@ -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 () => {
|
test('Invalid tag types gets rejected', async () => {
|
||||||
await app.request
|
await app.request
|
||||||
.post('/api/admin/tag-types')
|
.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 () => {
|
test('Can update a tag types description and icon', async () => {
|
||||||
await app.request.get('/api/admin/tag-types/simple').expect(200);
|
await app.request.get('/api/admin/tag-types/simple').expect(200);
|
||||||
await app.request
|
await app.request
|
||||||
@ -113,6 +146,32 @@ test('Can update a tag types description and icon', async () => {
|
|||||||
expect(res.body.tagType.icon).toBe('$');
|
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 () => {
|
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.get('/api/admin/tag-types/simple').expect(200);
|
||||||
await app.request
|
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 () => {
|
test('Validation of tag types allows numbers for description and icons because of coercion', async () => {
|
||||||
await app.request
|
await app.request
|
||||||
.post('/api/admin/tag-types/validate')
|
.post('/api/admin/tag-types/validate')
|
||||||
@ -216,3 +303,19 @@ test('Only required argument should be name', async () => {
|
|||||||
expect(res.body.name).toBe(name);
|
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);
|
||||||
|
});
|
||||||
|
@ -23,6 +23,13 @@ export const tagTypeSchema = {
|
|||||||
description: 'The icon of the tag type.',
|
description: 'The icon of the tag type.',
|
||||||
example: 'not-really-used',
|
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: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -9,11 +9,13 @@ test('tagTypesSchema', () => {
|
|||||||
name: 'simple',
|
name: 'simple',
|
||||||
description: 'Used to simplify filtering of features',
|
description: 'Used to simplify filtering of features',
|
||||||
icon: '#',
|
icon: '#',
|
||||||
|
color: '#FF0000',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'hashtag',
|
name: 'hashtag',
|
||||||
description: '',
|
description: '',
|
||||||
icon: null,
|
icon: null,
|
||||||
|
color: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,12 @@ export const updateTagTypeSchema = {
|
|||||||
description: 'The icon of the tag type.',
|
description: 'The icon of the tag type.',
|
||||||
example: 'not-really-used',
|
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: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -6,6 +6,10 @@ export const tagTypeSchema = Joi.object()
|
|||||||
name: customJoi.isUrlFriendly().min(2).max(50).required(),
|
name: customJoi.isUrlFriendly().min(2).max(50).required(),
|
||||||
description: Joi.string().allow(''),
|
description: Joi.string().allow(''),
|
||||||
icon: Joi.string().allow(null).allow(''),
|
icon: Joi.string().allow(null).allow(''),
|
||||||
|
color: Joi.string()
|
||||||
|
.pattern(/^#[0-9A-Fa-f]{6}$/)
|
||||||
|
.allow(null)
|
||||||
|
.allow(''),
|
||||||
})
|
})
|
||||||
.options({
|
.options({
|
||||||
allowUnknown: false,
|
allowUnknown: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user