fix: fehlende Dateien fuer Etiketten-Elemente-Umbau nachreichen
Backend Image / build-and-push (push) Successful in 2m19s
Frontend Image / build-and-push (push) Successful in 1m55s

git add -u im letzten Commit hat neue (untracked) Dateien übersprungen:
die Migration für LabelTemplate.elements, die neuen Typ-/DTO-Dateien und
den kompletten Drag-and-Drop-Editor fehlten dadurch im Build.
This commit is contained in:
Nick Szaunig committed 2026-09-01 16:59:48 +02:00
1 parent 5f4e1a7ba9
commit 938239e16e
9 files changed
+1134

No files matched your search

@@ -0,0 +1,78 @@
import { PermissionCatalogSyncService } from './permission-catalog-sync.service';
import { PrismaService } from '../../prisma/prisma.service';
import { PERMISSION_CATALOG } from './permission-catalog';
describe('PermissionCatalogSyncService', () => {
let service: PermissionCatalogSyncService;
let prisma: {
permission: { findMany: jest.Mock; create: jest.Mock };
role: { findMany: jest.Mock };
rolePermission: { createMany: jest.Mock };
};
beforeEach(() => {
prisma = {
permission: {
findMany: jest.fn(),
create: jest.fn((args: { data: { key: string } }) =>
Promise.resolve({ id: `perm-${args.data.key}`, ...args.data }),
),
},
role: { findMany: jest.fn().mockResolvedValue([]) },
rolePermission: { createMany: jest.fn().mockResolvedValue({ count: 0 }) },
};
service = new PermissionCatalogSyncService(
prisma as unknown as PrismaService,
);
});
it('does nothing when every catalog permission already exists', async () => {
prisma.permission.findMany.mockResolvedValue(
PERMISSION_CATALOG.map((entry) => ({ key: entry.key })),
);
await service.onModuleInit();
expect(prisma.permission.create).not.toHaveBeenCalled();
expect(prisma.role.findMany).not.toHaveBeenCalled();
expect(prisma.rolePermission.createMany).not.toHaveBeenCalled();
});
it('inserts missing catalog permissions and grants them to every system role', async () => {
const [first, ...rest] = PERMISSION_CATALOG;
prisma.permission.findMany.mockResolvedValue(
rest.map((entry) => ({ key: entry.key })),
);
prisma.role.findMany.mockResolvedValue([
{ id: 'role-admin' },
{ id: 'role-owner' },
]);
await service.onModuleInit();
expect(prisma.permission.create).toHaveBeenCalledWith({ data: first });
expect(prisma.role.findMany).toHaveBeenCalledWith({
where: { isSystem: true },
select: { id: true },
});
expect(prisma.rolePermission.createMany).toHaveBeenCalledWith({
data: [
{ roleId: 'role-admin', permissionId: `perm-${first.key}` },
{ roleId: 'role-owner', permissionId: `perm-${first.key}` },
],
skipDuplicates: true,
});
});
it('skips the role grant step when no system role exists', async () => {
prisma.permission.findMany.mockResolvedValue([]);
prisma.role.findMany.mockResolvedValue([]);
await service.onModuleInit();
expect(prisma.permission.create).toHaveBeenCalledTimes(
PERMISSION_CATALOG.length,
);
expect(prisma.rolePermission.createMany).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,116 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsIn,
IsNumber,
IsOptional,
IsString,
MaxLength,
} from 'class-validator';
import { PLACEHOLDER_KEYS } from '../label-element.types';
const ELEMENT_TYPES = ['text', 'qrCode', 'image', 'line'] as const;
const FONT_FAMILIES = ['sans', 'serif', 'mono'] as const;
const TEXT_ALIGNS = ['left', 'center', 'right'] as const;
const LINE_ORIENTATIONS = ['horizontal', 'vertical'] as const;
// One DTO shared by all element types rather than a discriminated-union of
// four DTOs: this is an internal, permission-gated design surface (not
// public input), so the pragmatic boundary check is "every field has the
// right primitive type" - the service picks which fields it reads per
// `type` at render time.
export class LabelElementDto {
@ApiProperty()
@IsString()
id!: string;
@ApiProperty({ enum: ELEMENT_TYPES })
@IsIn(ELEMENT_TYPES)
type!: (typeof ELEMENT_TYPES)[number];
@ApiProperty()
@IsNumber()
x!: number;
@ApiProperty()
@IsNumber()
y!: number;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
width?: number;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
height?: number;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
size?: number;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
length?: number;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
thickness?: number;
@ApiPropertyOptional({ description: 'text-Element: literaler Text' })
@IsOptional()
@IsString()
@MaxLength(500)
text?: string;
@ApiPropertyOptional({ enum: PLACEHOLDER_KEYS })
@IsOptional()
@IsIn(PLACEHOLDER_KEYS)
placeholderKey?: (typeof PLACEHOLDER_KEYS)[number] | null;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
fontSize?: number;
@ApiPropertyOptional({ enum: FONT_FAMILIES })
@IsOptional()
@IsIn(FONT_FAMILIES)
fontFamily?: (typeof FONT_FAMILIES)[number];
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
bold?: boolean;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
italic?: boolean;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(20)
color?: string;
@ApiPropertyOptional({ enum: TEXT_ALIGNS })
@IsOptional()
@IsIn(TEXT_ALIGNS)
align?: (typeof TEXT_ALIGNS)[number];
@ApiPropertyOptional({ enum: LINE_ORIENTATIONS })
@IsOptional()
@IsIn(LINE_ORIENTATIONS)
orientation?: (typeof LINE_ORIENTATIONS)[number];
@ApiPropertyOptional({ description: 'image-Element: data: URL (PNG/JPEG)' })
@IsOptional()
@IsString()
@MaxLength(3_000_000)
src?: string;
}
@@ -0,0 +1,57 @@
export type LabelFontFamily = 'sans' | 'serif' | 'mono';
export type LabelTextAlign = 'left' | 'center' | 'right';
export type LabelLineOrientation = 'horizontal' | 'vertical';
export const PLACEHOLDER_KEYS = [
'batchNumber',
'honeyType',
'lotNumber',
'bottlingDate',
'bestBeforeDate',
'jarSize',
'productName',
] as const;
export type PlaceholderKey = (typeof PLACEHOLDER_KEYS)[number];
interface LabelElementBase {
id: string;
x: number;
y: number;
}
export interface LabelTextElement extends LabelElementBase {
type: 'text';
width: number;
height: number;
text: string;
placeholderKey?: PlaceholderKey | null;
fontSize: number;
fontFamily: LabelFontFamily;
bold: boolean;
italic: boolean;
color: string;
align: LabelTextAlign;
}
export interface LabelQrCodeElement extends LabelElementBase {
type: 'qrCode';
size: number;
}
export interface LabelImageElement extends LabelElementBase {
type: 'image';
width: number;
height: number;
src: string;
}
export interface LabelLineElement extends LabelElementBase {
type: 'line';
length: number;
thickness: number;
color: string;
orientation: LabelLineOrientation;
}
export type LabelElement =
LabelTextElement | LabelQrCodeElement | LabelImageElement | LabelLineElement;