diff --git a/api/src/store/modules/paths.ts b/api/src/store/modules/paths.ts index 41a9e6a23..3a70d38e7 100644 --- a/api/src/store/modules/paths.ts +++ b/api/src/store/modules/paths.ts @@ -21,9 +21,7 @@ const initialState = { ), 'docker-autostart': '/var/lib/docker/unraid-autostart' as const, 'docker-socket': '/var/run/docker.sock' as const, - 'rclone-socket': resolvePath( - process.env.PATHS_RCLONE_SOCKET ?? ('/var/run/rclone.socket' as const) - ), + 'rclone-socket': resolvePath(process.env.PATHS_RCLONE_SOCKET ?? ('/var/run/rclone.socket' as const)), 'parity-checks': resolvePath( process.env.PATHS_PARITY_CHECKS ?? ('/boot/config/parity-checks.log' as const) ), diff --git a/api/src/unraid-api/auth/auth.service.ts b/api/src/unraid-api/auth/auth.service.ts index f5792597c..cf1db98ae 100644 --- a/api/src/unraid-api/auth/auth.service.ts +++ b/api/src/unraid-api/auth/auth.service.ts @@ -52,7 +52,11 @@ export class AuthService { async validateCookiesWithCsrfToken(request: FastifyRequest): Promise { try { - if (request.method !== 'GET' && !request.url.startsWith('/graphql/api/rclone-webgui/') && (!this.validateCsrfToken(request.headers['x-csrf-token'] || request.query.csrf_token))) { + if ( + request.method !== 'GET' && + !request.url.startsWith('/graphql/api/rclone-webgui/') && + !this.validateCsrfToken(request.headers['x-csrf-token'] || request.query.csrf_token) + ) { throw new UnauthorizedException('Invalid CSRF token'); } diff --git a/api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts b/api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts index 17fb78881..6f2c135d7 100644 --- a/api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts +++ b/api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts @@ -95,8 +95,8 @@ export class ConnectSettingsService { getState: store.getState, dispatch: store.dispatch, }); - } - return true; + } + return true; } async isSignedIn(): Promise { diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts index f67eae5cd..fb9c74a88 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts @@ -59,7 +59,6 @@ export class DockerService implements OnModuleInit { public async onModuleInit() { try { - this.logger.debug('Warming Docker cache on startup...'); await this.getContainers({ skipCache: true }); await this.getNetworks({ skipCache: true }); this.logger.debug('Docker cache warming complete.'); diff --git a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts b/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts index c440473fd..3b9fe0dcb 100644 --- a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts +++ b/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { FlashBackupResolver } from './flash-backup.resolver.js'; +import { FlashBackupResolver } from '@app/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.js'; import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js'; @Module({ diff --git a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts b/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts index a570179c9..6358fb30e 100644 --- a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts @@ -1,11 +1,10 @@ import { Inject, Logger } from '@nestjs/common'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; - import { FlashBackupStatus, InitiateFlashBackupInput, -} from './flash-backup.model.js'; +} from '@app/unraid-api/graph/resolvers/flash-backup/flash-backup.model.js'; import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; @Resolver() diff --git a/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.spec.ts b/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.spec.ts index 465bfb98d..5604e30fc 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.spec.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.spec.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, beforeEach } from 'vitest'; - -import { config as rawProviderConfig } from './config.js'; // Added .js extension -import { getProviderConfigSlice } from './rclone-jsonforms-config.js'; // Added .js extension -// Adjusted path assuming rclone.model.ts is sibling to jsonforms dir -import type { RCloneProviderOptionResponse } from '../rclone.model.js'; import type { JsonSchema7, Layout, SchemaBasedCondition } from '@jsonforms/core'; +import { beforeEach, describe, expect, it } from 'vitest'; + +// Adjusted path assuming rclone.model.ts is sibling to jsonforms dir +import type { RCloneProviderOptionResponse } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; +import { config as rawProviderConfig } from '@app/unraid-api/graph/resolvers/rclone/jsonforms/config.js'; // Added .js extension +import { getProviderConfigSlice } from '@app/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.js'; // Added .js extension // Placeholder type for UIElement if the original path doesn't resolve in tests // Make placeholder more specific to include expected properties @@ -15,7 +15,8 @@ type UIElement = { options?: Record; rule?: { effect: string; // RuleEffect is an enum, use string for simplicity in test type - condition: SchemaBasedCondition & { // Assert that it's SchemaBased + condition: SchemaBasedCondition & { + // Assert that it's SchemaBased scope: string; schema: JsonSchema7; }; @@ -37,14 +38,17 @@ interface RawProviderConfigEntry { // Process the raw config into the format expected by the functions under test const providerOptionsMap: Record = ( rawProviderConfig as RawProviderConfigEntry[] -).reduce((acc, provider) => { - if (provider.Name && Array.isArray(provider.Options)) { - // Ensure options conform to the expected type structure if necessary - // For now, we assume the structure matches RCloneProviderOptionResponse - acc[provider.Name] = provider.Options; - } - return acc; -}, {} as Record); +).reduce( + (acc, provider) => { + if (provider.Name && Array.isArray(provider.Options)) { + // Ensure options conform to the expected type structure if necessary + // For now, we assume the structure matches RCloneProviderOptionResponse + acc[provider.Name] = provider.Options; + } + return acc; + }, + {} as Record +); const providerNames = Object.keys(providerOptionsMap); @@ -72,7 +76,7 @@ describe('getProviderConfigSlice', () => { expect(result.elements).toEqual([]); }); - it('should return an empty slice if providerOptions are empty', () => { + it('should return an empty slice if providerOptions are empty', () => { const result = getProviderConfigSlice({ selectedProvider: testProvider, // Valid provider providerOptions: [], // Empty options @@ -82,7 +86,7 @@ describe('getProviderConfigSlice', () => { expect(result.elements).toEqual([]); }); - it('should return only standard options when type is \'standard\'', () => { + it("should return only standard options when type is 'standard'", () => { const result = getProviderConfigSlice({ selectedProvider: testProvider, providerOptions: s3Options, @@ -96,17 +100,17 @@ describe('getProviderConfigSlice', () => { expect(Object.keys(paramProps).length).toBeGreaterThan(0); // Check that all properties included are standard (Advanced !== true) - const standardOptions = s3Options.filter(opt => opt.Advanced !== true); - const uniqueStandardOptionNames = [...new Set(standardOptions.map(opt => opt.Name))]; + const standardOptions = s3Options.filter((opt) => opt.Advanced !== true); + const uniqueStandardOptionNames = [...new Set(standardOptions.map((opt) => opt.Name))]; // Assert against the count of UNIQUE standard option names expect(Object.keys(paramProps).length).toEqual(uniqueStandardOptionNames.length); // Check that each unique standard option name exists in the generated props - uniqueStandardOptionNames.forEach(name => { + uniqueStandardOptionNames.forEach((name) => { expect(paramProps[name]).toBeDefined(); // Find the first option with this name to check title (or implement more complex logic if needed) - const correspondingOption = standardOptions.find(opt => opt.Name === name); + const correspondingOption = standardOptions.find((opt) => opt.Name === name); expect(paramProps[name]?.title).toEqual(correspondingOption?.Name); }); @@ -116,20 +120,21 @@ describe('getProviderConfigSlice', () => { expect(result.elements.length).toEqual(uniqueStandardOptionNames.length); // Check elements based on unique names - uniqueStandardOptionNames.forEach(name => { + uniqueStandardOptionNames.forEach((name) => { // Use `as any` for type assertion on the result elements array const elementsArray = result.elements as any[]; // Find element by scope instead of label const expectedScope = `#/properties/parameters/properties/${name}`; const element = elementsArray.find((el) => el.scope === expectedScope); expect(element).toBeDefined(); // Check if element was found - if (element) { // Basic check + if (element) { + // Basic check expect(element.type).toEqual('Control'); } }); }); - it('should return only advanced options when type is \'advanced\'', () => { + it("should return only advanced options when type is 'advanced'", () => { const result = getProviderConfigSlice({ selectedProvider: testProvider, providerOptions: s3Options, @@ -143,16 +148,16 @@ describe('getProviderConfigSlice', () => { expect(Object.keys(paramProps).length).toBeGreaterThan(0); // Check that all properties included are advanced (Advanced === true) - const advancedOptions = s3Options.filter(opt => opt.Advanced === true); - const uniqueAdvancedOptionNames = [...new Set(advancedOptions.map(opt => opt.Name))]; + const advancedOptions = s3Options.filter((opt) => opt.Advanced === true); + const uniqueAdvancedOptionNames = [...new Set(advancedOptions.map((opt) => opt.Name))]; // Assert against the count of UNIQUE advanced option names expect(Object.keys(paramProps).length).toEqual(uniqueAdvancedOptionNames.length); // Check that each unique advanced option name exists in the generated props - uniqueAdvancedOptionNames.forEach(name => { + uniqueAdvancedOptionNames.forEach((name) => { expect(paramProps[name]).toBeDefined(); - const correspondingOption = advancedOptions.find(opt => opt.Name === name); + const correspondingOption = advancedOptions.find((opt) => opt.Name === name); expect(paramProps[name]?.title).toEqual(correspondingOption?.Name); }); @@ -162,14 +167,15 @@ describe('getProviderConfigSlice', () => { expect(result.elements.length).toEqual(uniqueAdvancedOptionNames.length); // Check elements based on unique names - uniqueAdvancedOptionNames.forEach(name => { + uniqueAdvancedOptionNames.forEach((name) => { // Use `as any` for type assertion on the result elements array const elementsArray = result.elements as any[]; // Find element by scope instead of label const expectedScope = `#/properties/parameters/properties/${name}`; const element = elementsArray.find((el) => el.scope === expectedScope); expect(element).toBeDefined(); // Check if element was found - if (element) { // Basic check + if (element) { + // Basic check expect(element.type).toEqual('Control'); } }); @@ -180,7 +186,7 @@ describe('getProviderConfigSlice', () => { const aliasOptions = providerOptionsMap[testProviderNoAdvanced]; // Pre-check: Verify that the chosen provider actually has no advanced options in our data - const hasAdvanced = aliasOptions?.some(opt => opt.Advanced === true); + const hasAdvanced = aliasOptions?.some((opt) => opt.Advanced === true); expect(hasAdvanced).toBe(false); // Ensure our assumption about 'alias' holds const result = getProviderConfigSlice({ diff --git a/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts b/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts index ebd4fa221..d0e76f384 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts @@ -1,4 +1,4 @@ -import type { Layout, SchemaBasedCondition } from '@jsonforms/core'; +import type { Layout, SchemaBasedCondition, ControlElement, LabelElement } from '@jsonforms/core'; import { JsonSchema7, RuleEffect } from '@jsonforms/core'; import { filter } from 'rxjs'; @@ -87,16 +87,30 @@ function translateRCloneOptionToJsonSchema({ */ function getBasicConfigSlice({ providerTypes }: { providerTypes: string[] }): SettingSlice { // Create UI elements for basic configuration (Step 1) - const basicConfigElements: UIElement[] = [ + const basicConfigElements: (ControlElement | LabelElement | Layout)[] = [ { - type: 'Control', + type: 'HorizontalLayout', scope: '#/properties/name', - label: 'Name of this remote (For your reference)', - options: { - placeholder: 'Enter a name', - format: 'string', - }, - }, + elements: [ + { + type: 'Label', + scope: '#/properties/name', + text: 'Remote Name', + options: { + // Optional styling + }, + } as LabelElement, + { + type: 'Control', + scope: '#/properties/name', + options: { + placeholder: 'Enter a name', + format: 'string', + description: 'Name to identify this remote configuration (e.g., my_google_drive). Use only letters, numbers, hyphens, and underscores.', + }, + } as ControlElement, + ], + } as Layout, { type: 'Control', scope: '#/properties/type', @@ -109,9 +123,35 @@ function getBasicConfigSlice({ providerTypes }: { providerTypes: string[] }): Se type: 'Label', text: 'Documentation Link', options: { - description: 'For more information, refer to the [RClone Config Documentation](https://rclone.org/commands/rclone_config/).', + description: + 'For more information, refer to the [RClone Config Documentation](https://rclone.org/commands/rclone_config/).', }, }, + // --- START: Added HorizontalLayout with visibility rule for testing --- + { + type: 'HorizontalLayout', + rule: { + effect: RuleEffect.HIDE, + condition: { + scope: '#/properties/name', + schema: { const: 'hide_me' }, // Hide if name is exactly 'hide_me' + }, + }, + elements: [ + { + type: 'Label', + text: 'Hidden Field Label', + } as LabelElement, + { + type: 'Control', + scope: '#/properties/hiddenField', // Needs corresponding schema property + options: { + placeholder: 'This field is hidden if name is hide_me', + }, + } as ControlElement, + ], + } as Layout, + // --- END: Added HorizontalLayout with visibility rule for testing --- ]; // Define the data schema for basic configuration @@ -130,6 +170,13 @@ function getBasicConfigSlice({ providerTypes }: { providerTypes: string[] }): Se default: providerTypes.length > 0 ? providerTypes[0] : '', enum: providerTypes, }, + // --- START: Added schema property for the hidden field --- + hiddenField: { + type: 'string', + title: 'Hidden Field', + description: 'This field should only be visible when the name is not \'hide_me\'', + }, + // --- END: Added schema property for the hidden field --- }; // Wrap the basic elements in a VerticalLayout marked for step 0 @@ -161,7 +208,7 @@ export function getProviderConfigSlice({ stepIndex: number; // Required step index for the rule }): SettingSlice { // Default properties when no provider is selected - let configProperties: DataSlice = {}; + const configProperties: DataSlice = {}; if (!selectedProvider || !providerOptions || providerOptions.length === 0) { return { @@ -465,7 +512,8 @@ export function buildRcloneConfigSchema({ text: 'Configure RClone Remote', options: { format: 'title', - description: 'This 3-step process will guide you through setting up your RClone remote configuration.', + description: + 'This 3-step process will guide you through setting up your RClone remote configuration.', }, }; diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts index 89ac10b4a..043bd2562 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts @@ -1,9 +1,9 @@ import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import crypto from 'crypto'; +import { ChildProcess } from 'node:child_process'; import { existsSync } from 'node:fs'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; -import { ChildProcess } from 'node:child_process'; import { execa } from 'execa'; import got from 'got'; @@ -115,7 +115,7 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy { '--rc-pass', this.rclonePassword, '--log-file', - logFilePath + logFilePath, ], { detached: false } // Keep attached to manage lifecycle ); @@ -129,7 +129,9 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy { // Handle unexpected exit this.rcloneProcess.on('exit', (code, signal) => { - this.logger.warn(`RClone process exited unexpectedly with code: ${code}, signal: ${signal}`); + this.logger.warn( + `RClone process exited unexpectedly with code: ${code}, signal: ${signal}` + ); this.rcloneProcess = null; this.isInitialized = false; }); diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts index 03304cc0c..52eebc33c 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts @@ -2,11 +2,10 @@ import { Injectable, Logger } from '@nestjs/common'; import { type Layout } from '@jsonforms/core'; +import { buildRcloneConfigSchema } from '@app/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.js'; +import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js'; import { RCloneProviderOptionResponse } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; -import { buildRcloneConfigSchema } from './jsonforms/rclone-jsonforms-config.js'; -import { RCloneApiService } from './rclone-api.service.js'; - /** * Service responsible for generating form UI schemas and form logic */ diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts index ef6afcf32..89decfaed 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts @@ -59,10 +59,10 @@ export class RCloneBackupConfigForm { @Field(() => GraphQLJSON) uiSchema!: Layout; - + @Field(() => String, { nullable: true }) providerType?: string; - + @Field(() => GraphQLJSON, { nullable: true }) parameters?: Record; } diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts index 8f1071cd5..5bf054236 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts @@ -1,18 +1,13 @@ import { Module } from '@nestjs/common'; -import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js'; import { RCloneFormService } from '@app/unraid-api/graph/resolvers/rclone/rclone-form.service.js'; import { RCloneBackupSettingsResolver } from '@app/unraid-api/graph/resolvers/rclone/rclone.resolver.js'; +import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; @Module({ - imports: [], - providers: [ - RCloneService, - RCloneApiService, - RCloneFormService, - RCloneBackupSettingsResolver, - ], - exports: [RCloneService, RCloneApiService] + imports: [], + providers: [RCloneService, RCloneApiService, RCloneFormService, RCloneBackupSettingsResolver], + exports: [RCloneService, RCloneApiService], }) export class RCloneModule {} diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts index 82e12fdee..3f5333a8c 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts @@ -1,22 +1,23 @@ import { Logger } from '@nestjs/common'; import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; - - import { GraphQLJSON } from 'graphql-scalars'; - - -import { AuthActionVerb, AuthPossession, UsePermissions } from '@app/unraid-api/graph/directives/use-permissions.directive.js'; +import { + AuthActionVerb, + AuthPossession, + UsePermissions, +} from '@app/unraid-api/graph/directives/use-permissions.directive.js'; import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js'; -import { CreateRCloneRemoteInput, RCloneBackupConfigForm, RCloneBackupSettings, RCloneRemote } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; - - - -import { RCloneFormService } from './rclone-form.service.js'; -import { RCloneService } from './rclone.service.js'; - +import { RCloneFormService } from '@app/unraid-api/graph/resolvers/rclone/rclone-form.service.js'; +import { + CreateRCloneRemoteInput, + RCloneBackupConfigForm, + RCloneBackupSettings, + RCloneRemote, +} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; +import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; @Resolver(() => RCloneBackupSettings) export class RCloneBackupSettingsResolver { @@ -86,4 +87,4 @@ export class RCloneBackupSettingsResolver { return []; } } -} \ No newline at end of file +} diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone.service.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone.service.ts index 24249dadc..fa681b6ce 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone.service.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone.service.ts @@ -1,19 +1,11 @@ import { Injectable, Logger } from '@nestjs/common'; - - import { type Layout } from '@jsonforms/core'; - - import type { SettingSlice } from '@app/unraid-api/types/json-forms.js'; import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js'; import { RCloneFormService } from '@app/unraid-api/graph/resolvers/rclone/rclone-form.service.js'; - - - - /** * Types for rclone backup configuration UI */ @@ -69,7 +61,7 @@ export class RCloneService { const providersResponse = await this.rcloneApiService.getProviders(); if (providersResponse) { // Extract provider types - this._providerTypes = providersResponse.map((provider) => provider.Name); + this._providerTypes = providersResponse.map((provider) => provider.Name); this._providerOptions = providersResponse; this.logger.debug(`Loaded ${this._providerTypes.length} provider types`); } @@ -95,4 +87,4 @@ export class RCloneService { async getConfiguredRemotes(): Promise { return this.rcloneApiService.listRemotes(); } -} \ No newline at end of file +} diff --git a/api/src/unraid-api/rest/rest.controller.ts b/api/src/unraid-api/rest/rest.controller.ts index 15f681514..bfece7607 100644 --- a/api/src/unraid-api/rest/rest.controller.ts +++ b/api/src/unraid-api/rest/rest.controller.ts @@ -1,19 +1,17 @@ -import { Controller, Get, Logger, Param, Res, Req, All } from '@nestjs/common'; +import { All, Controller, Get, Logger, Param, Req, Res } from '@nestjs/common'; +import got from 'got'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import type { FastifyReply, FastifyRequest } from '@app/unraid-api/types/fastify.js'; import { Public } from '@app/unraid-api/auth/public.decorator.js'; import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; import { RestService } from '@app/unraid-api/rest/rest.service.js'; -import got from 'got'; @Controller() export class RestController { protected logger = new Logger(RestController.name); - constructor( - private readonly restService: RestService, - ) {} + constructor(private readonly restService: RestService) {} @Get('/') @Public() @@ -56,7 +54,7 @@ export class RestController { return res.status(500).send(`Error: Failed to get customizations`); } } -/* + /* @All('/graphql/api/rclone-webgui/*') @UsePermissions({ action: AuthActionVerb.READ, diff --git a/api/src/unraid-api/rest/rest.module.ts b/api/src/unraid-api/rest/rest.module.ts index b6065369c..3ce7f4907 100644 --- a/api/src/unraid-api/rest/rest.module.ts +++ b/api/src/unraid-api/rest/rest.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; +import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js'; import { RestController } from '@app/unraid-api/rest/rest.controller.js'; import { RestService } from '@app/unraid-api/rest/rest.service.js'; -import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js'; + @Module({ imports: [RCloneModule], controllers: [RestController], diff --git a/api/src/unraid-api/types/json-forms.test.ts b/api/src/unraid-api/types/json-forms.test.ts index 975cc1e96..c8c01c633 100644 --- a/api/src/unraid-api/types/json-forms.test.ts +++ b/api/src/unraid-api/types/json-forms.test.ts @@ -1,6 +1,7 @@ -import { describe, it, expect } from 'vitest'; -import { mergeSettingSlices, createEmptySettingSlice } from './json-forms.js'; -import type { SettingSlice, DataSlice, UIElement } from './json-forms.js'; +import { describe, expect, it } from 'vitest'; + +import type { DataSlice, SettingSlice, UIElement } from '@app/unraid-api/types/json-forms.js'; +import { createEmptySettingSlice, mergeSettingSlices } from '@app/unraid-api/types/json-forms.js'; describe('mergeSettingSlices', () => { it('should return an empty slice when merging an empty array', () => { @@ -87,4 +88,4 @@ describe('mergeSettingSlices', () => { expect(mergedSlice.properties).toEqual(expectedProperties); expect(mergedSlice.elements).toEqual(expectedElements); }); -}); \ No newline at end of file +}); diff --git a/unraid-ui/src/composables/useTeleport.ts b/unraid-ui/src/composables/useTeleport.ts index 066c31193..41d2d5eb1 100644 --- a/unraid-ui/src/composables/useTeleport.ts +++ b/unraid-ui/src/composables/useTeleport.ts @@ -4,17 +4,14 @@ const useTeleport = () => { const teleportTarget = ref('#modals'); const determineTeleportTarget = () => { - const myModalsComponent = document.querySelector('unraid-modals') || document.querySelector('uui-modals'); - console.log('myModalsComponent', myModalsComponent, 'has shadowRoot', myModalsComponent?.shadowRoot); + const myModalsComponent = + document.querySelector('unraid-modals') || document.querySelector('uui-modals'); if (!myModalsComponent?.shadowRoot) return; const potentialTarget = myModalsComponent.shadowRoot.querySelector('#modals'); if (!potentialTarget) return; - console.log('potentialTarget', potentialTarget); - teleportTarget.value = potentialTarget as HTMLElement; - console.log('[determineTeleportTarget] teleportTarget', teleportTarget.value); }; onMounted(() => { diff --git a/unraid-ui/src/forms/ControlLayout.vue b/unraid-ui/src/forms/ControlLayout.vue deleted file mode 100644 index 2b40c99fa..000000000 --- a/unraid-ui/src/forms/ControlLayout.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/unraid-ui/src/forms/ControlWrapper.vue b/unraid-ui/src/forms/ControlWrapper.vue new file mode 100644 index 000000000..f0d400acb --- /dev/null +++ b/unraid-ui/src/forms/ControlWrapper.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/unraid-ui/src/forms/FormErrors.vue b/unraid-ui/src/forms/FormErrors.vue new file mode 100644 index 000000000..b6fb905c4 --- /dev/null +++ b/unraid-ui/src/forms/FormErrors.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/unraid-ui/src/forms/HorizontalLayout.vue b/unraid-ui/src/forms/HorizontalLayout.vue new file mode 100644 index 000000000..d9a0cefa5 --- /dev/null +++ b/unraid-ui/src/forms/HorizontalLayout.vue @@ -0,0 +1,48 @@ + + + diff --git a/unraid-ui/src/forms/InputField.vue b/unraid-ui/src/forms/InputField.vue index 97385dce7..677efe7fc 100644 --- a/unraid-ui/src/forms/InputField.vue +++ b/unraid-ui/src/forms/InputField.vue @@ -1,6 +1,5 @@ diff --git a/unraid-ui/src/forms/NumberField.vue b/unraid-ui/src/forms/NumberField.vue index b649b5bd8..a615bb82f 100644 --- a/unraid-ui/src/forms/NumberField.vue +++ b/unraid-ui/src/forms/NumberField.vue @@ -5,7 +5,6 @@ import { NumberFieldIncrement, NumberFieldInput, } from '@/components/form/number'; -import ControlLayout from '@/forms/ControlLayout.vue'; import { cn } from '@/lib/utils'; import type { ControlElement } from '@jsonforms/core'; import type { RendererProps } from '@jsonforms/vue'; @@ -34,7 +33,6 @@ const classOverride = computed(() => { diff --git a/unraid-ui/src/forms/PreconditionsLabel.vue b/unraid-ui/src/forms/PreconditionsLabel.vue index 23a26aa36..e34012f0a 100644 --- a/unraid-ui/src/forms/PreconditionsLabel.vue +++ b/unraid-ui/src/forms/PreconditionsLabel.vue @@ -1,5 +1,4 @@ diff --git a/unraid-ui/src/forms/Select.vue b/unraid-ui/src/forms/Select.vue index 42440dad7..5dfb87471 100644 --- a/unraid-ui/src/forms/Select.vue +++ b/unraid-ui/src/forms/Select.vue @@ -9,7 +9,6 @@ import { SelectValue, } from '@/components/form/select'; import useTeleport from '@/composables/useTeleport'; -import ControlLayout from '@/forms/ControlLayout.vue'; import type { ControlElement } from '@jsonforms/core'; import { useJsonFormsControl } from '@jsonforms/vue'; import type { RendererProps } from '@jsonforms/vue'; @@ -42,39 +41,38 @@ const onSelectOpen = () => { diff --git a/unraid-ui/src/forms/SteppedLayout.vue b/unraid-ui/src/forms/SteppedLayout.vue index 5f3a32ded..a52e4ca7f 100644 --- a/unraid-ui/src/forms/SteppedLayout.vue +++ b/unraid-ui/src/forms/SteppedLayout.vue @@ -11,47 +11,56 @@ import { type JsonSchema, type Layout, type UISchemaElement, - // Actions, // No longer needed + Actions, type CoreActions, type JsonFormsSubStates } from '@jsonforms/core'; import { DispatchRenderer, useJsonFormsLayout, type RendererProps } from '@jsonforms/vue'; -import { computed, ref } from 'vue'; // Import ref, remove inject/onMounted +import { computed, inject } from 'vue'; // Define props based on RendererProps const props = defineProps>(); -// --- JSON Forms Composables and Context --- +// --- JSON Forms Context Injection --- +const jsonforms = inject('jsonforms'); +const dispatch = inject<(action: CoreActions) => void>('dispatch'); // Inject dispatch separately + +if (!jsonforms || !dispatch) { + throw new Error("'jsonforms' or 'dispatch' context wasn't provided. Are you within JsonForms?"); +} +const { core } = jsonforms; // Extract core state + +// --- Layout Specific Composables --- const { layout } = useJsonFormsLayout(props); -// --- Step Configuration --- - -// Expect options.steps: [{ label: string, description: string }, ...] +// --- Step Configuration --- Use props.uischema const stepsConfig = computed(() => props.uischema.options?.steps || []); +const numSteps = computed(() => stepsConfig.value.length); -// --- Current Step Logic --- - -// Use local state for the current step index -const localCurrentStep = ref(0); - -// currentStep now reflects the local state -const currentStep = computed(() => localCurrentStep.value); +// --- Current Step Logic --- Use injected core.data +const currentStep = computed(() => { + console.log('[SteppedLayout] currentStep computed. core.data.configStep:', core?.data?.configStep); + return core!.data?.configStep ?? 0; +}); +const isLastStep = computed(() => currentStep.value === numSteps.value - 1); // --- Step Update Logic --- - const updateStep = (newStep: number) => { // Validate step index bounds - if (newStep < 0 || newStep >= stepsConfig.value.length) { + if (newStep < 0 || newStep >= numSteps.value) { return; } - // Simply update the local state - localCurrentStep.value = newStep; + // Update the 'configStep' property in the JSON Forms data + dispatch(Actions.update('configStep', () => newStep)); }; // --- Filtered Elements for Current Step --- - const currentStepElements = computed(() => { - return (props.uischema.elements || []).filter((element: UISchemaElement) => { + const filtered = (props.uischema.elements || []).filter((element: UISchemaElement) => { return element.options?.step === currentStep.value; }); + + console.log(`[SteppedLayout] currentStepElements computed for step ${currentStep.value}. Found elements:`, JSON.stringify(filtered.map(el => ({ type: el.type, scope: (el as any).scope })))); // Log type/scope + + return filtered; }); // --- Stepper State Logic --- @@ -65,7 +74,7 @@ const getStepState = (stepIndex: number): StepState => {