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",