mirror of
https://github.com/hexastack/hexabot
synced 2025-05-09 15:10:57 +00:00
Merge pull request #347 from Hexastack/fix/list-attachment
fix: content vs element
This commit is contained in:
commit
db13d02c96
@ -120,7 +120,6 @@ export const contentMessage: StdOutgoingListMessage = {
|
|||||||
id: '1',
|
id: '1',
|
||||||
entity: 'rank',
|
entity: 'rank',
|
||||||
title: 'First',
|
title: 'First',
|
||||||
// @ts-expect-error Necessary workaround
|
|
||||||
desc: 'About being first',
|
desc: 'About being first',
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
payload: attachmentWithUrl,
|
payload: attachmentWithUrl,
|
||||||
@ -136,7 +135,6 @@ export const contentMessage: StdOutgoingListMessage = {
|
|||||||
id: '2',
|
id: '2',
|
||||||
entity: 'rank',
|
entity: 'rank',
|
||||||
title: 'Second',
|
title: 'Second',
|
||||||
// @ts-expect-error Necessary workaround
|
|
||||||
desc: 'About being second',
|
desc: 'About being second',
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
payload: attachmentWithUrl,
|
payload: attachmentWithUrl,
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||||
import { Content } from '@/cms/schemas/content.schema';
|
|
||||||
|
|
||||||
import { Message } from '../message.schema';
|
import { Message } from '../message.schema';
|
||||||
|
|
||||||
@ -84,9 +83,14 @@ export type StdOutgoingButtonsMessage = {
|
|||||||
buttons: Button[];
|
buttons: Button[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ContentElement = { id: string; title: string } & Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
|
||||||
export type StdOutgoingListMessage = {
|
export type StdOutgoingListMessage = {
|
||||||
options: ContentOptions;
|
options: ContentOptions;
|
||||||
elements: Content[];
|
elements: ContentElement[];
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
skip: number;
|
skip: number;
|
||||||
|
@ -442,9 +442,7 @@ describe('BlockService', () => {
|
|||||||
{ status: true, entity: contentType.id },
|
{ status: true, entity: contentType.id },
|
||||||
{ skip: 0, limit: 2, sort: ['createdAt', 'desc'] },
|
{ skip: 0, limit: 2, sort: ['createdAt', 'desc'] },
|
||||||
);
|
);
|
||||||
const flattenedElements = elements.map((element) =>
|
const flattenedElements = elements.map(Content.toElement);
|
||||||
Content.flatDynamicFields(element),
|
|
||||||
);
|
|
||||||
expect(result.format).toEqualPayload(
|
expect(result.format).toEqualPayload(
|
||||||
blockProductListMock.options.content?.display,
|
blockProductListMock.options.content?.display,
|
||||||
);
|
);
|
||||||
@ -476,9 +474,7 @@ describe('BlockService', () => {
|
|||||||
{ status: true, entity: contentType.id },
|
{ status: true, entity: contentType.id },
|
||||||
{ skip: 2, limit: 2, sort: ['createdAt', 'desc'] },
|
{ skip: 2, limit: 2, sort: ['createdAt', 'desc'] },
|
||||||
);
|
);
|
||||||
const flattenedElements = elements.map((element) =>
|
const flattenedElements = elements.map(Content.toElement);
|
||||||
Content.flatDynamicFields(element),
|
|
||||||
);
|
|
||||||
expect(result.format).toEqual(
|
expect(result.format).toEqual(
|
||||||
blockProductListMock.options.content?.display,
|
blockProductListMock.options.content?.display,
|
||||||
);
|
);
|
||||||
|
@ -6,10 +6,11 @@
|
|||||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||||
import { Transform, Type } from 'class-transformer';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import mongoose, { Document } from 'mongoose';
|
import mongoose, { Document } from 'mongoose';
|
||||||
|
|
||||||
|
import { ContentElement } from '@/chat/schemas/types/message';
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||||
@ -51,14 +52,14 @@ export class ContentStub extends BaseSchema {
|
|||||||
/**
|
/**
|
||||||
* Helper to return the internal url of this content.
|
* Helper to return the internal url of this content.
|
||||||
*/
|
*/
|
||||||
static getUrl(item: Content): string {
|
static getUrl(item: ContentElement): string {
|
||||||
return new URL('/content/view/' + item.id, config.apiPath).toString();
|
return new URL('/content/view/' + item.id, config.apiPath).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper that returns the relative chatbot payload for this content.
|
* Helper that returns the relative chatbot payload for this content.
|
||||||
*/
|
*/
|
||||||
static getPayload(item: Content): string {
|
static getPayload(item: ContentElement): string {
|
||||||
return 'postback' in item ? (item.postback as string) : item.title;
|
return 'postback' in item ? (item.postback as string) : item.title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,12 +69,18 @@ export class Content extends ContentStub {
|
|||||||
@Transform(({ obj }) => obj.entity.toString())
|
@Transform(({ obj }) => obj.entity.toString())
|
||||||
entity: string;
|
entity: string;
|
||||||
|
|
||||||
static flatDynamicFields(element: Content) {
|
/**
|
||||||
Object.entries(element.dynamicFields).forEach(([key, value]) => {
|
* Converts a content object to an element (A flat representation of a content)
|
||||||
element[key] = value;
|
*
|
||||||
});
|
* @param content
|
||||||
element.dynamicFields = undefined;
|
* @returns An object that has all dynamic fields accessible at top level
|
||||||
return element;
|
*/
|
||||||
|
static toElement(content: Content): ContentElement {
|
||||||
|
return {
|
||||||
|
id: content.id,
|
||||||
|
title: content.title,
|
||||||
|
...content.dynamicFields,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,10 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
|||||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||||
import { FileType } from '@/chat/schemas/types/attachment';
|
import { FileType } from '@/chat/schemas/types/attachment';
|
||||||
import { OutgoingMessageFormat } from '@/chat/schemas/types/message';
|
import {
|
||||||
|
ContentElement,
|
||||||
|
OutgoingMessageFormat,
|
||||||
|
} from '@/chat/schemas/types/message';
|
||||||
import { ContentOptions } from '@/chat/schemas/types/options';
|
import { ContentOptions } from '@/chat/schemas/types/options';
|
||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.service';
|
||||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||||
@ -159,7 +162,10 @@ describe('ContentService', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
it('should return all content attachment ids', () => {
|
it('should return all content attachment ids', () => {
|
||||||
const result = contentService.getAttachmentIds(contents, 'image');
|
const result = contentService.getAttachmentIds(
|
||||||
|
contents.map(Content.toElement),
|
||||||
|
'image',
|
||||||
|
);
|
||||||
expect(result).toEqual(['123', '456']);
|
expect(result).toEqual(['123', '456']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,16 +178,14 @@ describe('ContentService', () => {
|
|||||||
describe('populateAttachments', () => {
|
describe('populateAttachments', () => {
|
||||||
it('should return populated content', async () => {
|
it('should return populated content', async () => {
|
||||||
const storeContents = await contentService.find({ title: /^store/ });
|
const storeContents = await contentService.find({ title: /^store/ });
|
||||||
const populatedStoreContents: Content[] = await Promise.all(
|
const elements: ContentElement[] = await Promise.all(
|
||||||
storeContents.map(async (store) => {
|
storeContents.map(Content.toElement).map(async (store) => {
|
||||||
const attachmentId = store.dynamicFields.image.payload.attachment_id;
|
const attachmentId = store.image.payload.attachment_id;
|
||||||
if (attachmentId) {
|
if (attachmentId) {
|
||||||
const attachment = await attachmentService.findOne(attachmentId);
|
const attachment = await attachmentService.findOne(attachmentId);
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
dynamicFields: {
|
|
||||||
...store.dynamicFields,
|
|
||||||
image: {
|
image: {
|
||||||
type: 'image',
|
type: 'image',
|
||||||
payload: {
|
payload: {
|
||||||
@ -189,7 +193,6 @@ describe('ContentService', () => {
|
|||||||
url: `http://localhost:4000/attachment/download/${attachment.id}/${attachment.name}`,
|
url: `http://localhost:4000/attachment/download/${attachment.id}/${attachment.name}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,10 +200,10 @@ describe('ContentService', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const result = await contentService.populateAttachments(
|
const result = await contentService.populateAttachments(
|
||||||
storeContents,
|
storeContents.map(Content.toElement),
|
||||||
'image',
|
'image',
|
||||||
);
|
);
|
||||||
expect(result).toEqualPayload(populatedStoreContents);
|
expect(result).toEqualPayload(elements);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -221,9 +224,7 @@ describe('ContentService', () => {
|
|||||||
{ status: true },
|
{ status: true },
|
||||||
{ skip: 0, limit: 10, sort: ['createdAt', 'desc'] },
|
{ skip: 0, limit: 10, sort: ['createdAt', 'desc'] },
|
||||||
);
|
);
|
||||||
const flattenedElements = actualData.map((content) =>
|
const flattenedElements = actualData.map(Content.toElement);
|
||||||
Content.flatDynamicFields(content),
|
|
||||||
);
|
|
||||||
const content = await contentService.getContent(contentOptions, 0);
|
const content = await contentService.getContent(contentOptions, 0);
|
||||||
expect(content?.elements).toEqualPayload(flattenedElements, [
|
expect(content?.elements).toEqualPayload(flattenedElements, [
|
||||||
...IGNORED_TEST_FIELDS,
|
...IGNORED_TEST_FIELDS,
|
||||||
@ -237,9 +238,7 @@ describe('ContentService', () => {
|
|||||||
{ status: true, entity: contentType.id },
|
{ status: true, entity: contentType.id },
|
||||||
{ skip: 0, limit: 10, sort: ['createdAt', 'desc'] },
|
{ skip: 0, limit: 10, sort: ['createdAt', 'desc'] },
|
||||||
);
|
);
|
||||||
const flattenedElements = actualData.map((content) =>
|
const flattenedElements = actualData.map(Content.toElement);
|
||||||
Content.flatDynamicFields(content),
|
|
||||||
);
|
|
||||||
const content = await contentService.getContent(
|
const content = await contentService.getContent(
|
||||||
{
|
{
|
||||||
...contentOptions,
|
...contentOptions,
|
||||||
@ -257,9 +256,7 @@ describe('ContentService', () => {
|
|||||||
{ status: true, entity: contentType.id, title: /^Jean/ },
|
{ status: true, entity: contentType.id, title: /^Jean/ },
|
||||||
{ skip: 0, limit: 10, sort: ['createdAt', 'desc'] },
|
{ skip: 0, limit: 10, sort: ['createdAt', 'desc'] },
|
||||||
);
|
);
|
||||||
const flattenedElements = actualData.map((content) =>
|
const flattenedElements = actualData.map(Content.toElement);
|
||||||
Content.flatDynamicFields(content),
|
|
||||||
);
|
|
||||||
const content = await contentService.getContent(
|
const content = await contentService.getContent(
|
||||||
{
|
{
|
||||||
...contentOptions,
|
...contentOptions,
|
||||||
@ -276,9 +273,7 @@ describe('ContentService', () => {
|
|||||||
{ status: true },
|
{ status: true },
|
||||||
{ skip: 2, limit: 2, sort: ['createdAt', 'desc'] },
|
{ skip: 2, limit: 2, sort: ['createdAt', 'desc'] },
|
||||||
);
|
);
|
||||||
const flattenedElements = actualData.map((content) =>
|
const flattenedElements = actualData.map(Content.toElement);
|
||||||
Content.flatDynamicFields(content),
|
|
||||||
);
|
|
||||||
const content = await contentService.getContent(
|
const content = await contentService.getContent(
|
||||||
{
|
{
|
||||||
...contentOptions,
|
...contentOptions,
|
||||||
|
@ -11,7 +11,10 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||||
import { WithUrl } from '@/chat/schemas/types/attachment';
|
import { WithUrl } from '@/chat/schemas/types/attachment';
|
||||||
import { StdOutgoingListMessage } from '@/chat/schemas/types/message';
|
import {
|
||||||
|
ContentElement,
|
||||||
|
StdOutgoingListMessage,
|
||||||
|
} from '@/chat/schemas/types/message';
|
||||||
import { ContentOptions } from '@/chat/schemas/types/options';
|
import { ContentOptions } from '@/chat/schemas/types/options';
|
||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.service';
|
||||||
import { BaseService } from '@/utils/generics/base-service';
|
import { BaseService } from '@/utils/generics/base-service';
|
||||||
@ -57,10 +60,10 @@ export class ContentService extends BaseService<
|
|||||||
*
|
*
|
||||||
* @return A list of attachment IDs.
|
* @return A list of attachment IDs.
|
||||||
*/
|
*/
|
||||||
getAttachmentIds(contents: Content[], attachmentFieldName: string) {
|
getAttachmentIds(contents: ContentElement[], attachmentFieldName: string) {
|
||||||
return contents.reduce((acc, content) => {
|
return contents.reduce((acc, content) => {
|
||||||
if (attachmentFieldName in content.dynamicFields) {
|
if (attachmentFieldName in content) {
|
||||||
const attachment = content.dynamicFields[attachmentFieldName];
|
const attachment = content[attachmentFieldName];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof attachment === 'object' &&
|
typeof attachment === 'object' &&
|
||||||
@ -84,16 +87,16 @@ export class ContentService extends BaseService<
|
|||||||
/**
|
/**
|
||||||
* Populates attachment fields within content entities with detailed attachment information.
|
* Populates attachment fields within content entities with detailed attachment information.
|
||||||
*
|
*
|
||||||
* @param contents - An array of content entities.
|
* @param elements - An array of content entities.
|
||||||
* @param attachmentFieldName - The name of the attachment field to populate.
|
* @param attachmentFieldName - The name of the attachment field to populate.
|
||||||
*
|
*
|
||||||
* @return A list of content with populated attachment data.
|
* @return A list of content with populated attachment data.
|
||||||
*/
|
*/
|
||||||
async populateAttachments(
|
async populateAttachments(
|
||||||
contents: Content[],
|
elements: ContentElement[],
|
||||||
attachmentFieldName: string,
|
attachmentFieldName: string,
|
||||||
): Promise<Content[]> {
|
): Promise<ContentElement[]> {
|
||||||
const attachmentIds = this.getAttachmentIds(contents, attachmentFieldName);
|
const attachmentIds = this.getAttachmentIds(elements, attachmentFieldName);
|
||||||
|
|
||||||
if (attachmentIds.length > 0) {
|
if (attachmentIds.length > 0) {
|
||||||
const attachments = await this.attachmentService.find({
|
const attachments = await this.attachmentService.find({
|
||||||
@ -107,8 +110,8 @@ export class ContentService extends BaseService<
|
|||||||
},
|
},
|
||||||
{} as { [key: string]: WithUrl<Attachment> },
|
{} as { [key: string]: WithUrl<Attachment> },
|
||||||
);
|
);
|
||||||
const populatedContents = contents.map((content) => {
|
const populatedContents = elements.map((content) => {
|
||||||
const attachmentField = content.dynamicFields[attachmentFieldName];
|
const attachmentField = content[attachmentFieldName];
|
||||||
if (
|
if (
|
||||||
typeof attachmentField === 'object' &&
|
typeof attachmentField === 'object' &&
|
||||||
'attachment_id' in attachmentField.payload
|
'attachment_id' in attachmentField.payload
|
||||||
@ -116,8 +119,6 @@ export class ContentService extends BaseService<
|
|||||||
const attachmentId = attachmentField?.payload?.attachment_id;
|
const attachmentId = attachmentField?.payload?.attachment_id;
|
||||||
return {
|
return {
|
||||||
...content,
|
...content,
|
||||||
dynamicFields: {
|
|
||||||
...content.dynamicFields,
|
|
||||||
[attachmentFieldName]: {
|
[attachmentFieldName]: {
|
||||||
type: attachmentField.type,
|
type: attachmentField.type,
|
||||||
payload: {
|
payload: {
|
||||||
@ -128,7 +129,6 @@ export class ContentService extends BaseService<
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return content;
|
return content;
|
||||||
@ -136,7 +136,7 @@ export class ContentService extends BaseService<
|
|||||||
});
|
});
|
||||||
return populatedContents;
|
return populatedContents;
|
||||||
}
|
}
|
||||||
return contents;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,21 +175,15 @@ export class ContentService extends BaseService<
|
|||||||
limit,
|
limit,
|
||||||
sort: ['createdAt', 'desc'],
|
sort: ['createdAt', 'desc'],
|
||||||
});
|
});
|
||||||
|
const elements = contents.map(Content.toElement);
|
||||||
const attachmentFieldName = options.fields.image_url;
|
const attachmentFieldName = options.fields.image_url;
|
||||||
if (attachmentFieldName) {
|
if (attachmentFieldName) {
|
||||||
// Populate attachment when there's an image field
|
// Populate attachment when there's an image field
|
||||||
const populatedContents = await this.populateAttachments(
|
|
||||||
contents,
|
|
||||||
attachmentFieldName,
|
|
||||||
);
|
|
||||||
const flatContent = populatedContents.map((content) => ({
|
|
||||||
...content,
|
|
||||||
...content.dynamicFields,
|
|
||||||
dynamicFields: undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: flatContent,
|
elements: await this.populateAttachments(
|
||||||
|
elements,
|
||||||
|
attachmentFieldName,
|
||||||
|
),
|
||||||
pagination: {
|
pagination: {
|
||||||
total,
|
total,
|
||||||
skip,
|
skip,
|
||||||
@ -198,7 +192,7 @@ export class ContentService extends BaseService<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
elements: contents,
|
elements,
|
||||||
pagination: {
|
pagination: {
|
||||||
total,
|
total,
|
||||||
skip,
|
skip,
|
||||||
|
@ -31,6 +31,7 @@ import { WithUrl } from '@/chat/schemas/types/attachment';
|
|||||||
import { Button, ButtonType } from '@/chat/schemas/types/button';
|
import { Button, ButtonType } from '@/chat/schemas/types/button';
|
||||||
import {
|
import {
|
||||||
AnyMessage,
|
AnyMessage,
|
||||||
|
ContentElement,
|
||||||
FileType,
|
FileType,
|
||||||
IncomingMessage,
|
IncomingMessage,
|
||||||
OutgoingMessage,
|
OutgoingMessage,
|
||||||
@ -963,7 +964,10 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
*
|
*
|
||||||
* @returns An array of elements object
|
* @returns An array of elements object
|
||||||
*/
|
*/
|
||||||
_formatElements(data: any[], options: BlockOptions): Web.MessageElement[] {
|
_formatElements(
|
||||||
|
data: ContentElement[],
|
||||||
|
options: BlockOptions,
|
||||||
|
): Web.MessageElement[] {
|
||||||
if (!options.content || !options.content.fields) {
|
if (!options.content || !options.content.fields) {
|
||||||
throw new Error('Content options are missing the fields');
|
throw new Error('Content options are missing the fields');
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user