diff --git a/src/app/plugins/ui/plugin-management/plugin-management.component.html b/src/app/plugins/ui/plugin-management/plugin-management.component.html index 35d1701fcb..03fe483ccc 100644 --- a/src/app/plugins/ui/plugin-management/plugin-management.component.html +++ b/src/app/plugins/ui/plugin-management/plugin-management.component.html @@ -87,7 +87,12 @@ {{ plugin.manifest.name }} - v{{ plugin.manifest.version }} + v{{ plugin.manifest.version }} + @if (getPluginAuthor(plugin); as author) { + + {{ T.PLUGINS.AUTHORED_BY | translate: { author } }} + + } @if (isPluginLoading(plugin)) { autorenew diff --git a/src/app/plugins/ui/plugin-management/plugin-management.component.scss b/src/app/plugins/ui/plugin-management/plugin-management.component.scss index 1441a2fd46..d1e369fb82 100644 --- a/src/app/plugins/ui/plugin-management/plugin-management.component.scss +++ b/src/app/plugins/ui/plugin-management/plugin-management.component.scss @@ -43,6 +43,7 @@ plugin-icon { mat-card-subtitle { display: flex; align-items: center; + flex-wrap: wrap; gap: var(--s); mat-chip { @@ -50,6 +51,10 @@ mat-card-subtitle { } } +.plugin-author { + overflow-wrap: anywhere; +} + .empty-state { text-align: center; padding: var(--s6) var(--s3); diff --git a/src/app/plugins/ui/plugin-management/plugin-management.component.spec.ts b/src/app/plugins/ui/plugin-management/plugin-management.component.spec.ts new file mode 100644 index 0000000000..3253e3ff59 --- /dev/null +++ b/src/app/plugins/ui/plugin-management/plugin-management.component.spec.ts @@ -0,0 +1,116 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { TranslateModule } from '@ngx-translate/core'; +import { GlobalConfigService } from '../../../features/config/global-config.service'; +import { PluginBridgeService } from '../../plugin-bridge.service'; +import { PluginCacheService } from '../../plugin-cache.service'; +import { PluginConfigService } from '../../plugin-config.service'; +import { PluginMetaPersistenceService } from '../../plugin-meta-persistence.service'; +import { PluginManifest } from '../../plugin-api.model'; +import { PluginService } from '../../plugin.service'; +import { PluginManagementComponent } from './plugin-management.component'; + +type PluginManifestWithAuthor = PluginManifest & { author?: string }; + +describe('PluginManagementComponent', () => { + let component: PluginManagementComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PluginManagementComponent, TranslateModule.forRoot()], + providers: [ + { + provide: PluginService, + useValue: { + pluginStates: signal(new Map()), + }, + }, + { + provide: PluginMetaPersistenceService, + useValue: {}, + }, + { + provide: PluginCacheService, + useValue: {}, + }, + { + provide: PluginConfigService, + useValue: {}, + }, + { + provide: GlobalConfigService, + useValue: { localization: signal({ lng: 'en' }) }, + }, + { + provide: MatDialog, + useValue: {}, + }, + { + provide: Store, + useValue: { selectSignal: () => signal([]) }, + }, + { + provide: PluginBridgeService, + useValue: {}, + }, + ], + }); + + component = TestBed.createComponent(PluginManagementComponent).componentInstance; + }); + + it('returns trimmed plugin author from the manifest', () => { + const manifest: PluginManifestWithAuthor = { + id: 'test-plugin', + name: 'Test Plugin', + manifestVersion: 1, + version: '1.0.0', + minSupVersion: '1.0.0', + hooks: [], + permissions: [], + author: ' Super Productivity ', + }; + + expect( + component.getPluginAuthor({ + manifest, + loaded: false, + isEnabled: false, + }), + ).toBe('Super Productivity'); + }); + + it('hides missing or blank plugin authors', () => { + const manifest: PluginManifestWithAuthor = { + id: 'test-plugin', + name: 'Test Plugin', + manifestVersion: 1, + version: '1.0.0', + minSupVersion: '1.0.0', + hooks: [], + permissions: [], + }; + const blankAuthorManifest: PluginManifestWithAuthor = { + ...manifest, + author: ' ', + }; + + expect( + component.getPluginAuthor({ + manifest, + loaded: false, + isEnabled: false, + }), + ).toBeNull(); + + expect( + component.getPluginAuthor({ + manifest: blankAuthorManifest, + loaded: false, + isEnabled: false, + }), + ).toBeNull(); + }); +}); diff --git a/src/app/plugins/ui/plugin-management/plugin-management.component.ts b/src/app/plugins/ui/plugin-management/plugin-management.component.ts index 9f5d73baaf..71c9dead47 100644 --- a/src/app/plugins/ui/plugin-management/plugin-management.component.ts +++ b/src/app/plugins/ui/plugin-management/plugin-management.component.ts @@ -50,6 +50,10 @@ interface CommunityPlugin { stars?: number; } +interface PluginManifestAuthor { + author?: unknown; +} + @Component({ selector: 'plugin-management', templateUrl: './plugin-management.component.html', @@ -257,6 +261,11 @@ export class PluginManagementComponent { return !plugin.error && (!this.requiresNodeExecution(plugin) || IS_ELECTRON); } + getPluginAuthor(plugin: PluginInstance): string | null { + const author = (plugin.manifest as PluginManifestAuthor).author; + return typeof author === 'string' && author.trim().length > 0 ? author.trim() : null; + } + getNodeExecutionMessage(): string { return this._translateService.instant('PLUGINS.NODE_EXECUTION_REQUIRED'); } diff --git a/src/app/t.const.ts b/src/app/t.const.ts index fda2b8f676..e2a4b4322b 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -2851,6 +2851,7 @@ const T = { PLUGINS: { ACTION_TYPE_NOT_ALLOWED: 'PLUGINS.ACTION_TYPE_NOT_ALLOWED', ALREADY_INITIALIZED: 'PLUGINS.ALREADY_INITIALIZED', + AUTHORED_BY: 'PLUGINS.AUTHORED_BY', CANCEL: 'PLUGINS.CANCEL', CAPABILITIES: { ACCESS_FILES: 'PLUGINS.CAPABILITIES.ACCESS_FILES', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 65ee9e162a..56ee1bb495 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2788,6 +2788,7 @@ "PLUGINS": { "ACTION_TYPE_NOT_ALLOWED": "Action type \"{{type}}\" is not allowed", "ALREADY_INITIALIZED": "Plugin is already initialized", + "AUTHORED_BY": "by {{author}}", "CANCEL": "Cancel", "CAPABILITIES": { "ACCESS_FILES": "Access and modify files on your system",