Compare commits

..
Author SHA1 Message Date
Philipinho 64e47b5679 Merge branch 'main' into fix/package-updates 2026-08-19 19:56:49 +01:00
Philipinho 1ad94c03dc chore: package updates 2026-08-19 19:52:22 +01:00
10 changed files with 46 additions and 151 deletions
@@ -9,7 +9,6 @@ import {
Text,
UnstyledButton,
Button,
MultiSelect,
} from "@mantine/core";
import { IconPlus, IconTrash } from "@tabler/icons-react";
import {
@@ -53,9 +52,6 @@ const NO_VALUE_OPERATORS: FilterOperator[] = ["isEmpty", "isNotEmpty"];
// stored value so a stale shape isn't sent to the engine.
function valueClass(op: FilterOperator, inputKind: string): string {
if (NO_VALUE_OPERATORS.includes(op)) return "none";
if (inputKind === "choices") {
return op === "any" || op === "none" ? "choicesMulti" : "choicesSingle";
}
if (inputKind === "person") {
return op === "any" || op === "none" ? "personMulti" : "personSingle";
}
@@ -74,10 +70,6 @@ function getOperatorsForType(type: string): FilterOperator[] {
DEFAULT_FILTER_OPERATORS) as FilterOperator[];
}
function isMultiChoice(op: FilterCondition["op"]): boolean {
return op === "any" || op === "none";
}
function FilterValueInput({
condition,
property,
@@ -129,32 +121,6 @@ function FilterValueInput({
const typeOptions = property.typeOptions as SelectTypeOptions | undefined;
const choices = typeOptions?.choices ?? [];
const choiceOptions = choices.map((c) => ({ value: c.id, label: c.name }));
if (isMultiChoice(condition.op)) {
const { value } = condition;
const selected = (
Array.isArray(value) ? value : value ? [value] : []
).filter((id) => choices.some((c) => c.id === id));
return (
<MultiSelect
size="xs"
data={choiceOptions}
comboboxProps={{ withinPortal: false }}
value={selected}
onChange={(values) => onChange(values)}
w={160}
styles={{
pillsList: {
maxHeight: 70,
overflowY: "auto",
},
}}
maxDropdownHeight={220}
/>
);
}
return (
<Select
size="xs"
@@ -233,18 +199,11 @@ export function ViewFilterConfigPopover({
label: p.name,
}));
const [unSaved, setUnSaved] = useState(false)
const [draft, setDraft] = useState<FilterCondition | null>(null);
const [draftConditions, setDraftConditions] =
useState<FilterCondition[]>(conditions);
useEffect(() => {
if (opened) {
setDraftConditions(conditions);
setDraft(null);
setUnSaved(false)
}
}, [opened, conditions]);
if (!opened) setDraft(null);
}, [opened]);
const handleStartDraft = useCallback(() => {
const firstProperty = properties[0];
@@ -257,21 +216,14 @@ export function ViewFilterConfigPopover({
}, [properties]);
const handleSaveDraft = useCallback(() => {
const nextConditions = draft
? [...draftConditions, draft]
: draftConditions;
onChange(nextConditions);
if (!draft) return;
onChange([...conditions, draft]);
setDraft(null);
setUnSaved(false)
}, [draft, draftConditions, onChange]);
}, [draft, conditions, onChange]);
const handleCancelDraft = useCallback(() => {
setDraftConditions(conditions)
setDraft(null);
setUnSaved(false)
}, [conditions]);
}, []);
const handleDraftPropertyChange = useCallback(
(propertyId: string | null) => {
@@ -320,19 +272,17 @@ export function ViewFilterConfigPopover({
const handleRemove = useCallback(
(index: number) => {
setUnSaved(true);
setDraftConditions((current) => current.filter((_, i) => i !== index));
onChange(conditions.filter((_, i) => i !== index));
},
[],
[conditions, onChange],
);
const handlePropertyChange = useCallback(
(index: number, propertyId: string | null) => {
if (!propertyId) return;
const newProperty = properties.find((p) => p.id === propertyId);
setUnSaved(true)
setDraftConditions((current) =>
current.map((f, i) => {
onChange(
conditions.map((f, i) => {
if (i !== index) return f;
if (newProperty) {
const validOperators = getOperatorsForType(newProperty.type);
@@ -352,16 +302,15 @@ export function ViewFilterConfigPopover({
}),
);
},
[properties],
[conditions, properties, onChange],
);
const handleOperatorChange = useCallback(
(index: number, operator: string | null) => {
if (!operator) return;
const op = operator as FilterOperator;
setUnSaved(true)
setDraftConditions((current) =>
current.map((f, i) => {
onChange(
conditions.map((f, i) => {
if (i !== index) return f;
const kind = inputKindForProperty(
properties.find((p) => p.id === f.propertyId),
@@ -371,17 +320,16 @@ export function ViewFilterConfigPopover({
}),
);
},
[properties],
[conditions, properties, onChange],
);
const handleValueChange = useCallback(
(index: number, value: unknown) => {
setUnSaved(true)
setDraftConditions((current) =>
current.map((f, i) => (i === index ? { ...f, value } : f)),
onChange(
conditions.map((f, i) => (i === index ? { ...f, value } : f)),
);
},
[],
[conditions, onChange],
);
return (
@@ -414,13 +362,13 @@ export function ViewFilterConfigPopover({
{t("Filter by")}
</Text>
{draftConditions.length === 0 && !draft && (
{conditions.length === 0 && !draft && (
<Text size="xs" c="dimmed">
{t("No filters applied")}
</Text>
)}
{draftConditions.map((condition, index) => {
{conditions.map((condition, index) => {
const needsValue = !NO_VALUE_OPERATORS.includes(condition.op);
const property = properties.find(
(p) => p.id === condition.propertyId,
@@ -523,6 +471,14 @@ export function ViewFilterConfigPopover({
/>
)}
</Group>
<Group justify="flex-end" gap="xs">
<Button variant="default" size="xs" onClick={handleCancelDraft}>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSaveDraft}>
{t("Save")}
</Button>
</Group>
</Stack>
);
})()}
@@ -536,20 +492,6 @@ export function ViewFilterConfigPopover({
{t("Add filter")}
</UnstyledButton>
)}
<Group justify="flex-end" gap="xs">
<Button
variant="default"
size="xs"
onClick={handleCancelDraft}
disabled={!draft && !unSaved}
>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSaveDraft} disabled={!draft && !unSaved}>
{t("Save")}
</Button>
</Group>
</Stack>
</Popover.Dropdown>
</Popover>
+2 -13
View File
@@ -22,8 +22,7 @@ import { TelemetryModule } from './integrations/telemetry/telemetry.module';
import { RedisModule } from '@nestjs-labs/nestjs-ioredis';
import { RedisConfigService } from './integrations/redis/redis-config.service';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis, { defaultReconnectStrategy } from '@keyv/redis';
import { parseRedisUrl } from './common/helpers';
import KeyvRedis from '@keyv/redis';
import { LoggerModule } from './common/logger/logger.module';
import { ClsModule } from 'nestjs-cls';
import { NoopAuditModule } from './integrations/audit/audit.module';
@@ -63,20 +62,10 @@ try {
isGlobal: true,
useFactory: async (environmentService: EnvironmentService) => {
const redisUrl = environmentService.getRedisUrl();
const { family, tls } = parseRedisUrl(redisUrl);
return {
ttl: 5 * 1000,
stores: [
new KeyvRedis({
url: redisUrl,
socket: {
family,
reconnectStrategy: defaultReconnectStrategy,
...tls,
},
}),
],
stores: [new KeyvRedis(redisUrl)],
};
},
inject: [EnvironmentService],
@@ -66,7 +66,6 @@ export class CollaborationGateway {
password: this.redisConfig.password,
db: this.redisConfig.db,
family: this.redisConfig.family,
tls: this.redisConfig.tls,
retryStrategy: createRetryStrategy(),
}),
serverId: `collab-${os?.hostname()}-${nanoid(10)}`,
+4 -12
View File
@@ -30,14 +30,13 @@ export type RedisConfig = {
db: number;
password?: string;
family?: number;
tls?: { rejectUnauthorized?: boolean };
};
export function parseRedisUrl(redisUrl: string): RedisConfig {
// format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6][&rejectUnauthorized=false]
// format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6]
const url = new URL(redisUrl);
const { hostname, port, password, pathname, protocol, searchParams } = url;
const portInt = port ? parseInt(port, 10) : 6379;
const { hostname, port, password, pathname, searchParams } = url;
const portInt = parseInt(port, 10);
let db: number = 0;
// extract db value if present
@@ -55,14 +54,7 @@ export function parseRedisUrl(redisUrl: string): RedisConfig {
family = parseInt(familyParam, 10);
}
const tls =
protocol === 'rediss:'
? searchParams.get('rejectUnauthorized') === 'false'
? { rejectUnauthorized: false }
: {}
: undefined;
return { host: hostname, port: portInt, password: password || undefined, db, family, tls };
return { host: hostname, port: portInt, password, db, family };
}
export function createRetryStrategy() {
@@ -339,25 +339,15 @@ export class SpaceMemberService {
return;
}
await executeTx(this.db, async (trx) => {
await trx
.selectFrom('spaces')
.select('id')
.where('id', '=', dto.spaceId)
.forUpdate()
.executeTakeFirst();
if (spaceMember.role === SpaceRole.ADMIN) {
await this.validateLastAdmin(dto.spaceId);
}
if (spaceMember.role === SpaceRole.ADMIN) {
await this.validateLastAdmin(dto.spaceId, trx);
}
await this.spaceMemberRepo.updateSpaceMember(
{ role: dto.role },
spaceMember.id,
dto.spaceId,
trx,
);
});
await this.spaceMemberRepo.updateSpaceMember(
{ role: dto.role },
spaceMember.id,
dto.spaceId,
);
this.auditService.log({
event: AuditEvent.SPACE_MEMBER_ROLE_CHANGED,
@@ -378,14 +368,10 @@ export class SpaceMemberService {
});
}
async validateLastAdmin(
spaceId: string,
trx?: KyselyTransaction,
): Promise<void> {
async validateLastAdmin(spaceId: string): Promise<void> {
const spaceOwnerCount = await this.spaceMemberRepo.roleCountBySpaceId(
SpaceRole.ADMIN,
spaceId,
trx,
);
if (spaceOwnerCount === 1) {
throw new BadRequestException(
@@ -46,10 +46,8 @@ export class SpaceMemberRepo {
updatableSpaceMember: UpdatableSpaceMember,
spaceMemberId: string,
spaceId: string,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
await this.db
.updateTable('spaceMembers')
.set(updatableSpaceMember)
.where('id', '=', spaceMemberId)
@@ -94,13 +92,8 @@ export class SpaceMemberRepo {
.execute();
}
async roleCountBySpaceId(
role: string,
spaceId: string,
trx?: KyselyTransaction,
): Promise<number> {
const db = dbOrTx(this.db, trx);
const { count } = await db
async roleCountBySpaceId(role: string, spaceId: string): Promise<number> {
const { count } = await this.db
.selectFrom('spaceMembers')
.select((eb) => eb.fn.count('role').as('count'))
.where('role', '=', role)
@@ -5,7 +5,6 @@ import {
import { Injectable, Logger } from '@nestjs/common';
import { EnvironmentService } from '../environment/environment.service';
import { Redis } from 'ioredis';
import { parseRedisUrl } from '../../common/helpers';
@Injectable()
export class RedisHealthIndicator {
@@ -20,10 +19,8 @@ export class RedisHealthIndicator {
const indicator = this.healthIndicatorService.check(key);
try {
const redisUrl = this.environmentService.getRedisUrl();
const redis = new Redis(redisUrl, {
const redis = new Redis(this.environmentService.getRedisUrl(), {
maxRetriesPerRequest: 15,
tls: parseRedisUrl(redisUrl).tls,
});
await redis.ping();
@@ -18,7 +18,6 @@ import { GeneralQueueProcessor } from './processors/general-queue.processor';
password: redisConfig.password,
db: redisConfig.db,
family: redisConfig.family,
tls: redisConfig.tls,
retryStrategy: createRetryStrategy(),
},
defaultJobOptions: {
@@ -19,7 +19,6 @@ export class RedisConfigService implements RedisOptionsFactory {
password: redisConfig.password,
db: redisConfig.db,
family: redisConfig.family,
tls: redisConfig.tls,
retryStrategy: createRetryStrategy(),
},
};
@@ -17,7 +17,6 @@ export class WsRedisIoAdapter extends IoAdapter {
const options: RedisOptions = {
family: this.redisConfig.family,
tls: this.redisConfig.tls,
retryStrategy: createRetryStrategy(),
};