fix(server): PDF template logo not showing on reopen
Generate presigned URL for companyLogoKey when retrieving PDF template to allow the logo to display when reopening the branding template drawer. - Inject GetAttachmentPresignedUrl service in GetPdfTemplateService - Generate companyLogoUri from companyLogoKey in template attributes - Add companyLogoUri to transformer included attributes Fixes the issue where logo uploads and saves but doesn't show when reopening the branding template customization drawer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import * as multerS3 from 'multer-s3';
|
||||
import { S3_CLIENT, S3Module } from "../S3/S3.module";
|
||||
import { DeleteAttachment } from "./DeleteAttachment";
|
||||
import { GetAttachment } from "./GetAttachment";
|
||||
import { getAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
|
||||
import { GetAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
|
||||
import { LinkAttachment } from "./LinkAttachment";
|
||||
import { UnlinkAttachment } from "./UnlinkAttachment";
|
||||
import { ValidateAttachments } from "./ValidateAttachments";
|
||||
@@ -35,12 +35,12 @@ const models = [
|
||||
|
||||
@Module({
|
||||
imports: [S3Module, ...models],
|
||||
exports: [...models],
|
||||
exports: [...models, GetAttachmentPresignedUrl],
|
||||
controllers: [AttachmentsController],
|
||||
providers: [
|
||||
DeleteAttachment,
|
||||
GetAttachment,
|
||||
getAttachmentPresignedUrl,
|
||||
GetAttachmentPresignedUrl,
|
||||
LinkAttachment,
|
||||
UnlinkAttachment,
|
||||
ValidateAttachments,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DeleteAttachment } from './DeleteAttachment';
|
||||
import { GetAttachment } from './GetAttachment';
|
||||
import { LinkAttachment } from './LinkAttachment';
|
||||
import { UnlinkAttachment } from './UnlinkAttachment';
|
||||
import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
|
||||
import { GetAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsApplication {
|
||||
@@ -14,7 +14,7 @@ export class AttachmentsApplication {
|
||||
private readonly getDocumentService: GetAttachment,
|
||||
private readonly linkDocumentService: LinkAttachment,
|
||||
private readonly unlinkDocumentService: UnlinkAttachment,
|
||||
private readonly getPresignedUrlService: getAttachmentPresignedUrl,
|
||||
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { S3_CLIENT } from '../S3/S3.module';
|
||||
|
||||
@Injectable()
|
||||
export class getAttachmentPresignedUrl {
|
||||
export class GetAttachmentPresignedUrl {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as path from 'path';
|
||||
// import config from '@/config';
|
||||
|
||||
export const getUploadedObjectUri = (objectKey: string) => {
|
||||
return '';
|
||||
// return new URL(
|
||||
// path.join(config.s3.bucket, objectKey),
|
||||
// config.s3.endpoint
|
||||
// ).toString();
|
||||
};
|
||||
@@ -15,6 +15,8 @@ import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBase
|
||||
import { SyncSystemUserToTenantService } from './commands/SyncSystemUserToTenant.service';
|
||||
import { SyncSystemUserToTenantSubscriber } from './subscribers/SyncSystemUserToTenant.subscriber';
|
||||
import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service';
|
||||
import { AttachmentsModule } from '../Attachments/Attachment.module';
|
||||
import { TransformerModule } from '../Transformer/Transformer.module';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
@@ -36,6 +38,8 @@ import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
TenantDBManagerModule,
|
||||
AttachmentsModule,
|
||||
TransformerModule,
|
||||
],
|
||||
controllers: [OrganizationController],
|
||||
})
|
||||
|
||||
@@ -79,6 +79,13 @@ export class OrganizationMetadataResponseDto {
|
||||
})
|
||||
logoKey: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Logo URL (presigned or public) for display',
|
||||
example: 'https://...',
|
||||
nullable: true,
|
||||
})
|
||||
logoUri: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Organization address details',
|
||||
example: '123 Main St, New York, NY',
|
||||
|
||||
@@ -3,14 +3,20 @@ import { throwIfTenantNotExists } from '../Organization/_utils';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ModelObject } from 'objection';
|
||||
import { GetAttachmentPresignedUrl } from '@/modules/Attachments/GetAttachmentPresignedUrl';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { GetCurrentOrganizationTransformer } from './GetCurrentOrganization.transformer';
|
||||
|
||||
@Injectable()
|
||||
export class GetCurrentOrganizationService {
|
||||
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||
constructor(
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the current organization metadata.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<ITenant[]>}
|
||||
*/
|
||||
async getCurrentOrganization(): Promise<ModelObject<TenantModel>> {
|
||||
@@ -21,6 +27,13 @@ export class GetCurrentOrganizationService {
|
||||
|
||||
throwIfTenantNotExists(tenant);
|
||||
|
||||
return tenant;
|
||||
const logoUri = tenant.metadata?.logoKey ?
|
||||
await this.getPresignedUrlService.getPresignedUrl(tenant.metadata.logoKey) : null;
|
||||
|
||||
return await this.transformer.transform(
|
||||
tenant,
|
||||
new GetCurrentOrganizationTransformer(),
|
||||
{ logoUri },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { GetCurrentOrganizationMetadataTransformer } from './GetCurrentOrganizationMetadata.transformer';
|
||||
|
||||
export class GetCurrentOrganizationTransformer extends Transformer {
|
||||
/**
|
||||
* Transforms the tenant/organization for the current-organization response.
|
||||
* Delegates metadata transformation to GetCurrentOrganizationMetadataTransformer
|
||||
* and injects the pre-computed logoUri from options.
|
||||
*/
|
||||
transform = (tenant: Record<string, any>) => {
|
||||
const metadataTransformer = new GetCurrentOrganizationMetadataTransformer();
|
||||
const transformedMetadata = this.item(tenant.metadata, metadataTransformer, {
|
||||
logoUri: this.options?.logoUri,
|
||||
});
|
||||
|
||||
return {
|
||||
...tenant,
|
||||
metadata: transformedMetadata,
|
||||
};
|
||||
};
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
|
||||
export class GetCurrentOrganizationMetadataTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes in the metadata response.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['logoUri'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Logo URI (presigned or public URL) for display.
|
||||
* Provided via options from the service after resolving logoKey.
|
||||
* @param metadata
|
||||
* @returns {string | null}
|
||||
*/
|
||||
public logoUri = (metadata: Record<string, any>): string | null => {
|
||||
return this.options?.logoUri ?? null;
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { BrandingTemplateDTOTransformer } from './BrandingTemplateDTOTransformer
|
||||
import { GetOrganizationBrandingAttributesService } from './queries/GetOrganizationBrandingAttributes.service';
|
||||
import { GetPdfTemplates } from './queries/GetPdfTemplates.service';
|
||||
import { GetPdfTemplateBrandingState } from './queries/GetPdfTemplateBrandingState.service';
|
||||
import { AttachmentsModule } from '../Attachments/Attachment.module';
|
||||
|
||||
@Module({
|
||||
exports: [
|
||||
@@ -20,7 +21,7 @@ import { GetPdfTemplateBrandingState } from './queries/GetPdfTemplateBrandingSta
|
||||
BrandingTemplateDTOTransformer,
|
||||
GetOrganizationBrandingAttributesService,
|
||||
],
|
||||
imports: [TenancyDatabaseModule],
|
||||
imports: [TenancyDatabaseModule, AttachmentsModule],
|
||||
controllers: [PdfTemplatesController],
|
||||
providers: [
|
||||
PdfTemplateApplication,
|
||||
|
||||
@@ -56,16 +56,6 @@ export class PdfTemplateModel extends TenantBaseModel {
|
||||
return ['companyLogoUri'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the company logo uri.
|
||||
* @returns {string}
|
||||
*/
|
||||
// get companyLogoUri() {
|
||||
// return this.attributes?.companyLogoKey
|
||||
// ? getUploadedObjectUri(this.attributes.companyLogoKey)
|
||||
// : '';
|
||||
// }
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
+16
-3
@@ -1,10 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommonOrganizationBrandingAttributes } from '../types';
|
||||
import { TenancyContext } from '../../Tenancy/TenancyContext.service';
|
||||
import { GetAttachmentPresignedUrl } from '@/modules/Attachments/GetAttachmentPresignedUrl';
|
||||
|
||||
@Injectable()
|
||||
export class GetOrganizationBrandingAttributesService {
|
||||
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||
constructor(
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the given organization branding attributes initial state.
|
||||
@@ -17,13 +21,22 @@ export class GetOrganizationBrandingAttributesService {
|
||||
const companyName = tenantMetadata?.name;
|
||||
const primaryColor = tenantMetadata?.primaryColor;
|
||||
const companyLogoKey = tenantMetadata?.logoKey;
|
||||
const companyLogoUri = tenantMetadata?.logoUri;
|
||||
const companyAddress = tenantMetadata?.addressTextFormatted;
|
||||
|
||||
let companyLogoUri: string | null = null;
|
||||
if (companyLogoKey) {
|
||||
try {
|
||||
companyLogoUri =
|
||||
await this.getPresignedUrlService.getPresignedUrl(companyLogoKey);
|
||||
} catch {
|
||||
companyLogoUri = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
companyName,
|
||||
companyAddress,
|
||||
companyLogoUri,
|
||||
companyLogoUri: companyLogoUri ?? undefined,
|
||||
companyLogoKey,
|
||||
primaryColor,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GetPdfTemplateTransformer } from './GetPdfTemplate.transformer';
|
||||
import { PdfTemplateModel } from '../models/PdfTemplate';
|
||||
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { GetAttachmentPresignedUrl } from '@/modules/Attachments/GetAttachmentPresignedUrl';
|
||||
|
||||
@Injectable()
|
||||
export class GetPdfTemplateService {
|
||||
@@ -13,6 +14,7 @@ export class GetPdfTemplateService {
|
||||
typeof PdfTemplateModel
|
||||
>,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -29,8 +31,19 @@ export class GetPdfTemplateService {
|
||||
.findById(templateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const companyLogoKey = template.attributes?.companyLogoKey;
|
||||
let companyLogoUri: string | null = null;
|
||||
|
||||
if (companyLogoKey) {
|
||||
try {
|
||||
companyLogoUri =
|
||||
await this.getPresignedUrlService.getPresignedUrl(companyLogoKey);
|
||||
} catch {
|
||||
companyLogoUri = null;
|
||||
}
|
||||
}
|
||||
return this.transformer.transform(
|
||||
template,
|
||||
{ ...template, companyLogoUri },
|
||||
new GetPdfTemplateTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export class GetPdfTemplateTransformer extends Transformer {
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['createdAtFormatted', 'resourceFormatted', 'attributes'];
|
||||
return ['createdAtFormatted', 'resourceFormatted', 'attributes', 'companyLogoUri'];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -28,6 +28,15 @@ export class GetPdfTemplateTransformer extends Transformer {
|
||||
// return getTransactionTypeLabel(template.resource);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the company logo URI.
|
||||
* @param {Object} template
|
||||
* @returns {string | null}
|
||||
*/
|
||||
protected companyLogoUri = (template) => {
|
||||
return template.companyLogoUri;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves transformed brand attributes.
|
||||
* @param {} template
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
organizationAddressTextFormat,
|
||||
} from '@/utils/address-text-format';
|
||||
import { findByIsoCountryCode } from '@bigcapital/utils';
|
||||
// import { getUploadedObjectUri } from '../../services/Attachments/utils';
|
||||
|
||||
export class TenantMetadata extends BaseModel {
|
||||
public baseCurrency!: string;
|
||||
@@ -65,17 +64,9 @@ export class TenantMetadata extends BaseModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization logo url.
|
||||
* @returns {string | null}
|
||||
* Retrieves the organization address formatted text.
|
||||
* @returns {string}
|
||||
*/
|
||||
// public get logoUri() {
|
||||
// return this.logoKey ? getUploadedObjectUri(this.logoKey) : null;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieves the organization address formatted text.
|
||||
// * @returns {string}
|
||||
// */
|
||||
public get addressTextFormatted() {
|
||||
const addressCountry = findByIsoCountryCode(this.location);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user