mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
Feat/plugin system (#7951)
* feat: create plugin interface * feat: create plugin metadata interface * feat: create plugin manager interface * feat: create plugin download strategy interface * feat: define plugin channels * feat: add constants * feat: create plugin download context types * feat: create plugin download context * feat: create metadata service * feat: create a migration for plugins * feat: implement CDN download strategy * feat: implement donload context factory * feat: implement plugin manager * feat: implement plugin event listeners * feat: add pluglin listenners to all desktop apps * feat: create and update index.ts * feat: Updated nx.json and yarn.lock files with changes to dependencies and versions. * Update error message in DownloadContextFactory from 'Uknown context' to 'ERROR::Unknown strategy' * Added 'main' and 'pathname' columns to 'plugins' table * chore(deps): add unzipper package * feat: improve no data message module * feat: add new methods to plugin manager and handle invokers * feat: implement plugin renderer * Update error message in AddPluginComponent * Refactor: PluginManager to use lazy loader, update plugin path, and fix CDN download strategy * Rename PluginHandlerChannel enum values from 'plugin::' to 'plugins:: * feat: update styles for .error class and nb-card in add-plugin.component.scss * Refactor: PluginMetadataService, ElectronPluginListener, PluginListComponent, and PluginElectronService: update database queries, event handling, and IPC communication * Updated plugin management components, services, and translations in desktop-ui-lib and ui-core packages. * Changed lazyLoader to return module.default instead of module * Updated PluginListComponent to set plugin to null after activation and view, and refactored PluginElectronService to properly handle ipcRenderer event listeners. * refactor: desktop-libs and desktop-ui-lib: update plugin listeners, plugin manager, and plugin list component * feat: added 'renderer' field to plugin metadata, updated plugin manager and plugin loader services, and modified plugin list and plugin components to handle renderer field * feat: added ngIf condition to 'button-view' to only render when 'plugin.renderer' is truthy * Update plugin database migration to make renderer column nullable, and modify PluginManager to set process.noAsar during download and update plugin operations, and use force option when copying files. * Refactor PluginManager and TimeTrackerComponent: update plugin uninstallation, remove unnecessary imports, and add new imports and functionality to TimeTrackerComponent * feat: added uninstall method to PluginListComponent and updated PluginElectronService to send plugin name instead of entire plugin object * feat: updated plugin list component HTML and TS files: renamed 'remove' to 'uninstall' button, refactored plugin loading and observation, and made minor adjustments to code organization and syntax. * refactory: moved pluginListeners() call from app.whenReady() to ipcMainHandler function * fix: PluginManager to use fs.rmSync instead of fs.rmdirSync, and fix minor issues in PluginListComponent" * fix: cspell spelling * fix: cspell spelling
This commit is contained in:
@@ -1,25 +1,21 @@
|
||||
import { RouterModule, Routes, ExtraOptions } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
|
||||
import {
|
||||
ImageViewerComponent,
|
||||
SettingsComponent,
|
||||
ScreenCaptureComponent,
|
||||
TimeTrackerComponent,
|
||||
SetupComponent,
|
||||
UpdaterComponent,
|
||||
SplashScreenComponent,
|
||||
NgxLoginComponent,
|
||||
AboutComponent,
|
||||
AlwaysOnComponent,
|
||||
AuthGuard,
|
||||
ImageViewerComponent,
|
||||
NgxLoginComponent,
|
||||
NoAuthGuard,
|
||||
ScreenCaptureComponent,
|
||||
ServerDownPage,
|
||||
AlwaysOnComponent
|
||||
SettingsComponent,
|
||||
SetupComponent,
|
||||
SplashScreenComponent,
|
||||
TimeTrackerComponent,
|
||||
UpdaterComponent
|
||||
} from '@gauzy/desktop-ui-lib';
|
||||
import {
|
||||
NbAuthComponent,
|
||||
NbRequestPasswordComponent,
|
||||
NbResetPasswordComponent
|
||||
} from '@nebular/auth';
|
||||
import { NbAuthComponent, NbRequestPasswordComponent, NbResetPasswordComponent } from '@nebular/auth';
|
||||
import { AppModuleGuard } from './app.module.guards';
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -68,7 +64,8 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent
|
||||
component: SettingsComponent,
|
||||
loadChildren: () => import('@gauzy/desktop-ui-lib').then((m) => m.pluginRoutes)
|
||||
},
|
||||
{
|
||||
path: 'updater',
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
"default": {
|
||||
"runner": "nx-cloud",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "lint", "test", "e2e"],
|
||||
"cacheableOperations": [
|
||||
"build",
|
||||
"lint",
|
||||
"test",
|
||||
"e2e"
|
||||
],
|
||||
"parallel": 1,
|
||||
"accessToken": "NWJlMDFmMTMtYzUyZS00ZGE4LTk0NzctNWIzZTkxMzNhMTFlfHJlYWQtd3JpdGU="
|
||||
}
|
||||
|
||||
@@ -52,10 +52,12 @@
|
||||
"sound-play": "1.1.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"underscore": "^1.13.3",
|
||||
"undici": "^6.10.2"
|
||||
"undici": "^6.10.2",
|
||||
"unzipper": "^0.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/unzipper": "^0.10.9",
|
||||
"electron": "28.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
export * from './lib/desktop-timer';
|
||||
export * from './lib/desktop-notifier';
|
||||
export * from './lib/desktop-store';
|
||||
export * from './lib/desktop-screenshot';
|
||||
export * from './lib/desktop-ipc';
|
||||
export * from './lib/desktop-tray';
|
||||
export * from './lib/desktop-wakatime';
|
||||
export * from './lib/desktop-menu';
|
||||
export * from './lib/desktop-server';
|
||||
export * from './lib/decorators';
|
||||
export * from './lib/interfaces';
|
||||
export * from './lib/desktop-dialog';
|
||||
export * from './lib/contexts';
|
||||
export * from './lib/strategies';
|
||||
export * from './lib/config';
|
||||
export * from './lib/update-server/desktop-local-update-server';
|
||||
export * from './lib/contexts';
|
||||
export * from './lib/decorators';
|
||||
export * from './lib/desktop-dialog';
|
||||
export * from './lib/desktop-ipc';
|
||||
export * from './lib/desktop-menu';
|
||||
export * from './lib/desktop-notifier';
|
||||
export * from './lib/desktop-screenshot';
|
||||
export * from './lib/desktop-server';
|
||||
export * from './lib/desktop-store';
|
||||
export * from './lib/desktop-timer';
|
||||
export * from './lib/desktop-tray';
|
||||
export * from './lib/desktop-updater';
|
||||
export * from './lib/offline/desktop-offline-mode-handler';
|
||||
export * from './lib/states';
|
||||
export * from './lib/offline';
|
||||
export * from './lib/translation';
|
||||
export * from './lib/server';
|
||||
export * from './lib/desktop-wakatime';
|
||||
export * from './lib/error-handler';
|
||||
export * from './lib/integrations';
|
||||
export * from './lib/interfaces';
|
||||
export * from './lib/offline';
|
||||
export * from './lib/offline/desktop-offline-mode-handler';
|
||||
export * from './lib/plugin-system';
|
||||
export * from './lib/server';
|
||||
export * from './lib/states';
|
||||
export * from './lib/strategies';
|
||||
export * from './lib/translation';
|
||||
export * from './lib/update-server/desktop-local-update-server';
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { BrowserWindow, ipcMain, screen, desktopCapturer, app, systemPreferences } from 'electron';
|
||||
import TimerHandler from './desktop-timer';
|
||||
import moment from 'moment';
|
||||
import { LocalStore } from './desktop-store';
|
||||
import { notifyScreenshot, takeshot } from './desktop-screenshot';
|
||||
import { resetPermissions } from 'mac-screen-capture-permissions';
|
||||
import * as _ from 'underscore';
|
||||
import { IActivityWatchEventResult } from '@gauzy/contracts';
|
||||
import { ScreenCaptureNotification, loginPage } from '@gauzy/desktop-window';
|
||||
import NotificationDesktop from './desktop-notifier';
|
||||
import { DesktopPowerManager } from './desktop-power-manager';
|
||||
import { PowerManagerPreventDisplaySleep, PowerManagerDetectInactivity } from './decorators';
|
||||
import { DesktopOsInactivityHandler } from './desktop-os-inactivity-handler';
|
||||
import { DesktopOfflineModeHandler, IntervalTO } from './offline';
|
||||
import { Interval, IntervalService, Timer, TimerService, TimerTO, UserService } from './offline';
|
||||
import { DialogStopTimerLogoutConfirmation } from './decorators';
|
||||
import { DesktopDialog } from './desktop-dialog';
|
||||
import { TranslateService } from './translation';
|
||||
import { IPowerManager, IDesktopEvent } from './interfaces';
|
||||
import { BrowserWindow, app, desktopCapturer, ipcMain, screen, systemPreferences } from 'electron';
|
||||
import log from 'electron-log';
|
||||
import { resetPermissions } from 'mac-screen-capture-permissions';
|
||||
import moment from 'moment';
|
||||
import * as _ from 'underscore';
|
||||
import { SleepInactivityTracking } from './contexts';
|
||||
import { RemoteSleepTracking } from './strategies';
|
||||
import {
|
||||
DialogStopTimerLogoutConfirmation,
|
||||
PowerManagerDetectInactivity,
|
||||
PowerManagerPreventDisplaySleep
|
||||
} from './decorators';
|
||||
import { DesktopDialog } from './desktop-dialog';
|
||||
import NotificationDesktop from './desktop-notifier';
|
||||
import { DesktopOsInactivityHandler } from './desktop-os-inactivity-handler';
|
||||
import { DesktopPowerManager } from './desktop-power-manager';
|
||||
import { notifyScreenshot, takeshot } from './desktop-screenshot';
|
||||
import { LocalStore } from './desktop-store';
|
||||
import TimerHandler from './desktop-timer';
|
||||
import { UIError } from './error-handler';
|
||||
import {
|
||||
ActivityWatchAfkService,
|
||||
@@ -25,11 +25,23 @@ import {
|
||||
ActivityWatchEventManager,
|
||||
ActivityWatchEventTableList
|
||||
} from './integrations';
|
||||
import { IActivityWatchEventResult } from '@gauzy/contracts';
|
||||
import { IDesktopEvent, IPowerManager } from './interfaces';
|
||||
import {
|
||||
DesktopOfflineModeHandler,
|
||||
Interval,
|
||||
IntervalService,
|
||||
IntervalTO,
|
||||
Timer,
|
||||
TimerService,
|
||||
TimerTO,
|
||||
UserService
|
||||
} from './offline';
|
||||
import { pluginListeners } from './plugin-system';
|
||||
import { RemoteSleepTracking } from './strategies';
|
||||
import { TranslateService } from './translation';
|
||||
|
||||
const timerHandler = new TimerHandler();
|
||||
|
||||
import log from 'electron-log';
|
||||
console.log = log.log;
|
||||
Object.assign(console, log.functions);
|
||||
|
||||
@@ -343,6 +355,8 @@ export function ipcMainHandler(store, startServer, knex, config, timeTrackerWind
|
||||
log.error('Error on finish synced timer', error);
|
||||
}
|
||||
});
|
||||
|
||||
pluginListeners();
|
||||
}
|
||||
|
||||
function isScreenUnauthorized() {
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Knex } from 'knex';
|
||||
import { TABLE_PLUGINS } from '../../../plugin-system';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TABLE_PLUGINS);
|
||||
await knex.schema.createTable(TABLE_PLUGINS, (table: Knex.TableBuilder) => {
|
||||
table.increments('id').primary().notNullable().unique();
|
||||
table.string('name').notNullable();
|
||||
table.string('main').notNullable();
|
||||
table.string('renderer').nullable();
|
||||
table.string('pathname').notNullable();
|
||||
table.string('version').notNullable();
|
||||
table.boolean('isActivate').defaultTo(false);
|
||||
table.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.dropTable(TABLE_PLUGINS);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IPluginDownloadStrategy, PluginDownloadContext, PluginDownloadContextType } from '../shared';
|
||||
import { CdnDownloadStrategy } from './strategies/cdn-download.strategy';
|
||||
|
||||
export class DownloadContextFactory {
|
||||
public static getContext(name = PluginDownloadContextType.CDN) {
|
||||
let strategy: IPluginDownloadStrategy;
|
||||
switch (name) {
|
||||
case PluginDownloadContextType.CDN:
|
||||
strategy = new CdnDownloadStrategy();
|
||||
break;
|
||||
//TODO: Add more strategies later
|
||||
default:
|
||||
throw new Error('ERROR::Unknown strategy');
|
||||
}
|
||||
|
||||
return new PluginDownloadContext(strategy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { app } from 'electron';
|
||||
import * as logger from 'electron-log';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { PluginMetadataService } from '../database/plugin-metadata.service';
|
||||
import { IPlugin, IPluginManager, IPluginMetadata, PluginDownloadContextType } from '../shared';
|
||||
import { lazyLoader } from '../shared/lazy-loader';
|
||||
import { DownloadContextFactory } from './download-context.factory';
|
||||
|
||||
export class PluginManager implements IPluginManager {
|
||||
private plugins: Map<string, IPlugin> = new Map();
|
||||
private activePlugins: Set<string> = new Set();
|
||||
private pluginMetadataService = new PluginMetadataService();
|
||||
private pluginPath = path.join(app.getPath('userData'), 'plugins');
|
||||
private factory = DownloadContextFactory;
|
||||
|
||||
public async downloadPlugin<U>(config: U, contextType?: PluginDownloadContextType): Promise<void> {
|
||||
logger.info(`Downloading plugin...`);
|
||||
process.noAsar = true;
|
||||
const context = this.factory.getContext(contextType);
|
||||
const { metadata, pathDirname } = await context.execute(config);
|
||||
const plugin = this.plugins.get(metadata.name);
|
||||
if (plugin) {
|
||||
await this.updatePlugin(metadata);
|
||||
} else {
|
||||
await this.installPlugin(metadata, pathDirname);
|
||||
}
|
||||
fs.rmSync(pathDirname, { recursive: true, force: true });
|
||||
process.noAsar = false;
|
||||
}
|
||||
|
||||
public async updatePlugin(pluginMetadata: IPluginMetadata): Promise<void> {
|
||||
logger.info(`Update plugin ${pluginMetadata.name}`);
|
||||
const persistance = await this.pluginMetadataService.findOne({ name: pluginMetadata.name });
|
||||
|
||||
if (pluginMetadata.version === persistance.version) {
|
||||
logger.info(`Version is already update`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (persistance.isActivate) {
|
||||
this.deactivatePlugin(pluginMetadata.name);
|
||||
}
|
||||
const pluginPath = persistance.pathname;
|
||||
const backupPath = persistance.pathname + '-backup';
|
||||
fs.renameSync(pluginPath, backupPath);
|
||||
|
||||
if (!fs.existsSync(pluginPath)) {
|
||||
fs.mkdirSync(pluginPath, { recursive: true });
|
||||
}
|
||||
logger.info(`Updating plugin ${pluginMetadata.name}`);
|
||||
const plugin = await lazyLoader(path.join(pluginPath, pluginMetadata.main));
|
||||
this.plugins.set(pluginMetadata.name, plugin);
|
||||
|
||||
await this.pluginMetadataService.update({
|
||||
name: pluginMetadata.name,
|
||||
version: pluginMetadata.version
|
||||
});
|
||||
|
||||
if (persistance.isActivate) {
|
||||
this.activatePlugin(pluginMetadata.name);
|
||||
}
|
||||
}
|
||||
|
||||
public async installPlugin(pluginMetadata: IPluginMetadata, source: string): Promise<void> {
|
||||
const pluginDir = path.join(this.pluginPath, `${Date.now()}-${pluginMetadata.name}`);
|
||||
if (!fs.existsSync(pluginDir)) {
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
}
|
||||
if (source) {
|
||||
fs.cpSync(source, pluginDir, { recursive: true, force: true });
|
||||
}
|
||||
logger.info(`Installing plugin ${pluginMetadata.name}`);
|
||||
const plugin = await lazyLoader(path.join(pluginDir, pluginMetadata.main));
|
||||
this.plugins.set(pluginMetadata.name, plugin);
|
||||
|
||||
await this.pluginMetadataService.create({
|
||||
name: pluginMetadata.name,
|
||||
version: pluginMetadata.version,
|
||||
main: pluginMetadata.main,
|
||||
renderer: pluginMetadata.renderer,
|
||||
pathname: pluginDir
|
||||
});
|
||||
}
|
||||
|
||||
public async activatePlugin(name: string): Promise<void> {
|
||||
const plugin = this.plugins.get(name);
|
||||
if (plugin) {
|
||||
plugin.activate();
|
||||
this.activePlugins.add(name);
|
||||
await this.pluginMetadataService.update({ name, isActivate: true });
|
||||
plugin.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
public async deactivatePlugin(name: string): Promise<void> {
|
||||
const plugin = this.plugins.get(name);
|
||||
if (plugin) {
|
||||
plugin.dispose();
|
||||
plugin.deactivate();
|
||||
this.activePlugins.delete(name);
|
||||
await this.pluginMetadataService.update({ name, isActivate: false });
|
||||
}
|
||||
}
|
||||
|
||||
public async uninstallPlugin(name: string): Promise<void> {
|
||||
const metadata = await this.pluginMetadataService.findOne({ name });
|
||||
const plugin = this.plugins.get(name);
|
||||
if (plugin) {
|
||||
await this.deactivatePlugin(name);
|
||||
this.plugins.delete(name);
|
||||
await this.pluginMetadataService.delete({ name });
|
||||
fs.rmSync(metadata.pathname, { recursive: true, force: true });
|
||||
logger.info(`Uninstalling plugin ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async loadPlugins(): Promise<void> {
|
||||
logger.info('Loading plugins...');
|
||||
const pluginMetadatas = await this.pluginMetadataService.findAll();
|
||||
|
||||
for (const metadata of pluginMetadatas) {
|
||||
const plugin = await lazyLoader(path.join(metadata.pathname, metadata.main));
|
||||
this.plugins.set(metadata.name, plugin);
|
||||
|
||||
if (metadata.isActivate) {
|
||||
await this.activatePlugin(metadata.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getAllPlugins(): Promise<IPluginMetadata[]> {
|
||||
return this.pluginMetadataService.findAll();
|
||||
}
|
||||
|
||||
public getOnePlugin(name: string): Promise<IPluginMetadata> {
|
||||
return this.pluginMetadataService.findOne({ name });
|
||||
}
|
||||
|
||||
public initializePlugins(): void {
|
||||
this.plugins.forEach((plugin) => plugin.initialize());
|
||||
}
|
||||
|
||||
public disposePlugins(): void {
|
||||
this.plugins.forEach((plugin) => plugin.dispose());
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { app } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
import path, { join } from 'path';
|
||||
import * as unzipper from 'unzipper';
|
||||
import { ICdnDownloadConfig, IPluginDownloadResponse, IPluginDownloadStrategy } from '../../shared';
|
||||
|
||||
export class CdnDownloadStrategy implements IPluginDownloadStrategy {
|
||||
async execute<T>(config: T): Promise<IPluginDownloadResponse> {
|
||||
const tempDir = join(app.getPath('userData'), 'temp');
|
||||
const { url } = config as ICdnDownloadConfig;
|
||||
|
||||
try {
|
||||
// Ensure the temp directory exists
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
console.log('✔ Temp directory verified/created');
|
||||
|
||||
// Fetch the file
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download the file: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const buffer = await response.buffer();
|
||||
const fileName = path.basename(url);
|
||||
const ext = path.extname(url);
|
||||
const zipFilePath = path.join(tempDir, fileName);
|
||||
|
||||
if (ext !== '.zip') {
|
||||
throw new Error('The plugin should be inside a .zip file');
|
||||
}
|
||||
|
||||
// Write the file to the temp directory
|
||||
fs.writeFileSync(zipFilePath, buffer);
|
||||
console.log(`✔ Plugin ${fileName} downloaded successfully`);
|
||||
|
||||
// Unzip the file
|
||||
const tempDirPath = await this.unzip(zipFilePath, tempDir);
|
||||
|
||||
// Set Path Dirname
|
||||
const pathDirname = join(tempDirPath, fileName.replace(/\.zip$/, ''));
|
||||
|
||||
// Read and parse the metadata
|
||||
const metadata = JSON.parse(
|
||||
fs.readFileSync(join(pathDirname, 'manifest.json'), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
);
|
||||
return { pathDirname, metadata };
|
||||
} catch (error) {
|
||||
console.error(`✖ Error during download and extraction: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async unzip(filePath: string, extractDir: string): Promise<string> {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
fs.createReadStream(filePath)
|
||||
.pipe(unzipper.Extract({ path: extractDir }))
|
||||
.on('close', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
console.log('✔ File unzipped successfully');
|
||||
return extractDir;
|
||||
} catch (error) {
|
||||
console.error(`✖ Error during unzip: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { IDatabaseProvider } from '../../interfaces';
|
||||
import { ProviderFactory } from '../../offline';
|
||||
import {
|
||||
IPluginMetadataCreate,
|
||||
IPluginMetadataDelete,
|
||||
IPluginMetadataFindOne,
|
||||
IPluginMetadataPersistance,
|
||||
IPluginMetadataUpdate,
|
||||
TABLE_PLUGINS
|
||||
} from '../shared';
|
||||
|
||||
export class PluginMetadataService {
|
||||
private readonly db: IDatabaseProvider = ProviderFactory.instance;
|
||||
|
||||
public async create(input: IPluginMetadataCreate): Promise<void> {
|
||||
await this.db.connection<IPluginMetadataCreate>(TABLE_PLUGINS).insert(input);
|
||||
}
|
||||
|
||||
public async update(input: IPluginMetadataUpdate): Promise<void> {
|
||||
const query = this.buildQuery(input);
|
||||
await query.update({ isActivate: input.isActivate, version: input.version });
|
||||
}
|
||||
|
||||
public async delete(input: IPluginMetadataDelete): Promise<void> {
|
||||
const query = this.buildQuery(input);
|
||||
await query.del();
|
||||
}
|
||||
|
||||
public async findAll(): Promise<IPluginMetadataPersistance[]> {
|
||||
return this.db.connection<IPluginMetadataPersistance>(TABLE_PLUGINS).select('*');
|
||||
}
|
||||
|
||||
public async findOne(input: IPluginMetadataFindOne): Promise<IPluginMetadataPersistance> {
|
||||
const query = this.buildQuery(input);
|
||||
const result = await query.first();
|
||||
if (!result) {
|
||||
throw new Error(`Plugin metadata not found for input: ${JSON.stringify(input)}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findActivated(): Promise<IPluginMetadataPersistance[]> {
|
||||
return this.db.connection<IPluginMetadataPersistance>(TABLE_PLUGINS).select('*').where('isActivate', true);
|
||||
}
|
||||
|
||||
private buildQuery(input: { id?: string; name?: string }) {
|
||||
const query = this.db.connection<IPluginMetadataPersistance>(TABLE_PLUGINS).select('*');
|
||||
if (input.id) {
|
||||
query.where('id', input.id);
|
||||
}
|
||||
if (input.name) {
|
||||
if (input.id) {
|
||||
query.orWhere('name', input.name);
|
||||
} else {
|
||||
query.where('name', input.name);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ipcMain, IpcMainEvent } from 'electron';
|
||||
import * as logger from 'electron-log';
|
||||
import { PluginManager } from '../data-access/plugin-manager';
|
||||
import { IPluginManager, PluginChannel, PluginHandlerChannel } from '../shared';
|
||||
|
||||
class ElectronPluginListener {
|
||||
private pluginManager: IPluginManager;
|
||||
|
||||
constructor(pluginManager: IPluginManager) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.removeAllListeners();
|
||||
this.removeAllHandlers();
|
||||
}
|
||||
|
||||
public registerListeners(): void {
|
||||
const eventMap = {
|
||||
[PluginChannel.LOAD]: this.loadPlugins,
|
||||
[PluginChannel.INITIALIZE]: this.initializePlugins,
|
||||
[PluginChannel.DISPOSE]: this.disposePlugins,
|
||||
[PluginChannel.DOWNLOAD]: this.downloadPlugin,
|
||||
[PluginChannel.ACTIVATE]: this.activatePlugin,
|
||||
[PluginChannel.DEACTIVATE]: this.deactivatePlugin,
|
||||
[PluginChannel.UNINSTALL]: this.uninstallPlugin
|
||||
};
|
||||
|
||||
for (const [channel, handler] of Object.entries(eventMap)) {
|
||||
ipcMain.on(channel, this.handleEvent(handler));
|
||||
}
|
||||
}
|
||||
|
||||
public registerHandlers(): void {
|
||||
ipcMain.handle(PluginHandlerChannel.GET_ALL, async () => {
|
||||
try {
|
||||
const plugins = await this.pluginManager.getAllPlugins();
|
||||
return plugins;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
ipcMain.handle(PluginHandlerChannel.GET_ONE, async (_, name) => {
|
||||
try {
|
||||
const plugin = await this.pluginManager.getOnePlugin(name);
|
||||
return plugin;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private removeAllListeners(): void {
|
||||
Object.values(PluginChannel).forEach((channel) => ipcMain.removeAllListeners(channel));
|
||||
}
|
||||
|
||||
private removeAllHandlers(): void {
|
||||
Object.values(PluginHandlerChannel).forEach((channel) => ipcMain.removeHandler(channel));
|
||||
}
|
||||
|
||||
private handleEvent(handler: (event: IpcMainEvent, ...args: any[]) => Promise<void> | void) {
|
||||
return async (event: IpcMainEvent, ...args: any[]) => {
|
||||
try {
|
||||
await handler.call(this, event, ...args);
|
||||
event.reply(PluginChannel.STATUS, { status: 'success' });
|
||||
} catch (error: any) {
|
||||
logger.error('Error handling event:', error);
|
||||
event.reply(PluginChannel.STATUS, { status: 'error', message: error?.message ?? String(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async loadPlugins(event: IpcMainEvent): Promise<void> {
|
||||
await this.pluginManager.loadPlugins();
|
||||
}
|
||||
|
||||
private initializePlugins(event: IpcMainEvent): void {
|
||||
this.pluginManager.initializePlugins();
|
||||
}
|
||||
|
||||
private disposePlugins(event: IpcMainEvent): void {
|
||||
this.pluginManager.disposePlugins();
|
||||
}
|
||||
|
||||
private async downloadPlugin(event: IpcMainEvent, config: any): Promise<void> {
|
||||
await this.pluginManager.downloadPlugin(config);
|
||||
}
|
||||
|
||||
private async activatePlugin(event: IpcMainEvent, name: string): Promise<void> {
|
||||
await this.pluginManager.activatePlugin(name);
|
||||
}
|
||||
|
||||
private async deactivatePlugin(event: IpcMainEvent, name: string): Promise<void> {
|
||||
await this.pluginManager.deactivatePlugin(name);
|
||||
}
|
||||
|
||||
private async uninstallPlugin(event: IpcMainEvent, name: string): Promise<void> {
|
||||
await this.pluginManager.uninstallPlugin(name);
|
||||
}
|
||||
}
|
||||
|
||||
export async function pluginListeners(): Promise<void> {
|
||||
const pluginManager: IPluginManager = new PluginManager();
|
||||
const listener = new ElectronPluginListener(pluginManager);
|
||||
listener.registerListeners();
|
||||
listener.registerHandlers();
|
||||
await pluginManager.loadPlugins();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from './data-access/download-context.factory';
|
||||
export * from './data-access/plugin-manager';
|
||||
export * from './data-access/strategies/cdn-download.strategy';
|
||||
export * from './database/plugin-metadata.service';
|
||||
export * from './events/plugin.event';
|
||||
export * from './shared/concretes/plugin-download.context';
|
||||
export * from './shared/constant';
|
||||
export * from './shared/enumerations/plugin-download-context-type';
|
||||
export * from './shared/enumerations/plugin.channel';
|
||||
export * from './shared/interfaces/plugin-download-strategy.interface';
|
||||
export * from './shared/interfaces/plugin-manager.interface';
|
||||
export * from './shared/interfaces/plugin-metadata.interface';
|
||||
export * from './shared/interfaces/plugin.interface';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { IPluginDownloadResponse, IPluginDownloadStrategy } from '../interfaces';
|
||||
|
||||
export class PluginDownloadContext {
|
||||
private strategy: IPluginDownloadStrategy;
|
||||
|
||||
constructor(strategy: IPluginDownloadStrategy) {
|
||||
this.strategy = strategy;
|
||||
}
|
||||
|
||||
public setStrategy(strategy: IPluginDownloadStrategy): void {
|
||||
this.strategy = strategy;
|
||||
}
|
||||
|
||||
public async execute<T>(packageConfig: T): Promise<IPluginDownloadResponse> {
|
||||
return this.strategy.execute(packageConfig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const TABLE_PLUGINS = 'plugins';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './plugin.channel';
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export enum PluginDownloadContextType {
|
||||
CDN = 'cdn'
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export enum PluginChannel {
|
||||
LOAD = 'plugins::load',
|
||||
INITIALIZE = 'plugins::initialize',
|
||||
DISPOSE = 'plugins::dispose',
|
||||
DOWNLOAD = 'plugin::download',
|
||||
ACTIVATE = 'plugin::activate',
|
||||
DEACTIVATE = 'plugin::deactivate',
|
||||
UNINSTALL = 'plugin::uninstall',
|
||||
STATUS = 'plugin::status'
|
||||
}
|
||||
|
||||
export enum PluginHandlerChannel {
|
||||
GET_ALL = 'plugins::getAll',
|
||||
GET_ONE = 'plugins::getOne'
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './concretes/plugin-download.context';
|
||||
export * from './constant';
|
||||
export * from './enumerations/plugin-download-context-type';
|
||||
export * from './enumerations/plugin.channel';
|
||||
export * from './interfaces/plugin-download-strategy.interface';
|
||||
export * from './interfaces/plugin-manager.interface';
|
||||
export * from './interfaces/plugin-metadata.interface';
|
||||
export * from './interfaces/plugin.interface';
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './plugin-download-strategy.interface';
|
||||
export * from './plugin-manager.interface';
|
||||
export * from './plugin-metadata.interface';
|
||||
export * from './plugin.interface';
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { IPluginMetadata } from './plugin-metadata.interface';
|
||||
|
||||
export interface IPluginDownloadResponse {
|
||||
pathDirname: string;
|
||||
metadata: IPluginMetadata;
|
||||
}
|
||||
|
||||
export interface IPluginDownloadStrategy {
|
||||
execute<T>(config: T): Promise<IPluginDownloadResponse>;
|
||||
}
|
||||
|
||||
export interface ICdnDownloadConfig {
|
||||
url: string;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { IPluginMetadata } from './plugin-metadata.interface';
|
||||
|
||||
export interface IPluginManager {
|
||||
loadPlugins(): Promise<void>;
|
||||
initializePlugins(): void;
|
||||
disposePlugins(): void;
|
||||
downloadPlugin(config: any): Promise<void>;
|
||||
activatePlugin(name: string): Promise<void>;
|
||||
deactivatePlugin(name: string): Promise<void>;
|
||||
uninstallPlugin(name: string): Promise<void>;
|
||||
getAllPlugins(): Promise<IPluginMetadata[]>;
|
||||
getOnePlugin(name: string): Promise<IPluginMetadata>;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
export interface IPluginMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
main: string;
|
||||
renderer?: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export type IPluginMetadataCreate = IPluginMetadata & {
|
||||
isActivate?: boolean;
|
||||
pathname: string;
|
||||
};
|
||||
|
||||
export type IPluginMetadataUpdate = Partial<
|
||||
Pick<IPluginMetadataCreate, 'isActivate' | 'name' | 'version'> & { id?: string }
|
||||
>;
|
||||
|
||||
export type IPluginMetadataPersistance = IPluginMetadataCreate & IPluginMetadataUpdate;
|
||||
|
||||
export type IPluginMetadataFindOne = Pick<IPluginMetadataUpdate, 'id' | 'name'>;
|
||||
|
||||
export type IPluginMetadataDelete = IPluginMetadataFindOne;
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface IPlugin {
|
||||
name: string;
|
||||
version: string;
|
||||
initialize(): void;
|
||||
dispose(): void;
|
||||
activate(): void;
|
||||
deactivate(): void;
|
||||
component?(): void;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Dynamically imports a module at the specified path.
|
||||
*
|
||||
* @param {string} pathModule - The path to the module to import.
|
||||
* @returns {Promise<any>} A promise that resolves to the imported module.
|
||||
* @throws Will throw an error if the module fails to load.
|
||||
*/
|
||||
export async function lazyLoader(pathModule: string): Promise<any> {
|
||||
try {
|
||||
const module = require(pathModule);
|
||||
return module.default;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load module at path: ${pathModule}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,38 @@
|
||||
export * from './lib/always-on/always-on.component';
|
||||
export * from './lib/always-on/always-on.module';
|
||||
export * from './lib/auth';
|
||||
export * from './lib/constants';
|
||||
export * from './lib/desktop-ui-lib.module';
|
||||
export * from './lib/image-viewer/image-viewer.module';
|
||||
export * from './lib/image-viewer/image-viewer.component';
|
||||
export * from './lib/screen-capture/screen-capture.module';
|
||||
export * from './lib/screen-capture/screen-capture.component';
|
||||
export * from './lib/settings/settings.module';
|
||||
export * from './lib/settings/settings.component';
|
||||
export * from './lib/setup/setup.module';
|
||||
export * from './lib/setup/setup.service';
|
||||
export * from './lib/setup/setup.component';
|
||||
export * from './lib/dialogs/about/about.component';
|
||||
export * from './lib/dialogs/about/about.module';
|
||||
export * from './lib/dialogs/alert/alert.component';
|
||||
export * from './lib/dialogs/alert/alert.module';
|
||||
export * from './lib/electron/services';
|
||||
export * from './lib/image-viewer/image-viewer.component';
|
||||
export * from './lib/image-viewer/image-viewer.module';
|
||||
export * from './lib/integrations';
|
||||
export * from './lib/interceptors';
|
||||
export * from './lib/ngx-translate';
|
||||
export * from './lib/screen-capture/screen-capture.component';
|
||||
export * from './lib/screen-capture/screen-capture.module';
|
||||
export * from './lib/server-dashboard/server-dashboard.component';
|
||||
export * from './lib/server-dashboard/server-dashboard.module';
|
||||
export * from './lib/server-down/server-down.module';
|
||||
export * from './lib/services';
|
||||
export * from './lib/settings/plugins/plugin-routing.module';
|
||||
export * from './lib/settings/settings.component';
|
||||
export * from './lib/settings/settings.module';
|
||||
export * from './lib/setup/setup.component';
|
||||
export * from './lib/setup/setup.module';
|
||||
export * from './lib/setup/setup.service';
|
||||
export * from './lib/time-tracker/time-tracker.module';
|
||||
export * from './lib/splash-screen/splash-screen.component';
|
||||
export * from './lib/splash-screen/splash-screen.module';
|
||||
export * from './lib/time-tracker/organization-selector/user-organization.service';
|
||||
export * from './lib/time-tracker/time-tracker.component';
|
||||
export * from './lib/time-tracker/time-tracker.module';
|
||||
export * from './lib/time-tracker/time-tracker.service';
|
||||
export * from './lib/updater/updater.component';
|
||||
export * from './lib/updater/updater.module';
|
||||
export * from './lib/splash-screen/splash-screen.module';
|
||||
export * from './lib/splash-screen/splash-screen.component';
|
||||
export * from './lib/server-dashboard/server-dashboard.component';
|
||||
export * from './lib/server-dashboard/server-dashboard.module';
|
||||
export * from './lib/electron/services';
|
||||
export * from './lib/dialogs/about/about.component';
|
||||
export * from './lib/dialogs/about/about.module';
|
||||
export * from './lib/services';
|
||||
export * from './lib/time-tracker/organization-selector/user-organization.service';
|
||||
export * from './lib/auth';
|
||||
export * from './lib/server-down/server-down.module';
|
||||
export * from './lib/interceptors';
|
||||
export * from './lib/ngx-translate';
|
||||
export * from './lib/always-on/always-on.component';
|
||||
export * from './lib/always-on/always-on.module';
|
||||
export * from './lib/constants';
|
||||
export * from './lib/integrations';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Auth Module
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SpinnerButtonDirective } from './spinner-button.directive';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NbSpinnerModule } from '@nebular/theme';
|
||||
|
||||
|
||||
import { DynamicDirective } from './dynamic.directive';
|
||||
import { SpinnerButtonDirective } from './spinner-button.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SpinnerButtonDirective],
|
||||
exports: [SpinnerButtonDirective],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NbSpinnerModule
|
||||
]
|
||||
declarations: [SpinnerButtonDirective, DynamicDirective],
|
||||
exports: [SpinnerButtonDirective, DynamicDirective],
|
||||
imports: [CommonModule, NbSpinnerModule]
|
||||
})
|
||||
export class DesktopDirectiveModule { }
|
||||
export class DesktopDirectiveModule {}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dynamicDirective]'
|
||||
})
|
||||
export class DynamicDirective {
|
||||
constructor(public viewContainerRef: ViewContainerRef) {}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<nb-card>
|
||||
<nb-card-header class="d-flex flex-column">
|
||||
<span class="cancel"><i (click)="close()" class="fas fa-times"></i></span>
|
||||
<h4>Plugin</h4>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<label>Add new plugin</label>
|
||||
<input nbInput #input type="url" [status]="error ? 'danger': 'basic'"
|
||||
placeholder="https://cdn.server.tld/public/plugin.zip">
|
||||
<div class="error" *ngIf="error">
|
||||
{{error}}
|
||||
</div>
|
||||
<button nbButton status="primary" (click)="installPlugin(input.value)">Install<nb-icon
|
||||
*gauzySpinnerButton="installing" icon="cloud-download-outline"></nb-icon></button>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background-color: rgb(255 0 0 / 10%);
|
||||
border-radius: 4px;
|
||||
text-wrap: pretty;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
nb-card {
|
||||
width: 350px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
nb-card-body {
|
||||
background-color: var(--gauzy-card-2);
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0em;
|
||||
color: var(--gauzy-text-color-1);
|
||||
}
|
||||
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: var(--gauzy-text-color-2);
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
nb-card-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Component, inject, NgZone, OnInit } from '@angular/core';
|
||||
import { NbDialogRef } from '@nebular/theme';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { tap } from 'rxjs';
|
||||
import { PluginElectronService } from '../../services/plugin-electron.service';
|
||||
|
||||
@UntilDestroy({ checkProperties: true })
|
||||
@Component({
|
||||
selector: 'ngx-add-plugin',
|
||||
templateUrl: './add-plugin.component.html',
|
||||
styleUrls: ['./add-plugin.component.scss']
|
||||
})
|
||||
export class AddPluginComponent implements OnInit {
|
||||
private readonly pluginElectronService = inject(PluginElectronService);
|
||||
private readonly dialogRef = inject(NbDialogRef<AddPluginComponent>);
|
||||
private readonly ngZone = inject(NgZone);
|
||||
public installing = false;
|
||||
public error = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pluginElectronService.status
|
||||
.pipe(
|
||||
tap(({ status, message }) =>
|
||||
this.ngZone.run(() => {
|
||||
this.installing = false;
|
||||
if (status === 'success') {
|
||||
this.close();
|
||||
} else {
|
||||
console.error(message);
|
||||
this.error = message;
|
||||
}
|
||||
})
|
||||
),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public installPlugin(value: string) {
|
||||
if (!value) {
|
||||
this.error = "The server URL mustn't be empty.";
|
||||
return;
|
||||
}
|
||||
this.installing = true;
|
||||
this.pluginElectronService.downloadAndInstall({ url: value.trim() });
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<div class="row">
|
||||
<div routerLink="['/settings', 'plugins']" class="col-12">
|
||||
<h4>{{ 'TIMER_TRACKER.SETTINGS.PLUGIN' | translate }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0em;
|
||||
color: var(--gauzy-text-color-1);
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { PluginLayoutComponent } from './plugin-layout.component';
|
||||
|
||||
describe('PluginLayoutComponent', () => {
|
||||
let component: PluginLayoutComponent;
|
||||
let fixture: ComponentFixture<PluginLayoutComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [PluginLayoutComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PluginLayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ngx-plugin-layout',
|
||||
templateUrl: './plugin-layout.component.html',
|
||||
styleUrls: ['./plugin-layout.component.scss']
|
||||
})
|
||||
export class PluginLayoutComponent {}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<div class="button-container mb-3">
|
||||
<div class="button-add">
|
||||
<button (click)="addPlugin()" status="primary" nbButton> <nb-icon icon="plus-outline"></nb-icon>New
|
||||
Plugin</button>
|
||||
</div>
|
||||
<div *ngIf="plugin" class="button-hide">
|
||||
<button *ngIf="plugin.renderer" class="button-view" status="basic" (click)="view()" nbButton><nb-icon
|
||||
icon="eye-outline"></nb-icon>View</button>
|
||||
<button *gauzySpinnerButton="processing" [status]="plugin?.isActivate ? 'warning': 'success'"
|
||||
class="button-install" (click)="changeStatus()" nbButton><nb-icon
|
||||
[icon]="plugin?.isActivate ? 'close-circle-outline': 'checkmark-circle-outline'"></nb-icon>{{
|
||||
plugin?.isActivate ? 'Deactivate':
|
||||
'Activate' }}</button>
|
||||
<button *gauzySpinnerButton="processing" class="button-remove" status="danger" (click)="uninstall()"
|
||||
nbButton><nb-icon icon="trash-outline"></nb-icon>Uninstall</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="(plugins$ | async)?.length > 0; else noPlugin" class="col-12 custom-table">
|
||||
<div class="table-scroll-container">
|
||||
<angular2-smart-table #pluginTable (userRowSelect)="handleRowSelection($event)" [settings]="smartTableSettings"
|
||||
[source]="(sourceData$ | async)" style="cursor: pointer">
|
||||
</angular2-smart-table>
|
||||
</div>
|
||||
<div class="pagination-container">
|
||||
<ga-pagination [source]="(sourceData$ | async)"></ga-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noPlugin>
|
||||
<div class="no-data">
|
||||
<ngx-no-data-message [message]="'SM_TABLE.NO_DATA.PLUGIN' | translate">
|
||||
</ngx-no-data-message>
|
||||
</div>
|
||||
</ng-template>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
.custom-table {
|
||||
background-color: var(--gauzy-card-2);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 4px 0px;
|
||||
border-radius: var(--border-radius);
|
||||
height: calc(100vh - 11.4rem);
|
||||
width: auto;
|
||||
|
||||
.table-scroll-container {
|
||||
flex-grow: 10;
|
||||
max-height: unset;
|
||||
padding: 0 4px 0;
|
||||
margin-bottom: 8px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container,
|
||||
.button-hide {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
height: 550px;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { PluginListComponent } from './plugin-list.component';
|
||||
|
||||
describe('PluginListComponent', () => {
|
||||
let component: PluginListComponent;
|
||||
let fixture: ComponentFixture<PluginListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [PluginListComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PluginListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { Component, inject, NgZone, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NbDialogService } from '@nebular/theme';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Angular2SmartTableComponent, Cell, LocalDataSource } from 'angular2-smart-table';
|
||||
import { BehaviorSubject, concatMap, filter, from, Observable, switchMap, tap } from 'rxjs';
|
||||
import { PluginElectronService } from '../../services/plugin-electron.service';
|
||||
import { IPlugin } from '../../services/plugin-loader.service';
|
||||
import { AddPluginComponent } from '../add-plugin/add-plugin.component';
|
||||
import { PluginStatusComponent } from './plugin-status/plugin-status.component';
|
||||
import { PluginUpdateComponent } from './plugin-update/plugin-update.component';
|
||||
|
||||
@UntilDestroy({ checkProperties: true })
|
||||
@Component({
|
||||
selector: 'ngx-plugin-list',
|
||||
templateUrl: './plugin-list.component.html',
|
||||
styleUrls: ['./plugin-list.component.scss']
|
||||
})
|
||||
export class PluginListComponent implements OnInit {
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly pluginElectronService = inject(PluginElectronService);
|
||||
private readonly dialog = inject(NbDialogService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly ngZone = inject(NgZone);
|
||||
|
||||
public plugins$ = new BehaviorSubject<IPlugin[]>([]);
|
||||
public plugin: IPlugin = null;
|
||||
private _sourceData$ = new BehaviorSubject(new LocalDataSource([]));
|
||||
|
||||
public get sourceData$(): Observable<LocalDataSource> {
|
||||
return this._sourceData$.asObservable();
|
||||
}
|
||||
|
||||
private get sourceData(): LocalDataSource {
|
||||
return this._sourceData$.getValue();
|
||||
}
|
||||
|
||||
private _pluginTable: Angular2SmartTableComponent;
|
||||
public processing = false;
|
||||
|
||||
@ViewChild('pluginTable') set pluginTable(content: Angular2SmartTableComponent) {
|
||||
if (content) {
|
||||
this._pluginTable = content;
|
||||
this.onChangedSource();
|
||||
}
|
||||
}
|
||||
|
||||
public get pluginTable(): Angular2SmartTableComponent {
|
||||
return this._pluginTable;
|
||||
}
|
||||
|
||||
public readonly smartTableSettings = {
|
||||
columns: {
|
||||
name: {
|
||||
title: this.translateService.instant('SM_TABLE.NAME')
|
||||
},
|
||||
isActivate: {
|
||||
title: this.translateService.instant('SM_TABLE.STATUS'),
|
||||
type: 'custom',
|
||||
renderComponent: PluginStatusComponent,
|
||||
componentInitFunction: (instance: PluginStatusComponent, cell: Cell) => {
|
||||
instance.rowData = cell.getRow().getData();
|
||||
}
|
||||
},
|
||||
version: {
|
||||
title: this.translateService.instant('TIMER_TRACKER.SETTINGS.VERSION')
|
||||
},
|
||||
updatedAt: {
|
||||
title: this.translateService.instant('TIMER_TRACKER.SETTINGS.UPDATE'),
|
||||
type: 'custom',
|
||||
renderComponent: PluginUpdateComponent,
|
||||
componentInitFunction: (instance: PluginUpdateComponent, cell: Cell) => {
|
||||
instance.rowData = cell.getRow().getData();
|
||||
}
|
||||
}
|
||||
},
|
||||
hideSubHeader: true,
|
||||
actions: false,
|
||||
noDataMessage: this.translateService.instant('SM_TABLE.NO_DATA.PLUGIN'),
|
||||
pager: {
|
||||
display: true,
|
||||
perPage: 10,
|
||||
page: 1
|
||||
}
|
||||
};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.observePlugins();
|
||||
this.loadPlugins();
|
||||
}
|
||||
|
||||
private observePlugins(): void {
|
||||
this.pluginElectronService.status
|
||||
.pipe(
|
||||
tap(() => (this.processing = false)),
|
||||
filter((response) => response.status === 'success'),
|
||||
switchMap(() => from(this.pluginElectronService.plugins)),
|
||||
tap((plugins) => this.ngZone.run(() => (this.plugins = plugins))),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe();
|
||||
this.plugins$
|
||||
.pipe(
|
||||
tap(() => this.clearItem()),
|
||||
tap(() => (this.plugin = null)),
|
||||
concatMap((plugins) => this.sourceData.load(plugins)),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private loadPlugins(): void {
|
||||
from(this.pluginElectronService.plugins)
|
||||
.pipe(
|
||||
tap((plugins) => this.ngZone.run(() => (this.plugins = plugins))),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public handleRowSelection(event) {
|
||||
const selected = event.selected[0];
|
||||
this.plugin = selected && selected.id === this.plugin?.id ? this.plugin : selected;
|
||||
}
|
||||
|
||||
public changeStatus() {
|
||||
this.processing = true;
|
||||
if (this.plugin.isActivate) {
|
||||
this.pluginElectronService.deactivate(this.plugin);
|
||||
} else {
|
||||
this.pluginElectronService.activate(this.plugin);
|
||||
}
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
public view() {
|
||||
this.router.navigate(['/settings', 'plugins', this.plugin.name]);
|
||||
}
|
||||
|
||||
public addPlugin() {
|
||||
this.dialog
|
||||
.open(AddPluginComponent, { backdropClass: 'backdrop-blur' })
|
||||
.onClose.pipe(untilDestroyed(this))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public uninstall() {
|
||||
this.processing = true;
|
||||
this.pluginElectronService.uninstall(this.plugin);
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
private clearItem(): void {
|
||||
if (this.pluginTable && this.pluginTable.grid) {
|
||||
this.pluginTable.grid.dataSet['willSelect'] = 'indexed';
|
||||
this.pluginTable.grid.dataSet.deselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
private onChangedSource(): void {
|
||||
this.pluginTable.source.onChangedSource
|
||||
.pipe(
|
||||
tap(() => this.clearItem()),
|
||||
tap(() => {
|
||||
if (this.plugin) {
|
||||
this.pluginTable.grid.dataSet.getRows().forEach((row) => {
|
||||
if (row.getData().id === this.plugin.id) {
|
||||
this.pluginTable.grid.dataSet.selectRow(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private get plugins(): IPlugin[] {
|
||||
return this.plugins$.getValue();
|
||||
}
|
||||
|
||||
private set plugins(plugins: IPlugin[]) {
|
||||
this.plugins$.next(plugins);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<div class="badge-container">
|
||||
<nb-badge [status]="state?.status" [text]="state?.message | titlecase" class="badge" position="center"></nb-badge>
|
||||
</div>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.badge-container {
|
||||
position: relative;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { PluginStatusComponent } from './plugin-status.component';
|
||||
|
||||
describe('PluginStatusComponent', () => {
|
||||
let component: PluginStatusComponent;
|
||||
let fixture: ComponentFixture<PluginStatusComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [PluginStatusComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PluginStatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ngx-plugin-status',
|
||||
templateUrl: './plugin-status.component.html',
|
||||
styleUrls: ['./plugin-status.component.scss']
|
||||
})
|
||||
export class PluginStatusComponent {
|
||||
public rowData: any;
|
||||
|
||||
public get state() {
|
||||
return this.rowData.isActivate
|
||||
? { message: 'active', status: 'success' }
|
||||
: { message: 'inactive', status: 'danger' };
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{ lastUpdate | date: 'short' }}
|
||||
</div>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { PluginUpdateComponent } from './plugin-update.component';
|
||||
|
||||
describe('PluginUpdateComponent', () => {
|
||||
let component: PluginUpdateComponent;
|
||||
let fixture: ComponentFixture<PluginUpdateComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [PluginUpdateComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PluginUpdateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ngx-plugin-update',
|
||||
templateUrl: './plugin-update.component.html',
|
||||
styleUrls: ['./plugin-update.component.scss']
|
||||
})
|
||||
export class PluginUpdateComponent {
|
||||
public rowData: any;
|
||||
|
||||
public get lastUpdate() {
|
||||
return this.rowData.updatedAt;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<ng-template dynamicDirective></ng-template>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { PluginComponent } from './plugin.component';
|
||||
|
||||
describe('PluginComponent', () => {
|
||||
let component: PluginComponent;
|
||||
let fixture: ComponentFixture<PluginComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [PluginComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PluginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AfterViewInit, Component, inject, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { DynamicDirective } from 'packages/desktop-ui-lib/src/lib/directives/dynamic.directive';
|
||||
import { concatMap, from } from 'rxjs';
|
||||
import { PluginElectronService } from '../../services/plugin-electron.service';
|
||||
import { PluginLoaderService } from '../../services/plugin-loader.service';
|
||||
|
||||
@UntilDestroy({ checkProperties: true })
|
||||
@Component({
|
||||
selector: 'ngx-plugin',
|
||||
templateUrl: './plugin.component.html',
|
||||
styleUrls: ['./plugin.component.scss']
|
||||
})
|
||||
export class PluginComponent implements AfterViewInit {
|
||||
private readonly loaderService = inject(PluginLoaderService);
|
||||
private readonly electronService = inject(PluginElectronService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private _renderer: DynamicDirective;
|
||||
|
||||
@ViewChild(DynamicDirective, { static: true })
|
||||
public set renderer(value: DynamicDirective) {
|
||||
if (value) {
|
||||
this._renderer = value;
|
||||
}
|
||||
}
|
||||
|
||||
public get renderer(): DynamicDirective {
|
||||
return this._renderer;
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
from(this.electronService.plugin(this.route.snapshot.params.name))
|
||||
.pipe(
|
||||
concatMap((plugin) => this.loaderService.load(plugin, this.renderer.viewContainerRef)),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const pluginRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'plugins'
|
||||
},
|
||||
{
|
||||
path: 'plugins',
|
||||
loadComponent: () => import('./component/plugin-list/plugin-list.component').then((m) => m.PluginListComponent)
|
||||
},
|
||||
{
|
||||
path: 'plugins/:name',
|
||||
loadComponent: () => import('./component/plugin/plugin.component').then((m) => m.PluginComponent)
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'plugins'
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NbBadgeModule, NbButtonModule, NbCardModule, NbIconModule, NbInputModule } from '@nebular/theme';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Angular2SmartTableModule } from 'angular2-smart-table';
|
||||
import { DesktopDirectiveModule } from '../../directives/desktop-directive.module';
|
||||
import { NoDataModuleModule } from '../../time-tracker/no-data-message/no-data-message.module';
|
||||
import { PaginationModule } from '../../time-tracker/pagination/pagination.module';
|
||||
import { AddPluginComponent } from './component/add-plugin/add-plugin.component';
|
||||
import { PluginLayoutComponent } from './component/plugin-layout/plugin-layout.component';
|
||||
import { PluginListComponent } from './component/plugin-list/plugin-list.component';
|
||||
import { PluginStatusComponent } from './component/plugin-list/plugin-status/plugin-status.component';
|
||||
import { PluginUpdateComponent } from './component/plugin-list/plugin-update/plugin-update.component';
|
||||
import { PluginComponent } from './component/plugin/plugin.component';
|
||||
import { PluginElectronService } from './services/plugin-electron.service';
|
||||
import { PluginLoaderService } from './services/plugin-loader.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddPluginComponent,
|
||||
PluginListComponent,
|
||||
PluginComponent,
|
||||
PluginLayoutComponent,
|
||||
PluginStatusComponent,
|
||||
PluginUpdateComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
Angular2SmartTableModule,
|
||||
PaginationModule,
|
||||
NbButtonModule,
|
||||
NbInputModule,
|
||||
NbCardModule,
|
||||
DesktopDirectiveModule,
|
||||
NoDataModuleModule,
|
||||
NbIconModule,
|
||||
TranslateModule,
|
||||
RouterModule,
|
||||
NbBadgeModule
|
||||
],
|
||||
exports: [PluginLayoutComponent],
|
||||
providers: [PluginLoaderService, PluginElectronService]
|
||||
})
|
||||
export class PluginsModule {}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ElectronService } from '../../../electron/services';
|
||||
import { IPlugin } from './plugin-loader.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PluginElectronService {
|
||||
private electronService = inject(ElectronService);
|
||||
|
||||
public get plugins(): Promise<IPlugin[]> {
|
||||
return this.electronService.ipcRenderer.invoke('plugins::getAll');
|
||||
}
|
||||
|
||||
public plugin(name: string): Promise<IPlugin> {
|
||||
return this.electronService.ipcRenderer.invoke('plugins::getOne', name);
|
||||
}
|
||||
|
||||
public activate(plugin: IPlugin) {
|
||||
this.electronService.ipcRenderer.send('plugin::activate', plugin.name);
|
||||
}
|
||||
|
||||
public load() {
|
||||
this.electronService.ipcRenderer.send('plugins::load');
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
this.electronService.ipcRenderer.send('plugins::initialize');
|
||||
}
|
||||
|
||||
public deactivate(plugin: IPlugin) {
|
||||
this.electronService.ipcRenderer.send('plugin::deactivate', plugin.name);
|
||||
}
|
||||
|
||||
public downloadAndInstall<T>(config: T) {
|
||||
this.electronService.ipcRenderer.send('plugin::download', config);
|
||||
}
|
||||
|
||||
public uninstall(plugin: IPlugin) {
|
||||
this.electronService.ipcRenderer.send('plugin::uninstall', plugin.name);
|
||||
}
|
||||
|
||||
public get status(): Observable<{ status: string; message?: string }> {
|
||||
return new Observable((observer) => {
|
||||
const channel = 'plugin::status';
|
||||
const listener = (_, arg: { status: string; message?: string }) => {
|
||||
observer.next(arg);
|
||||
};
|
||||
|
||||
this.electronService.ipcRenderer.on(channel, listener);
|
||||
|
||||
return () => {
|
||||
this.electronService.ipcRenderer.removeListener(channel, listener);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ComponentRef, Injectable, Type, ViewContainerRef } from '@angular/core';
|
||||
import * as path from 'path';
|
||||
import { NoDataMessageComponent } from '../../../time-tracker/no-data-message/no-data-message.component';
|
||||
|
||||
export interface IPlugin {
|
||||
id?: string;
|
||||
renderer?: string;
|
||||
pathname?: string;
|
||||
name: string;
|
||||
version: string;
|
||||
isActivate: boolean;
|
||||
initialize(): void;
|
||||
dispose(): void;
|
||||
activate(): void;
|
||||
deactivate(): void;
|
||||
component?: Type<any>;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PluginLoaderService {
|
||||
/**
|
||||
* Loads a plugin component into the provided container.
|
||||
* @param plugin - The plugin to load.
|
||||
* @param viewContainerRef - The ViewContainerRef where the component will be loaded.
|
||||
* @returns A promise that resolves to the ComponentRef of the loaded component.
|
||||
*/
|
||||
public async load(plugin: IPlugin, viewContainerRef: ViewContainerRef): Promise<ComponentRef<any>> {
|
||||
viewContainerRef.clear();
|
||||
|
||||
if (!plugin.renderer) {
|
||||
return this.loadNoDataMessageComponent('Plugin does not have a component to load.', viewContainerRef);
|
||||
}
|
||||
|
||||
try {
|
||||
const componentModule = await import(/*webpackIgnore:true*/ path.join(plugin.pathname, plugin.renderer));
|
||||
const componentRef = viewContainerRef.createComponent(componentModule.default);
|
||||
return componentRef;
|
||||
} catch (error) {
|
||||
const message = 'Error loading component: ' + error;
|
||||
return this.loadNoDataMessageComponent(message, viewContainerRef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a NoDataMessageComponent with a specified message.
|
||||
* @param message - The message to display.
|
||||
*/
|
||||
private loadNoDataMessageComponent(message: string, viewContainerRef: ViewContainerRef) {
|
||||
try {
|
||||
const componentRef = viewContainerRef.createComponent(NoDataMessageComponent);
|
||||
const instance: NoDataMessageComponent = componentRef.instance;
|
||||
instance.message = message;
|
||||
console.warn(message);
|
||||
return componentRef;
|
||||
} catch (error) {
|
||||
console.error('Error loading NoDataMessageComponent:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1522,6 +1522,9 @@
|
||||
</nb-card>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="(selectedMenu$ | async) === 'TIMER_TRACKER.SETTINGS.PLUGINS'" >
|
||||
<ngx-plugin-layout></ngx-plugin-layout>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
class="row"
|
||||
*ngIf="(selectedMenu$ | async) === 'MENU.ABOUT'"
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
NgZone,
|
||||
AfterViewInit,
|
||||
Optional,
|
||||
Inject
|
||||
} from '@angular/core';
|
||||
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
|
||||
import { NbDialogService, NbToastrService } from '@nebular/theme';
|
||||
import { ElectronService } from '../electron/services';
|
||||
import { BehaviorSubject, Observable, filter, tap, firstValueFrom, from } from 'rxjs';
|
||||
import { AboutComponent } from '../dialogs/about/about.component';
|
||||
import { SetupService } from '../setup/setup.service';
|
||||
import * as moment from 'moment';
|
||||
import { TimeTrackerDateManager, TimeZoneManager, ToastrNotificationService, ZoneEnum } from '../services';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { AuthStrategy } from '../auth';
|
||||
import { DEFAULT_SCREENSHOT_FREQUENCY_OPTIONS, LanguagesEnum } from '@gauzy/contracts';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { LanguageSelectorService } from '../language/language-selector.service';
|
||||
import { GAUZY_ENV } from '../constants';
|
||||
import { AfterViewInit, Component, ElementRef, Inject, NgZone, OnInit, Optional, ViewChild } from '@angular/core';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { DEFAULT_SCREENSHOT_FREQUENCY_OPTIONS, LanguagesEnum } from '@gauzy/contracts';
|
||||
import { NbDialogService, NbToastrService } from '@nebular/theme';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as moment from 'moment';
|
||||
import { BehaviorSubject, Observable, filter, firstValueFrom, from, tap } from 'rxjs';
|
||||
import { AuthStrategy } from '../auth';
|
||||
import { GAUZY_ENV } from '../constants';
|
||||
import { AboutComponent } from '../dialogs/about/about.component';
|
||||
import { ElectronService } from '../electron/services';
|
||||
import { LanguageSelectorService } from '../language/language-selector.service';
|
||||
import { TimeTrackerDateManager, TimeZoneManager, ToastrNotificationService, ZoneEnum } from '../services';
|
||||
import { SetupService } from '../setup/setup.service';
|
||||
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
|
||||
|
||||
@UntilDestroy({ checkProperties: true })
|
||||
@Component({
|
||||
selector: 'ngx-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: [
|
||||
`
|
||||
:host nb-tab {
|
||||
@@ -571,12 +560,18 @@ export class SettingsComponent implements OnInit, AfterViewInit {
|
||||
await this.getUserDetails();
|
||||
}
|
||||
this.menus = this.isServer
|
||||
? ['TIMER_TRACKER.SETTINGS.UPDATE', 'TIMER_TRACKER.SETTINGS.ADVANCED_SETTINGS', 'MENU.ABOUT']
|
||||
? [
|
||||
'TIMER_TRACKER.SETTINGS.UPDATE',
|
||||
'TIMER_TRACKER.SETTINGS.ADVANCED_SETTINGS',
|
||||
'TIMER_TRACKER.SETTINGS.PLUGINS',
|
||||
'MENU.ABOUT'
|
||||
]
|
||||
: [
|
||||
...(auth && auth.allowScreenshotCapture ? ['TIMER_TRACKER.SETTINGS.SCREEN_CAPTURE'] : []),
|
||||
'TIMER_TRACKER.TIMER',
|
||||
'TIMER_TRACKER.SETTINGS.UPDATE',
|
||||
'TIMER_TRACKER.SETTINGS.ADVANCED_SETTINGS',
|
||||
'TIMER_TRACKER.SETTINGS.PLUGINS',
|
||||
'MENU.ABOUT'
|
||||
];
|
||||
const lastMenu =
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SettingsComponent } from './settings.component';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
NbLayoutModule,
|
||||
NbSidebarModule,
|
||||
NbMenuModule,
|
||||
NbCardModule,
|
||||
NbIconModule,
|
||||
NbListModule,
|
||||
NbSelectModule,
|
||||
NbToggleModule,
|
||||
NbInputModule,
|
||||
NbButtonModule,
|
||||
NbAccordionModule,
|
||||
NbAlertModule,
|
||||
NbButtonModule,
|
||||
NbCardModule,
|
||||
NbDialogService,
|
||||
NbIconModule,
|
||||
NbInputModule,
|
||||
NbLayoutModule,
|
||||
NbListModule,
|
||||
NbMenuModule,
|
||||
NbProgressBarModule,
|
||||
NbSelectModule,
|
||||
NbSidebarModule,
|
||||
NbSpinnerModule,
|
||||
NbTabsetModule,
|
||||
NbToastrService,
|
||||
NbAccordionModule,
|
||||
NbTooltipModule,
|
||||
NbSpinnerModule,
|
||||
NbDialogService
|
||||
NbToggleModule,
|
||||
NbTooltipModule
|
||||
} from '@nebular/theme';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
|
||||
import { DesktopDirectiveModule } from '../directives/desktop-directive.module';
|
||||
import { LanguageModule } from '../language/language.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Store } from '../services';
|
||||
import { DesktopDirectiveModule } from '../directives/desktop-directive.module';
|
||||
import { LanguageSelectorService } from '../language/language-selector.service';
|
||||
import { LanguageModule } from '../language/language.module';
|
||||
import { Store } from '../services';
|
||||
import { TaskRenderModule } from '../time-tracker/task-render';
|
||||
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
|
||||
import { PluginsModule } from './plugins/plugins.module';
|
||||
import { SettingsComponent } from './settings.component';
|
||||
import { SslModule } from './ssl';
|
||||
|
||||
@NgModule({
|
||||
@@ -56,7 +57,8 @@ import { SslModule } from './ssl';
|
||||
LanguageModule,
|
||||
TranslateModule,
|
||||
TaskRenderModule,
|
||||
SslModule
|
||||
SslModule,
|
||||
PluginsModule
|
||||
],
|
||||
providers: [NbToastrService, TimeTrackerService, NbDialogService, Store, LanguageSelectorService],
|
||||
exports: [SettingsComponent]
|
||||
|
||||
+15
-15
@@ -1,17 +1,17 @@
|
||||
<nb-card class="p-0 no-data-found">
|
||||
<nb-card-body>
|
||||
<div class="row align-items-center h-100">
|
||||
<div class="col-12">
|
||||
<div class="text-center d-flex justify-content-center py-3">
|
||||
<nb-icon icon="bar-chart-2-outline" class="no-record-icon"></nb-icon>
|
||||
</div>
|
||||
<h5 class="text-center m-0">
|
||||
{{ "SM_TABLE.NO_DATA_MESSAGE" | translate }}
|
||||
</h5>
|
||||
<p class="text-center">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
<nb-card-body>
|
||||
<div class="row align-items-center h-100">
|
||||
<div class="col-12">
|
||||
<div class="text-center d-flex justify-content-center py-3">
|
||||
<nb-icon icon="bar-chart-2-outline" class="no-record-icon"></nb-icon>
|
||||
</div>
|
||||
<h5 class="text-center m-0">
|
||||
{{ "SM_TABLE.NO_DATA_MESSAGE" | translate }}
|
||||
</h5>
|
||||
<p class="text-center">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NbCardModule, NbIconModule } from '@nebular/theme';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NoDataMessageComponent } from './no-data-message.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [NoDataMessageComponent],
|
||||
imports: [CommonModule, NbCardModule, NbIconModule, TranslateModule],
|
||||
exports: [NoDataMessageComponent]
|
||||
})
|
||||
export class NoDataModuleModule {}
|
||||
@@ -9,31 +9,8 @@ import {
|
||||
TemplateRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { NbDialogRef, NbDialogService, NbIconLibraries, NbToastrService } from '@nebular/theme';
|
||||
import { TimeTrackerService } from './time-tracker.service';
|
||||
import * as moment from 'moment';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import * as _ from 'underscore';
|
||||
import { LocalDataSource, Angular2SmartTableComponent, Cell } from 'angular2-smart-table';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import {
|
||||
asapScheduler,
|
||||
asyncScheduler,
|
||||
BehaviorSubject,
|
||||
concatMap,
|
||||
debounceTime,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
of,
|
||||
Subject,
|
||||
tap
|
||||
} from 'rxjs';
|
||||
import { ElectronService, LoggerService } from '../electron/services';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import 'moment-duration-format';
|
||||
import {
|
||||
ContactType,
|
||||
IOrganization,
|
||||
@@ -49,6 +26,42 @@ import {
|
||||
TaskStatusEnum
|
||||
} from '@gauzy/contracts';
|
||||
import { compressImage, distinctUntilChange } from '@gauzy/ui-core/common';
|
||||
import { NbDialogRef, NbDialogService, NbIconLibraries, NbToastrService } from '@nebular/theme';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Angular2SmartTableComponent, Cell, LocalDataSource } from 'angular2-smart-table';
|
||||
import * as moment from 'moment';
|
||||
import 'moment-duration-format';
|
||||
import {
|
||||
asapScheduler,
|
||||
asyncScheduler,
|
||||
BehaviorSubject,
|
||||
concatMap,
|
||||
debounceTime,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
of,
|
||||
Subject,
|
||||
tap
|
||||
} from 'rxjs';
|
||||
import * as _ from 'underscore';
|
||||
import { AlwaysOnService, AlwaysOnStateEnum } from '../always-on/always-on.service';
|
||||
import { AuthStrategy } from '../auth';
|
||||
import { GAUZY_ENV } from '../constants';
|
||||
import { ElectronService, LoggerService } from '../electron/services';
|
||||
import { ImageViewerService } from '../image-viewer/image-viewer.service';
|
||||
import { ActivityWatchViewService } from '../integrations';
|
||||
import { LanguageSelectorService } from '../language/language-selector.service';
|
||||
import {
|
||||
InterruptedSequenceQueue,
|
||||
ISequence,
|
||||
SequenceQueue,
|
||||
TimeSlotQueueService,
|
||||
ViewQueueStateUpdater
|
||||
} from '../offline-sync';
|
||||
import {
|
||||
ErrorHandlerService,
|
||||
NativeNotificationService,
|
||||
@@ -58,26 +71,13 @@ import {
|
||||
ToastrNotificationService,
|
||||
ZoneEnum
|
||||
} from '../services';
|
||||
import { TimeTrackerStatusService } from './time-tracker-status/time-tracker-status.service';
|
||||
import { IRemoteTimer } from './time-tracker-status/interfaces';
|
||||
import {
|
||||
InterruptedSequenceQueue,
|
||||
ISequence,
|
||||
SequenceQueue,
|
||||
TimeSlotQueueService,
|
||||
ViewQueueStateUpdater
|
||||
} from '../offline-sync';
|
||||
import { ImageViewerService } from '../image-viewer/image-viewer.service';
|
||||
import { AuthStrategy } from '../auth';
|
||||
import { LanguageSelectorService } from '../language/language-selector.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AlwaysOnService, AlwaysOnStateEnum } from '../always-on/always-on.service';
|
||||
import { TasksComponent } from '../tasks/tasks.component';
|
||||
import { TaskDurationComponent, TaskProgressComponent } from './task-render';
|
||||
import { TaskRenderCellComponent } from './task-render/task-render-cell/task-render-cell.component';
|
||||
import { TaskStatusComponent } from './task-render/task-status/task-status.component';
|
||||
import { GAUZY_ENV } from '../constants';
|
||||
import { TasksComponent } from '../tasks/tasks.component';
|
||||
import { ActivityWatchViewService } from '../integrations';
|
||||
import { IRemoteTimer } from './time-tracker-status/interfaces';
|
||||
import { TimeTrackerStatusService } from './time-tracker-status/time-tracker-status.service';
|
||||
import { TimeTrackerService } from './time-tracker.service';
|
||||
|
||||
enum TimerStartMode {
|
||||
MANUAL = 'manual',
|
||||
@@ -1608,6 +1608,8 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
|
||||
await this.updateOrganizationTeamEmployee();
|
||||
|
||||
this.electronService.ipcRenderer.send('request_permission');
|
||||
|
||||
this.electronService.ipcRenderer.send('start-capture-screen');
|
||||
} catch (error) {
|
||||
this._startMode = TimerStartMode.STOP;
|
||||
this.start$.next(false);
|
||||
@@ -1623,6 +1625,8 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
|
||||
|
||||
const config = { quitApp: this.quitApp, isEmergency };
|
||||
|
||||
this.electronService.ipcRenderer.send('stop-capture-screen');
|
||||
|
||||
if (this._startMode === TimerStartMode.MANUAL) {
|
||||
console.log('Stopping timer');
|
||||
const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config);
|
||||
|
||||
@@ -1,56 +1,50 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Angular2SmartTableModule } from 'angular2-smart-table';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TimeTrackerComponent } from './time-tracker.component';
|
||||
import { TimeTrackerService } from './time-tracker.service';
|
||||
import { CustomRenderComponent } from './custom-render-cell.component';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NbEvaIconsModule } from '@nebular/eva-icons';
|
||||
import {
|
||||
NbLayoutModule,
|
||||
NbCardModule,
|
||||
NbToggleModule,
|
||||
NbSelectModule,
|
||||
NbInputModule,
|
||||
NbBadgeModule,
|
||||
NbButtonModule,
|
||||
NbSpinnerModule,
|
||||
NbCardModule,
|
||||
NbCheckboxModule,
|
||||
NbDialogModule,
|
||||
NbDialogService,
|
||||
NbFormFieldModule,
|
||||
NbIconModule,
|
||||
NbInputModule,
|
||||
NbLayoutModule,
|
||||
NbSelectModule,
|
||||
NbSidebarModule,
|
||||
NbSidebarService,
|
||||
NbCheckboxModule,
|
||||
NbDialogService,
|
||||
NbDialogModule,
|
||||
NbSpinnerModule,
|
||||
NbToastrService,
|
||||
NbFormFieldModule,
|
||||
NbTooltipModule,
|
||||
NbBadgeModule
|
||||
NbToggleModule,
|
||||
NbTooltipModule
|
||||
} from '@nebular/theme';
|
||||
import { NbEvaIconsModule } from '@nebular/eva-icons';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { TasksModule } from '../tasks/tasks.module';
|
||||
import { PaginationModule } from './pagination/pagination.module';
|
||||
import { NoDataMessageComponent } from './no-data-message/no-data-message.component';
|
||||
import { HumanizePipe } from './pipes/humanize.pipe';
|
||||
import { UserOrganizationService } from './organization-selector/user-organization.service';
|
||||
import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component';
|
||||
import { ErrorHandlerService, NativeNotificationService, Store, ToastrNotificationService } from '../services';
|
||||
import { ElectronService, LoggerService } from '../electron/services';
|
||||
import { TimeTrackerStatusModule } from './time-tracker-status/time-tracker-status.module';
|
||||
import { TimeSlotQueueService } from '../offline-sync';
|
||||
import { ImageViewerModule } from '../image-viewer/image-viewer.module';
|
||||
import { NgxTranslateModule } from '../ngx-translate';
|
||||
import { LanguageSelectorService } from '../language/language-selector.service';
|
||||
import { Angular2SmartTableModule } from 'angular2-smart-table';
|
||||
import { AlwaysOnService } from '../always-on/always-on.service';
|
||||
import { TaskRenderModule } from './task-render/task-render.module';
|
||||
import { ElectronService, LoggerService } from '../electron/services';
|
||||
import { ImageViewerModule } from '../image-viewer/image-viewer.module';
|
||||
import { ActivityWatchModule } from '../integrations';
|
||||
import { LanguageSelectorService } from '../language/language-selector.service';
|
||||
import { NgxTranslateModule } from '../ngx-translate';
|
||||
import { TimeSlotQueueService } from '../offline-sync';
|
||||
import { ErrorHandlerService, NativeNotificationService, Store, ToastrNotificationService } from '../services';
|
||||
import { TasksModule } from '../tasks/tasks.module';
|
||||
import { CustomRenderComponent } from './custom-render-cell.component';
|
||||
import { NoDataModuleModule } from './no-data-message/no-data-message.module';
|
||||
import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component';
|
||||
import { UserOrganizationService } from './organization-selector/user-organization.service';
|
||||
import { PaginationModule } from './pagination/pagination.module';
|
||||
import { HumanizePipe } from './pipes/humanize.pipe';
|
||||
import { TaskRenderModule } from './task-render/task-render.module';
|
||||
import { TimeTrackerStatusModule } from './time-tracker-status/time-tracker-status.module';
|
||||
import { TimeTrackerComponent } from './time-tracker.component';
|
||||
import { TimeTrackerService } from './time-tracker.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TimeTrackerComponent,
|
||||
CustomRenderComponent,
|
||||
NoDataMessageComponent,
|
||||
HumanizePipe,
|
||||
OrganizationSelectorComponent
|
||||
],
|
||||
declarations: [TimeTrackerComponent, CustomRenderComponent, HumanizePipe, OrganizationSelectorComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NbLayoutModule,
|
||||
@@ -80,7 +74,8 @@ import { ActivityWatchModule } from '../integrations';
|
||||
ImageViewerModule,
|
||||
NgxTranslateModule,
|
||||
TaskRenderModule,
|
||||
ActivityWatchModule
|
||||
ActivityWatchModule,
|
||||
NoDataModuleModule
|
||||
],
|
||||
providers: [
|
||||
NbSidebarService,
|
||||
@@ -96,8 +91,8 @@ import { ActivityWatchModule } from '../integrations';
|
||||
Store,
|
||||
TimeSlotQueueService,
|
||||
LanguageSelectorService,
|
||||
AlwaysOnService,
|
||||
AlwaysOnService
|
||||
],
|
||||
exports: [TimeTrackerComponent],
|
||||
exports: [TimeTrackerComponent]
|
||||
})
|
||||
export class TimeTrackerModule { }
|
||||
export class TimeTrackerModule {}
|
||||
|
||||
@@ -210,7 +210,8 @@
|
||||
"ESTIMATE": "You have not created any estimate.",
|
||||
"EVENT_TYPE": "You have not created any event type.",
|
||||
"PRODUCT_TYPE_NO_DATA": "You have not created any product type.",
|
||||
"TEAM_DASHBOARD": "You don't have any teams"
|
||||
"TEAM_DASHBOARD": "You don't have any teams",
|
||||
"PLUGIN": "You haven't install any plugin."
|
||||
},
|
||||
"TRANSACTION_TYPE": "Type",
|
||||
"AMOUNT": "Amount",
|
||||
@@ -4034,7 +4035,11 @@
|
||||
"SSL_CERT_FILE_INFO": "Path to Client Certificate file for SSL.",
|
||||
"SECURE": "Secure",
|
||||
"SECURE_INFO": "True, if you want to verify the SSL Certs."
|
||||
}
|
||||
},
|
||||
"PLUGINS": "Plugins",
|
||||
"PLUGIN_DESCRIPTION": "Plugins",
|
||||
"PLUGIN": "Plugin",
|
||||
"VERSION": "Version"
|
||||
},
|
||||
"WEEKLY_LIMIT_EXCEEDED": "Weekly limit exceeded",
|
||||
"OF_HRS": "Of {{ limit }} hrs",
|
||||
|
||||
@@ -11675,14 +11675,7 @@
|
||||
dependencies:
|
||||
image-size "*"
|
||||
|
||||
"@types/ioredis4@npm:@types/ioredis@^4.28.10":
|
||||
version "4.28.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff"
|
||||
integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ioredis@^4.27.1":
|
||||
"@types/ioredis4@npm:@types/ioredis@^4.28.10", "@types/ioredis@^4.27.1":
|
||||
version "4.28.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff"
|
||||
integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==
|
||||
@@ -12284,6 +12277,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.15.tgz#29c776daecf6f1935da9adda17509686bf979947"
|
||||
integrity sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==
|
||||
|
||||
"@types/unzipper@^0.10.9":
|
||||
version "0.10.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/unzipper/-/unzipper-0.10.9.tgz#ccbc393ecd1ec013dbe9bc6f13332dad0aa00a0f"
|
||||
integrity sha512-vHbmFZAw8emNAOVkHVbS3qBnbr0x/qHQZ+ei1HE7Oy6Tyrptl+jpqnOX+BF5owcu/HZLOV0nJK+K9sjs1Ox2JA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/uuid@^3.4.4":
|
||||
version "3.4.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.13.tgz#fe890e517fb840620be284ee213e81d702b1f76b"
|
||||
@@ -34611,7 +34611,7 @@ string-similarity@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
|
||||
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -34629,15 +34629,6 @@ string-width@^1.0.1:
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^2.0.0, string-width@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
@@ -34730,7 +34721,7 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -34758,13 +34749,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
@@ -36578,6 +36562,17 @@ unzipper@^0.10.11:
|
||||
readable-stream "~2.3.6"
|
||||
setimmediate "~1.0.4"
|
||||
|
||||
unzipper@^0.12.1:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.12.1.tgz#244f4a899a692fc8e76bf3bab6b6f7dd850487bb"
|
||||
integrity sha512-wjYe5XddA387WIAZbEMWOT0U8kw9yf1MdfLHccJer1y7a80t3DqVv0SHOAWV5NDBD2TUPj/pFYmK9tCeY6l9UQ==
|
||||
dependencies:
|
||||
bluebird "~3.4.1"
|
||||
duplexer2 "~0.1.4"
|
||||
fs-extra "^11.2.0"
|
||||
graceful-fs "^4.2.2"
|
||||
node-int64 "^0.4.0"
|
||||
|
||||
upath@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b"
|
||||
@@ -37590,7 +37585,7 @@ wordwrap@~0.0.2:
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||
integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -37625,15 +37620,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
||||
Reference in New Issue
Block a user