fix(plugins): show plugin authors (#8190)

* fix(plugins): show plugin authors

* fix(plugins): translate plugin author label

---------

Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
This commit is contained in:
felix bear 2026-06-09 22:44:17 +09:00 committed by GitHub
parent b729fd941c
commit 72538c18d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 138 additions and 1 deletions

View file

@ -87,7 +87,12 @@
</div>
<mat-card-title>{{ plugin.manifest.name }}</mat-card-title>
<mat-card-subtitle>
v{{ plugin.manifest.version }}
<span>v{{ plugin.manifest.version }}</span>
@if (getPluginAuthor(plugin); as author) {
<span class="plugin-author">
{{ T.PLUGINS.AUTHORED_BY | translate: { author } }}
</span>
}
@if (isPluginLoading(plugin)) {
<mat-chip color="accent">
<mat-icon class="loading-icon">autorenew</mat-icon>

View file

@ -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);

View file

@ -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();
});
});

View file

@ -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');
}

View file

@ -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',

View file

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