Fix maintenance mode banner button color, timezone and date picker UX (#19255)

## Summary
- Set `accent="blue"` on InformationBanner action button so it renders
blue instead of default gray
- Add Banner storybook stories for all color × variant combinations
(BluePrimary, BlueSecondary, DangerPrimary, DangerSecondary)
- Use `useUserTimezone()` in `SettingsDatePickerInput` instead of
browser timezone (`Temporal.Now.timeZoneId()`) so dates respect the
admin's profile timezone preference
- Separate `onChange` from `onClose` in `SettingsDatePickerInput` so
changing the hour no longer forces the date picker to close
This commit is contained in:
Charles Bochet
2026-04-02 14:58:03 +02:00
committed by GitHub
parent 8581c11d56
commit 45c1c60acc
6 changed files with 244 additions and 102 deletions
@@ -22,7 +22,7 @@ describe('KeyValuePairService', () => {
service = new KeyValuePairService(keyValuePairRepository);
});
it('should insert a global null/null key when missing', async () => {
it('should upsert a global null/null key', async () => {
await service.set({
userId: null,
workspaceId: null,
@@ -31,45 +31,24 @@ describe('KeyValuePairService', () => {
type: KeyValuePairType.CONFIG_VARIABLE,
});
expect(keyValuePairRepository.findOne).toHaveBeenCalledWith({
where: {
userId: expect.any(Object),
workspaceId: expect.any(Object),
expect(keyValuePairRepository.upsert).toHaveBeenCalledWith(
{
userId: null,
workspaceId: null,
key: 'MAINTENANCE_MODE',
value: { startAt: '2026-04-02T10:00:00.000Z' },
type: KeyValuePairType.CONFIG_VARIABLE,
},
});
expect(keyValuePairRepository.insert).toHaveBeenCalledWith({
userId: null,
workspaceId: null,
key: 'MAINTENANCE_MODE',
value: { startAt: '2026-04-02T10:00:00.000Z' },
type: KeyValuePairType.CONFIG_VARIABLE,
});
expect(keyValuePairRepository.upsert).not.toHaveBeenCalled();
});
it('should update a global null/null key when present', async () => {
keyValuePairRepository.findOne.mockResolvedValue({
id: 'existing-id',
} as KeyValuePairEntity);
await service.set({
userId: null,
workspaceId: null,
key: 'MAINTENANCE_MODE',
value: { startAt: '2026-04-02T10:00:00.000Z' },
type: KeyValuePairType.CONFIG_VARIABLE,
});
expect(keyValuePairRepository.update).toHaveBeenCalledWith('existing-id', {
value: { startAt: '2026-04-02T10:00:00.000Z' },
});
{
conflictPaths: ['key'],
indexPredicate: '"userId" IS NULL AND "workspaceId" IS NULL',
},
);
expect(keyValuePairRepository.findOne).not.toHaveBeenCalled();
expect(keyValuePairRepository.insert).not.toHaveBeenCalled();
expect(keyValuePairRepository.upsert).not.toHaveBeenCalled();
});
it('should keep the existing workspace-null index behavior', async () => {
it('should upsert with userId-null index when workspaceId is null', async () => {
await service.set({
userId: 'user-id',
workspaceId: null,
@@ -87,8 +66,56 @@ describe('KeyValuePairService', () => {
type: KeyValuePairType.USER_VARIABLE,
},
{
conflictPaths: ['userId', 'workspaceId', 'key'],
indexPredicate: '"workspaceId" is NULL',
conflictPaths: ['key', 'userId'],
indexPredicate: '"workspaceId" IS NULL',
},
);
});
it('should upsert with workspaceId-null index when userId is null', async () => {
await service.set({
userId: null,
workspaceId: 'workspace-id',
key: 'WORKSPACE_SETTING',
value: 'test',
type: KeyValuePairType.CONFIG_VARIABLE,
});
expect(keyValuePairRepository.upsert).toHaveBeenCalledWith(
{
userId: null,
workspaceId: 'workspace-id',
key: 'WORKSPACE_SETTING',
value: 'test',
type: KeyValuePairType.CONFIG_VARIABLE,
},
{
conflictPaths: ['key', 'workspaceId'],
indexPredicate: '"userId" IS NULL',
},
);
});
it('should upsert with full conflict paths when both ids are present', async () => {
await service.set({
userId: 'user-id',
workspaceId: 'workspace-id',
key: 'USER_WORKSPACE_SETTING',
value: 42,
type: KeyValuePairType.USER_VARIABLE,
});
expect(keyValuePairRepository.upsert).toHaveBeenCalledWith(
{
userId: 'user-id',
workspaceId: 'workspace-id',
key: 'USER_WORKSPACE_SETTING',
value: 42,
type: KeyValuePairType.USER_VARIABLE,
},
{
conflictPaths: ['key', 'userId', 'workspaceId'],
indexPredicate: undefined,
},
);
});
@@ -82,43 +82,21 @@ export class KeyValuePairService<
type,
};
const conflictPaths: string[] = ['key'];
let indexPredicate: string | undefined;
if (hasNullUserAndWorkspace) {
const existingKeyValuePair = await keyValuePairRepository.findOne({
where: {
userId: IsNull(),
workspaceId: IsNull(),
key,
type,
},
});
if (existingKeyValuePair) {
await keyValuePairRepository.update(existingKeyValuePair.id, {
value,
});
return;
}
await keyValuePairRepository.insert(upsertData);
return;
indexPredicate = '"userId" IS NULL AND "workspaceId" IS NULL';
} else if (normalizedUserId === null) {
conflictPaths.push('workspaceId');
indexPredicate = '"userId" IS NULL';
} else if (normalizedWorkspaceId === null) {
conflictPaths.push('userId');
indexPredicate = '"workspaceId" IS NULL';
} else {
conflictPaths.push('userId', 'workspaceId');
}
const conflictPaths = Object.keys(upsertData).filter(
(conflictPath) =>
['userId', 'workspaceId', 'key'].includes(conflictPath) &&
// @ts-expect-error legacy noImplicitAny
upsertData[conflictPath] !== undefined,
);
const indexPredicate =
normalizedUserId === null
? '"userId" is NULL'
: normalizedWorkspaceId === null
? '"workspaceId" is NULL'
: undefined;
await keyValuePairRepository.upsert(upsertData, {
conflictPaths,
indexPredicate,