Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add plugins settings i18n + typing #249

Merged
merged 2 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/src/channel/channel.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface ChannelModuleOptions {
folder: string;
}

@InjectDynamicProviders('dist/**/*.channel.js')
@InjectDynamicProviders('dist/extensions/**/*.channel.js')
@Module({
controllers: [WebhookController, ChannelController],
providers: [ChannelService],
Expand Down
3 changes: 2 additions & 1 deletion api/src/channel/lib/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { LoggerService } from '@/logger/logger.service';
import BaseNlpHelper from '@/nlp/lib/BaseNlpHelper';
import { NlpService } from '@/nlp/services/nlp.service';
import { SettingService } from '@/setting/services/setting.service';
import { hyphenToUnderscore } from '@/utils/helpers/misc';
import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';

Expand Down Expand Up @@ -56,7 +57,7 @@ export default abstract class ChannelHandler<N extends string = string> {
}

protected getGroup() {
return this.getChannel().replaceAll('-', '_') as ChannelSetting<N>['group'];
return hyphenToUnderscore(this.getChannel()) as ChannelSetting<N>['group'];
}

async setup() {
Expand Down
5 changes: 2 additions & 3 deletions api/src/chat/controllers/block.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class BlockController extends BaseController<
private readonly categoryService: CategoryService,
private readonly labelService: LabelService,
private readonly userService: UserService,
private pluginsService: PluginService<BaseBlockPlugin>,
private pluginsService: PluginService<BaseBlockPlugin<any>>,
) {
super(blockService);
}
Expand Down Expand Up @@ -122,8 +122,7 @@ export class BlockController extends BaseController<
const plugins = this.pluginsService
.getAllByType(PluginType.block)
.map((p) => ({
title: p.title,
name: p.id,
id: p.id,
template: {
...p.template,
message: {
Expand Down
19 changes: 19 additions & 0 deletions api/src/chat/services/message.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,23 @@ export class MessageService extends BaseService<
limit,
);
}

/**
* Retrieves the latest messages for a given subscriber
*
* @param subscriber - The subscriber whose message history is being retrieved.
* @param limit - The maximum number of messages to return (defaults to 5).
*
* @returns The message history since the specified date.
*/
async findLastMessages(subscriber: Subscriber, limit: number = 5) {
const lastMessages = await this.findPage(
{
$or: [{ sender: subscriber.id }, { recipient: subscriber.id }],
},
{ sort: ['createdAt', 'desc'], skip: 0, limit },
);

return lastMessages.reverse();
}
}
Empty file.
62 changes: 31 additions & 31 deletions api/src/i18n/services/i18n.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { IfAnyOrNever } from 'nestjs-i18n/dist/types';

import { config } from '@/config';
import { Translation } from '@/i18n/schemas/translation.schema';
import { hyphenToUnderscore } from '@/utils/helpers/misc';

@Injectable()
export class I18nService<K = Record<string, unknown>>
Expand Down Expand Up @@ -84,41 +85,40 @@ export class I18nService<K = Record<string, unknown>>
}

async loadExtensionI18nTranslations() {
const extensionsDir = path.join(
__dirname,
'..',
'..',
'extensions',
'channels',
);
const baseDir = path.join(__dirname, '..', '..', 'extensions');
const extensionTypes = ['channels', 'plugins'];

try {
const extensionFolders = await fs.readdir(extensionsDir, {
withFileTypes: true,
});
for (const type of extensionTypes) {
const extensionsDir = path.join(baseDir, type);
const extensionFolders = await fs.readdir(extensionsDir, {
withFileTypes: true,
});

for (const folder of extensionFolders) {
if (folder.isDirectory()) {
const i18nPath = path.join(extensionsDir, folder.name, 'i18n');
const namespace = hyphenToUnderscore(folder.name);
try {
// Check if the i18n directory exists
await fs.access(i18nPath);

for (const folder of extensionFolders) {
if (folder.isDirectory()) {
const i18nPath = path.join(extensionsDir, folder.name, 'i18n');
const extensionName = folder.name.replaceAll('-', '_');
try {
// Check if the i18n directory exists
await fs.access(i18nPath);

// Load and merge translations
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
const translations = await i18nLoader.load();
for (const lang in translations) {
if (!this.extensionTranslations[lang]) {
this.extensionTranslations[lang] = {
[extensionName]: translations[lang],
};
} else {
this.extensionTranslations[lang][extensionName] =
translations[lang];
// Load and merge translations
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
const translations = await i18nLoader.load();
for (const lang in translations) {
if (!this.extensionTranslations[lang]) {
this.extensionTranslations[lang] = {
[namespace]: translations[lang],
};
} else {
this.extensionTranslations[lang][namespace] =
translations[lang];
}
}
} catch (error) {
// If the i18n folder does not exist or error in reading, skip this folder
}
} catch (error) {
// If the i18n folder does not exist or error in reading, skip this folder
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/nlp/nlp.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { NlpSampleService } from './services/nlp-sample.service';
import { NlpValueService } from './services/nlp-value.service';
import { NlpService } from './services/nlp.service';

@InjectDynamicProviders('dist/**/*.nlp.helper.js')
@InjectDynamicProviders('dist/extensions/**/*.nlp.helper.js')
@Module({
imports: [
MongooseModule.forFeature([
Expand Down
24 changes: 18 additions & 6 deletions api/src/plugins/base-block-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ import {
} from './types';

@Injectable()
export abstract class BaseBlockPlugin extends BasePlugin {
export abstract class BaseBlockPlugin<
T extends PluginSetting[],
> extends BasePlugin {
public readonly type: PluginType = PluginType.block;

constructor(id: string, pluginService: PluginService<BasePlugin>) {
public readonly settings: T;

constructor(
id: string,
settings: T,
pluginService: PluginService<BasePlugin>,
) {
super(id, pluginService);
this.settings = settings;
}

title: string;

settings: PluginSetting[];

template: PluginBlockTemplate;

effects?: PluginEffects;
Expand All @@ -42,4 +47,11 @@ export abstract class BaseBlockPlugin extends BasePlugin {
context: Context,
convId?: string,
): Promise<StdOutgoingEnvelope>;

protected getArguments(block: Block) {
if ('args' in block.message) {
return block.message.args as SettingObject<T>;
}
throw new Error(`Block "${block.name}" does not have any arguments.`);
}
}
2 changes: 1 addition & 1 deletion api/src/plugins/plugins.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { ContentModel } from '@/cms/schemas/content.schema';

import { PluginService } from './plugins.service';

@InjectDynamicProviders('dist/**/*.plugin.js')
@InjectDynamicProviders('dist/extensions/**/*.plugin.js')
@Global()
@Module({
imports: [
Expand Down
2 changes: 1 addition & 1 deletion api/src/plugins/plugins.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import { Test } from '@nestjs/testing';

import { DummyPlugin } from '@/extensions/plugins/dummy.plugin';
import { LoggerModule } from '@/logger/logger.module';
import { DummyPlugin } from '@/utils/test/dummy/dummy.plugin';

import { BaseBlockPlugin } from './base-block-plugin';
import { PluginService } from './plugins.service';
Expand Down
2 changes: 1 addition & 1 deletion api/src/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface CustomBlocks {}
type ChannelEvent = any;
type BlockAttrs = Partial<BlockCreateDto> & { name: string };

export type PluginSetting = SettingCreateDto;
export type PluginSetting = Omit<SettingCreateDto, 'weight'>;

export type PluginBlockTemplate = Omit<
BlockAttrs,
Expand Down
4 changes: 4 additions & 0 deletions api/src/utils/helpers/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@
export const isEmpty = (value: string): boolean => {
return value === undefined || value === null || value === '';
};

export const hyphenToUnderscore = (str: string) => {
return str.replaceAll('-', '_');
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@ import {
import { LoggerService } from '@/logger/logger.service';
import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
import { PluginService } from '@/plugins/plugins.service';
import { PluginSetting } from '@/plugins/types';

@Injectable()
export class DummyPlugin extends BaseBlockPlugin {
export class DummyPlugin extends BaseBlockPlugin<PluginSetting[]> {
constructor(
pluginService: PluginService,
private logger: LoggerService,
) {
super('dummy', pluginService);

this.title = 'Dummy';

this.settings = [];
super('dummy', [], pluginService);

this.template = { name: 'Dummy Plugin' };

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/settings/SettingInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ import { MIME_TYPES } from "@/utils/attachment";
interface RenderSettingInputProps {
setting: ISetting;
field: ControllerRenderProps<any, string>;
ns: string;
isDisabled?: (setting: ISetting) => boolean;
}

const SettingInput: React.FC<RenderSettingInputProps> = ({
setting,
field,
ns,
isDisabled = () => false,
}) => {
const { t } = useTranslate(setting.group);
const { t } = useTranslate(ns);
const label = t(`label.${setting.label}`, {
defaultValue: setting.label,
});
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export const Settings = () => {
<SettingInput
setting={setting}
field={field}
ns={setting.group}
isDisabled={isDisabled}
/>
</FormControl>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/visual-editor/CustomBlocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const CustomBlocks = () => {
<Grid container>
{customBlocks?.map((customBlock) => (
<Block
key={customBlock.name}
title={customBlock.title}
key={customBlock.id}
title={t(`title.${customBlock.id}`, { ns: customBlock.id })}
Icon={PluginIcon}
blockTemplate={customBlock.template}
name={customBlock.template.name}
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/components/visual-editor/form/OptionsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,19 @@ export const OptionsForm = () => {
const { onChange, ...rest } = field;

return (
<AutoCompleteEntitySelect<ICustomBlockTemplate, "title">
searchFields={["title", "name"]}
<AutoCompleteEntitySelect<ICustomBlockTemplate, "id">
searchFields={["id"]}
entity={EntityType.CUSTOM_BLOCK}
format={Format.BASIC}
idKey="name"
labelKey="title"
idKey="id"
labelKey="id"
label={t("label.effects")}
multiple={true}
getOptionLabel={(option) => {
return t(`title.${option.id}`, { ns: option.id });
}}
onChange={(_e, selected) =>
onChange(selected.map(({ name }) => name))
onChange(selected.map(({ id }) => id))
}
preprocess={(options) => {
return options.filter(({ effects }) => effects.length > 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ const PluginMessageForm = () => {
defaultValue={message.args?.[setting.label] || setting.value}
render={({ field }) => (
<FormControl fullWidth sx={{ paddingTop: ".75rem" }}>
<SettingInput setting={setting} field={field} />
<SettingInput
setting={setting}
field={field}
ns={message.plugin}
/>
</FormControl>
)}
/>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/services/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export const CustomBlockEntity = new schema.Entity(
EntityType.CUSTOM_BLOCK,
undefined,
{
idAttribute: ({ name }) => name,
idAttribute: ({ id }) => id,
},
);

Expand Down
10 changes: 4 additions & 6 deletions frontend/src/types/block.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import { EntityType, Format } from "@/services/types";
import { IBaseSchema, IFormat, OmitPopulate } from "./base.types";
import { ILabel } from "./label.types";
import {
StdOutgoingQuickRepliesMessage,
AttachmentForeignKey,
ContentOptions,
PayloadType,
StdOutgoingAttachmentMessage,
StdOutgoingButtonsMessage,
StdOutgoingListMessage,
StdOutgoingQuickRepliesMessage,
StdOutgoingTextMessage,
AttachmentForeignKey,
StdPluginMessage,
ContentOptions,
PayloadType,
} from "./message.types";
import { IUser } from "./user.types";

Expand Down Expand Up @@ -132,8 +132,6 @@ export interface IBlockFull extends IBlockStub, IFormat<Format.FULL> {
}

export interface ICustomBlockTemplateAttributes {
title: string;
name: string;
template: IBlockAttributes;
effects: string[];
}
Expand Down