chore: lint

This commit is contained in:
Eli Bosley
2025-04-21 15:44:16 -04:00
parent dada8e63c5
commit d3adbafbff
35 changed files with 680 additions and 500 deletions

View File

@@ -1357,16 +1357,20 @@ type RCloneBackupConfigForm {
id: ID!
dataSchema: JSON!
uiSchema: JSON!
providerType: String
parameters: JSON
}
type RCloneBackupSettings {
configForm(providerType: String, parameters: JSON): RCloneBackupConfigForm!
configForm(formOptions: RCloneConfigFormInput): RCloneBackupConfigForm!
drives: [RCloneDrive!]!
remotes: [String!]!
}
input RCloneConfigFormInput {
providerType: String
showAdvanced: Boolean = false
parameters: JSON
}
type RCloneRemote {
name: String!
type: String!

View File

@@ -1,4 +1,4 @@
import type { Layout, SchemaBasedCondition, ControlElement, LabelElement } from '@jsonforms/core';
import type { ControlElement, LabelElement, Layout, Rule, SchemaBasedCondition } from '@jsonforms/core';
import { JsonSchema7, RuleEffect } from '@jsonforms/core';
import { filter } from 'rxjs';
@@ -6,6 +6,53 @@ import type { DataSlice, SettingSlice, UIElement } from '@app/unraid-api/types/j
import { RCloneProviderOptionResponse } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js';
// --- START: Added Helper Function ---
/**
* Creates a HorizontalLayout containing a Label and a Control element.
*/
function createLabeledControl({
scope,
label,
description,
controlOptions,
labelOptions,
layoutOptions,
rule,
}: {
scope: string;
label: string;
description?: string;
controlOptions: ControlElement['options'];
labelOptions?: LabelElement['options'];
layoutOptions?: Layout['options'];
rule?: Rule;
}): Layout {
const layout: Layout & { scope?: string } = {
type: 'UnraidSettingsLayout',
scope: scope, // Apply scope to the layout for potential rules/visibility based on the field itself
options: layoutOptions,
elements: [
{
type: 'Label',
text: label,
scope: scope, // Scope might be needed for specific label behaviors
options: { ...labelOptions, description },
} as LabelElement,
{
type: 'Control',
scope: scope,
options: controlOptions,
} as ControlElement,
],
};
// Conditionally add the rule to the layout if provided
if (rule) {
layout.rule = rule;
}
return layout;
}
// --- END: Added Helper Function ---
/**
* Translates RClone config option to JsonSchema properties
*/
@@ -43,8 +90,9 @@ function translateRCloneOptionToJsonSchema({
examples: option.Examples?.map((example) => example.Value),
isPassword: option.IsPassword,
});
if (format && format !== schema.type) {
if (format && format !== schema.type && format !== 'combobox') {
// Don't add format if it's just the type (e.g., 'number')
// Don't add non-standard UI hints like 'combobox' to the schema format
schema.format = format;
}
@@ -66,13 +114,13 @@ function translateRCloneOptionToJsonSchema({
case 'sizesuffix':
// Pattern allows 'off' or digits followed by optional size units (K, M, G, T, P) and optional iB/B
// Allows multiple concatenated values like 1G100M
schema.pattern = '^(off|(\d+([KMGTPE]i?B?)?)+)$';
schema.pattern = '^(off|(d+([KMGTPE]i?B?)?)+)$';
schema.errorMessage = 'Invalid size format. Examples: "10G", "100M", "1.5GiB", "off".';
break;
case 'duration':
// Pattern allows 'off' or digits (with optional decimal) followed by time units (ns, us, ms, s, m, h)
// Allows multiple concatenated values like 1h15m
schema.pattern = '^(off|(\d+(\.\d+)?(ns|us|\u00b5s|ms|s|m|h))+)$'; // µs is µs
schema.pattern = '^(off|(d+(.d+)?(ns|us|\u00b5s|ms|s|m|h))+)$';
schema.errorMessage =
'Invalid duration format. Examples: "10s", "1.5m", "100ms", "1h15m", "off".';
break;
@@ -87,38 +135,28 @@ function translateRCloneOptionToJsonSchema({
*/
function getBasicConfigSlice({ providerTypes }: { providerTypes: string[] }): SettingSlice {
// Create UI elements for basic configuration (Step 1)
const basicConfigElements: (ControlElement | LabelElement | Layout)[] = [
{
type: 'HorizontalLayout',
const basicConfigElements: UIElement[] = [
// --- START: Refactored 'name' field using helper ---
createLabeledControl({
scope: '#/properties/name',
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',
label: 'Remote Name',
description:
'Name to identify this remote configuration (e.g., my_google_drive). Use only letters, numbers, hyphens, and underscores.',
controlOptions: {
placeholder: 'Enter a name',
format: 'string',
},
// Add layoutOptions if needed, e.g., layoutOptions: { style: 'margin-bottom: 1em;' }
}),
// --- END: Refactored 'name' field using helper ---
// --- START: Refactored 'type' field using helper ---
createLabeledControl({
scope: '#/properties/type',
label: 'Storage Provider Type',
options: {
description: 'Select the cloud storage provider to use for this remote.',
},
},
description: 'Select the cloud storage provider to use for this remote.',
controlOptions: {},
}),
{
type: 'Label',
text: 'Documentation Link',
@@ -126,35 +164,20 @@ function getBasicConfigSlice({ providerTypes }: { providerTypes: string[] }): Se
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'
},
} as LabelElement,
createLabeledControl({
scope: '#/properties/showAdvanced',
label: 'Show Advanced Options',
description: 'Display additional configuration options for experts.',
controlOptions: {
toggle: true,
},
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 ---
layoutOptions: {
style: 'margin-top: 1em;',
},
}),
];
// Define the data schema for basic configuration
const basicConfigProperties: Record<string, JsonSchema7> = {
name: {
type: 'string',
@@ -170,41 +193,39 @@ 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\'',
showAdvanced: {
type: 'boolean',
title: 'Show Advanced Options',
description: 'Whether to show advanced configuration options.',
default: false,
},
// --- END: Added schema property for the hidden field ---
};
// Wrap the basic elements in a VerticalLayout marked for step 0
const verticalLayoutElement: UIElement = {
type: 'VerticalLayout',
elements: basicConfigElements,
options: { step: 0 }, // Assign to step 0
options: { step: 0 },
};
return {
properties: basicConfigProperties as unknown as DataSlice,
elements: [verticalLayoutElement], // Return the VerticalLayout as the single element
elements: [verticalLayoutElement],
};
}
/**
* Step 2/3: Provider-specific configuration based on the selected provider and type (standard/advanced).
* Step 1/2: Provider-specific configuration (standard or advanced).
* Returns a SettingSlice containing properties and a VerticalLayout UI element with options.step = stepIndex.
*/
export function getProviderConfigSlice({
selectedProvider,
providerOptions,
type = 'standard',
stepIndex, // Added stepIndex parameter
isAdvancedStep, // Flag to determine if this is for the advanced step
stepIndex,
}: {
selectedProvider: string;
providerOptions: RCloneProviderOptionResponse[];
type?: 'standard' | 'advanced';
isAdvancedStep: boolean; // True if fetching advanced options, false for standard
stepIndex: number; // Required step index for the rule
}): SettingSlice {
// Default properties when no provider is selected
@@ -217,14 +238,16 @@ export function getProviderConfigSlice({
};
}
// Filter options based on the type (standard/advanced)
// Filter options based on whether we are fetching standard or advanced options
const filteredOptions = providerOptions.filter((option) => {
if (type === 'advanced' && option.Advanced === true) {
return true;
} else if (type === 'standard' && option.Advanced !== true) {
return true;
// If fetching advanced, include only options marked as Advanced
if (isAdvancedStep) {
return option.Advanced === true;
}
// If fetching standard, include only options *not* marked as Advanced
else {
return option.Advanced !== true;
}
return false;
});
// Ensure uniqueness based on Name *within this slice* to prevent overwrites
@@ -232,7 +255,9 @@ export function getProviderConfigSlice({
if (!acc.find((item) => item.Name === current.Name)) {
acc.push(current);
} else {
console.warn(`Duplicate RClone option name skipped in ${type} slice: ${current.Name}`);
console.warn(
`Duplicate RClone option name skipped in ${isAdvancedStep ? 'advanced' : 'standard'} slice: ${current.Name}`
);
}
return acc;
}, [] as RCloneProviderOptionResponse[]);
@@ -246,79 +271,113 @@ export function getProviderConfigSlice({
}
// Create dynamic UI control elements based on unique provider options
const controlElements = uniqueOptionsByName.map<UIElement>((option) => {
const format = getJsonFormElementForType({
rcloneType: option.Type,
examples: option.Examples?.map((example) => example.Value),
isPassword: option.IsPassword,
});
const controlElements = uniqueOptionsByName
// Filter out elements that are always hidden (Hide=1 without a provider filter)
.filter((option) => {
const providerFilter = option.Provider?.trim();
return !(option.Hide === 1 && !providerFilter);
})
.map((option): UIElement => {
const format = getJsonFormElementForType({
rcloneType: option.Type,
examples: option.Examples?.map((example) => example.Value),
isPassword: option.IsPassword,
});
const controlOptions: Record<string, any> = {
placeholder: option.Default?.toString() || '',
help: option.Help || '',
required: option.Required || false,
format,
hide: option.Hide === 1,
};
const controlOptions: Record<string, any> = {
placeholder: option.Default?.toString() || '',
// help: option.Help || '', // Help/Description should now be part of the control options for tooltip/aria
required: option.Required || false,
format, // Pass format hint
// hide: option.Hide === 1, // Hiding is handled by the rule effect below if needed, or potentially JSON Forms renderer behavior
};
// Use examples for placeholder if available
if (option.Examples && option.Examples.length > 0) {
const exampleValues = option.Examples.map((example) => example.Value).join(', ');
controlOptions.placeholder = `e.g., ${exampleValues}`;
}
// Only add toggle option for boolean fields without examples
if (format === 'checkbox' && (!option.Examples || option.Examples.length === 0)) {
controlOptions.toggle = true;
}
// Add examples as suggestions for combobox
if (format === 'combobox' && option.Examples && option.Examples.length > 0) {
controlOptions.suggestions = option.Examples.map((example) => ({
value: example.Value,
label: example.Value, // Set label to just the value
tooltip: example.Help || '', // Add tooltip with help text
}));
}
// --- Start: Add dynamic visibility rule based on Provider --- //
let providerRule: { effect: RuleEffect; condition: SchemaBasedCondition } | undefined =
undefined;
const providerFilter = option.Provider?.trim();
if (providerFilter) {
const isNegated = providerFilter.startsWith('!');
const providers = (isNegated ? providerFilter.substring(1) : providerFilter)
.split(',')
.map((p) => p.trim())
.filter((p) => p);
if (providers.length > 0) {
const conditionSchema = isNegated
? { not: { enum: providers } } // Show if type is NOT in the list
: { enum: providers }; // Show if type IS in the list
providerRule = {
effect: RuleEffect.SHOW,
condition: {
scope: '#/properties/type',
schema: conditionSchema,
} as SchemaBasedCondition,
};
// Use examples for placeholder if available
if (option.Examples && option.Examples.length > 0) {
const exampleValues = option.Examples.map((example) => example.Value).join(', ');
controlOptions.placeholder = `e.g., ${exampleValues}`;
}
}
// --- End: Add dynamic visibility rule based on Provider --- //
const uiElement: UIElement = {
type: 'Control',
scope: `#/properties/parameters/properties/${option.Name}`,
label: option.Help || option.Name,
options: controlOptions,
// Add the provider-specific rule if it was generated
...(providerRule && { rule: providerRule }),
};
return uiElement;
});
// Only add toggle option for boolean fields without examples
if (format === 'checkbox' && (!option.Examples || option.Examples.length === 0)) {
controlOptions.toggle = true;
}
// Add examples as suggestions for combobox
if (format === 'combobox' && option.Examples && option.Examples.length > 0) {
// Check if the underlying type is boolean, handle undefined option.Type
const isBooleanType = getJsonSchemaType(option.Type ?? '') === 'boolean';
controlOptions.suggestions = option.Examples.map((example) => ({
// Parse string "true"/"false" to boolean if the type is boolean, handle potential null/undefined
value: isBooleanType
? String(example.Value ?? '').toLowerCase() === 'true'
: example.Value,
// Ensure label is also a string, even if value is null/undefined
label: String(example.Value ?? ''),
tooltip: example.Help || '',
}));
}
// --- Start: Add dynamic visibility rule based on Provider --- //
let providerRule: Rule | undefined = undefined; // Define rule type explicitly
const providerFilter = option.Provider?.trim();
if (providerFilter) {
const isNegated = providerFilter.startsWith('!');
const providers = (isNegated ? providerFilter.substring(1) : providerFilter)
.split(',')
.map((p) => p.trim())
.filter((p) => p);
if (providers.length > 0) {
const conditionSchema = isNegated
? { not: { enum: providers } }
: { enum: providers };
// Show/Hide logic: If option.Hide === 1, we HIDE, otherwise default SHOW based on provider type
const effect = option.Hide === 1 ? RuleEffect.HIDE : RuleEffect.SHOW;
providerRule = {
effect: effect,
condition: {
scope: '#/properties/type',
schema: conditionSchema,
} as SchemaBasedCondition,
};
}
} else if (option.Hide === 1) {
// If no provider filter, but Hide is set, create a rule to always hide
// This needs a condition that is always true, which is tricky.
// A simple approach is a condition that will likely always evaluate based on the schema.
// Alternatively, rely on JSON Forms renderer interpretation of a missing rule but Hide=1 property.
// For robustness, let's add a rule that hides if the field itself exists (which it always will if rendered).
// Note: This specific 'always hide' might need adjustment based on JSON Forms implementation details.
// A more direct approach might be filtering these out *before* mapping if always hidden.
// Let's assume for now `option.Hide=1` without a provider filter means it's *always* hidden.
// We can filter these out instead of creating a complex rule.
// Revisit this if hidden fields without provider filters are needed dynamically.
// --- Simplified Logic: Filter out permanently hidden fields ---
if (option.Hide === 1 && !providerFilter) {
// Skip creating a UI element for this option entirely
// This case is now handled by the filter above
}
}
// --- End: Add dynamic visibility rule based on Provider --- //
// --- Use the helper function ---
const labeledControl = createLabeledControl({
scope: `#/properties/parameters/properties/${option.Name}`,
// Use Name as fallback label if Help is empty, otherwise use Help for label
label: option.Name, // Use Name for the label text
description: option.Help || undefined,
controlOptions: controlOptions,
rule: providerRule, // Apply the rule to the HorizontalLayout wrapper
});
// Layout is a valid UIElement, no cast needed if filter handles nulls
return labeledControl;
});
// Create dynamic properties schema based on unique provider options
const paramProperties: Record<string, JsonSchema7> = {};
@@ -347,8 +406,8 @@ export function getProviderConfigSlice({
// Wrap the control elements in a VerticalLayout marked for the specified stepIndex
const verticalLayoutElement: UIElement = {
type: 'VerticalLayout',
elements: controlElements,
options: { step: stepIndex }, // Assign to the specified stepIndex
elements: controlElements, // Use the refactored elements
options: { step: stepIndex, showDividers: true }, // Assign stepIndex and add showDividers
};
return {
@@ -358,7 +417,7 @@ export function getProviderConfigSlice({
}
/**
* Helper function to convert RClone option types to JSON Schema types
* Helper function to convert RClone type to a basic JSON Schema type string (e.g., 'string', 'number', 'boolean').
*/
function getJsonSchemaType(rcloneType: string): string {
switch (rcloneType?.toLowerCase()) {
@@ -400,19 +459,18 @@ function getJsonFormElementForType({
switch (rcloneType?.toLowerCase()) {
case 'int':
return 'number'; // Use NumberField
case 'size':
return 'number'; // Use NumberField
// Schema type 'integer'/'number' is sufficient.
// UI framework should infer NumberField from schema type.
return undefined;
case 'sizesuffix':
return undefined; // Use default InputField (via isStringControl)
case 'duration':
return undefined; // Use default InputField (via isStringControl)
case 'bool':
// Only use checkbox/toggle for boolean fields without examples
if (!examples || examples.length === 0) {
return 'checkbox';
}
return 'combobox'; // Use combobox for boolean fields with examples
// ALWAYS use checkbox/toggle for boolean fields, regardless of examples.
// RClone examples ("true"/"false") don't map well to UI boolean controls.
return 'toggle';
case 'text':
// Consider 'textarea' format later if needed
return undefined; // Use default InputField (via isStringControl)
@@ -436,74 +494,95 @@ export function buildRcloneConfigSchema({
providerTypes = [],
selectedProvider = '',
providerOptions = {},
showAdvanced = false,
}: {
providerTypes?: string[];
selectedProvider?: string;
providerOptions?: Record<string, RCloneProviderOptionResponse[]>;
showAdvanced?: boolean;
}): {
dataSchema: Record<string, any>;
dataSchema: { properties: DataSlice; type: 'object' };
uiSchema: Layout;
} {
// --- Schema Definition ---
// Define the step control property - REMOVED as SteppedLayout uses local state
// const stepControlProperty: Record<string, JsonSchema7> = {
// configStep: {
// type: 'number',
// minimum: 0,
// maximum: 2, // 3 steps: 0, 1, 2
// default: 0,
// },
// };
// --- Step Content Generation ---
const optionsForProvider = providerOptions[selectedProvider] || [];
const slicesToMerge: SettingSlice[] = [];
// Step 0: Basic Config
// Step 0: Basic Config (Always included)
const basicSlice = getBasicConfigSlice({ providerTypes });
slicesToMerge.push(basicSlice);
// Step 1: Standard Provider Config
const standardConfigSlice = getProviderConfigSlice({
selectedProvider,
providerOptions: optionsForProvider,
type: 'standard',
stepIndex: 1, // Assign to step 1
});
// Step 1: Standard Provider Config (Always included if provider selected)
if (selectedProvider && optionsForProvider.length > 0) {
const standardConfigSlice = getProviderConfigSlice({
selectedProvider,
providerOptions: optionsForProvider,
isAdvancedStep: false, // Fetch standard options
stepIndex: 1,
});
// Only add if there are actual standard options
if (
standardConfigSlice.elements.length > 0 ||
Object.keys(standardConfigSlice.properties).length > 0
) {
slicesToMerge.push(standardConfigSlice);
}
}
// Step 2: Advanced Provider Config
const advancedConfigSlice = getProviderConfigSlice({
selectedProvider,
providerOptions: optionsForProvider,
type: 'advanced',
stepIndex: 2, // Assign to step 2
});
// Step 2: Advanced Provider Config (Conditionally included)
let advancedConfigSlice: SettingSlice | null = null;
if (showAdvanced && selectedProvider && optionsForProvider.length > 0) {
advancedConfigSlice = getProviderConfigSlice({
selectedProvider,
providerOptions: optionsForProvider,
isAdvancedStep: true, // Fetch advanced options
stepIndex: 2,
});
// Only add if there are actual advanced options
if (
advancedConfigSlice.elements.length > 0 ||
Object.keys(advancedConfigSlice.properties).length > 0
) {
slicesToMerge.push(advancedConfigSlice);
}
}
// Merge all properties: basic + standard + advanced
const mergedProperties = mergeSettingSlices([basicSlice, standardConfigSlice, advancedConfigSlice]);
// Merge all relevant slices
const mergedSlices = mergeSettingSlices(slicesToMerge);
// Construct the final dataSchema
const dataSchema = {
// Add explicit type annotation to satisfy stricter type checking
const dataSchema: { properties: DataSlice; type: 'object' } = {
type: 'object',
properties: mergedProperties.properties,
properties: mergedSlices.properties,
// Add required fields if necessary, e.g., ['name', 'type']
// required: ['name', 'type'], // Example: Make name and type required globally
};
// --- UI Schema Definition ---
// Define the SteppedLayout UI element, now containing step content elements
// Define steps based on whether advanced options are shown
const steps = [{ label: 'Set up Remote Config', description: 'Name and provider selection' }];
if (selectedProvider) {
steps.push({ label: 'Set up Drive', description: 'Provider-specific configuration' });
}
if (
showAdvanced &&
advancedConfigSlice &&
(advancedConfigSlice.elements.length > 0 ||
Object.keys(advancedConfigSlice.properties).length > 0)
) {
steps.push({ label: 'Advanced Config', description: 'Optional advanced settings' });
}
// Define the SteppedLayout UI element
const steppedLayoutElement: UIElement = {
type: 'SteppedLayout',
options: {
steps: [
{ label: 'Set up Remote Config', description: 'Name and provider selection' },
{ label: 'Set up Drive', description: 'Provider-specific configuration' },
{ label: 'Advanced Config', description: 'Optional advanced settings' },
],
steps: steps, // Use dynamically generated steps
},
// Nest the step content elements directly inside the SteppedLayout
elements: mergedProperties.elements,
elements: mergedSlices.elements,
};
// Define the overall title label

View File

@@ -6,7 +6,7 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { execa } from 'execa';
import got from 'got';
import got, { HTTPError } from 'got';
import {
RCloneProviderOptionResponse,
@@ -270,15 +270,20 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
* Create a new remote configuration
*/
async createRemote(name: string, type: string, parameters: Record<string, any> = {}): Promise<any> {
// Combine the required parameters for the create request
// Structure the payload as expected by Rclone API
const params = {
name,
type,
...parameters,
parameters: parameters, // Nest the parameters object under the 'parameters' key
};
this.logger.log(`Creating new remote: ${name} of type: ${type}`);
return this.callRcloneApi('config/create', params);
this.logger.log(
`Creating new remote: ${name} of type: ${type} with params: ${JSON.stringify(params)}`
); // Added params logging
const result = await this.callRcloneApi('config/create', params);
// console.log('Result was: ', result); // Result is usually empty on success, potentially remove
this.logger.log(`Successfully created remote: ${name}`); // Improved success log
return result; // Rclone 'config/create' usually returns an empty object on success
}
/**
@@ -339,8 +344,8 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
* Generic method to call the RClone RC API
*/
private async callRcloneApi(endpoint: string, params: Record<string, any> = {}): Promise<any> {
const url = `${this.rcloneBaseUrl}/${endpoint}`;
try {
const url = `${this.rcloneBaseUrl}/${endpoint}`;
this.logger.debug(`Calling RClone API: ${url} with params: ${JSON.stringify(params)}`);
const response = await got.post(url, {
@@ -350,13 +355,67 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
headers: {
Authorization: `Basic ${Buffer.from(`${this.rcloneUsername}:${this.rclonePassword}`).toString('base64')}`,
},
// Add timeout? retry logic? Consider these based on need.
});
return response.body;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Error calling RClone API (${endpoint}): ${errorMessage} ${error}`);
throw error;
let detailedErrorMessage = 'An unknown error occurred';
if (error instanceof HTTPError) {
const statusCode = error.response.statusCode;
let rcloneError = 'Could not extract Rclone error details.';
const responseBody = error.response.body; // Get the body
try {
let errorBody: any;
// Check if the body is a string that needs parsing or already an object
if (typeof responseBody === 'string') {
errorBody = JSON.parse(responseBody);
} else if (typeof responseBody === 'object' && responseBody !== null) {
errorBody = responseBody; // It's already an object
}
if (errorBody && errorBody.error) {
rcloneError = `Rclone Error: ${errorBody.error}`;
// Add input details if available, check for different structures
if (errorBody.input) {
rcloneError += ` | Input: ${JSON.stringify(errorBody.input)}`;
} else if (params) {
// Fallback to original params if errorBody.input is missing
rcloneError += ` | Original Params: ${JSON.stringify(params)}`;
}
} else if (responseBody) {
// Body exists but doesn't match expected error structure
rcloneError = `Non-standard error response body: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`;
} else {
rcloneError = 'Empty error response body received.';
}
} catch (parseOrAccessError) {
// Handle errors during parsing or accessing properties
rcloneError = `Failed to process error response body. Raw body: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`;
}
// Construct the detailed message for the new error
detailedErrorMessage = `Rclone API Error (${endpoint}, HTTP ${statusCode}): ${rcloneError}`;
// Log the detailed error including the original stack if available
this.logger.error(
`Original ${detailedErrorMessage} | Params: ${JSON.stringify(params)}`,
error.stack // Log the original HTTPError stack
);
// Throw a NEW error with the detailed Rclone message
throw new Error(detailedErrorMessage);
} else if (error instanceof Error) {
// For non-HTTP errors, log and re-throw as before
detailedErrorMessage = `Error calling RClone API (${endpoint}) with params ${JSON.stringify(params)}: ${error.message}`;
this.logger.error(detailedErrorMessage, error.stack);
throw error; // Re-throw original non-HTTP error
} else {
// Handle unknown error types
detailedErrorMessage = `Unknown error calling RClone API (${endpoint}) with params ${JSON.stringify(params)}: ${String(error)}`;
this.logger.error(detailedErrorMessage);
throw new Error(detailedErrorMessage); // Throw a new error for unknown types
}
}
}

View File

@@ -4,7 +4,11 @@ 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 {
RCloneConfigFormInput,
RCloneProviderOptionResponse,
} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
/**
* Service responsible for generating form UI schemas and form logic
@@ -41,10 +45,12 @@ export class RCloneFormService {
/**
* Returns both data schema and UI schema for the form
*/
async getFormSchemas(selectedProvider: string = ''): Promise<{
dataSchema: Record<string, any>;
async getFormSchemas(options: RCloneConfigFormInput): Promise<{
dataSchema: { properties: DataSlice; type: 'object' };
uiSchema: Layout;
}> {
const { providerType: selectedProvider = '', showAdvanced = false } = options;
// Ensure provider info is loaded
if (Object.keys(this.providerOptions).length === 0) {
await this.loadProviderInfo();
@@ -54,6 +60,7 @@ export class RCloneFormService {
providerTypes: this.providerNames,
selectedProvider,
providerOptions: this.providerOptions,
showAdvanced,
});
}
}

View File

@@ -1,6 +1,7 @@
import { Field, ID, InputType, ObjectType } from '@nestjs/graphql';
import { type Layout } from '@jsonforms/core';
import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator';
import { GraphQLJSON } from 'graphql-scalars';
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
@@ -49,6 +50,24 @@ export interface RCloneProviderOptionResponse {
Examples?: Array<{ Value: string; Help: string; Provider: string }>;
}
@InputType()
export class RCloneConfigFormInput {
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
providerType?: string;
@Field(() => Boolean, { defaultValue: false, nullable: true })
@IsOptional()
@IsBoolean()
showAdvanced?: boolean;
@Field(() => GraphQLJSON, { nullable: true })
@IsOptional()
@IsObject()
parameters?: Record<string, unknown>;
}
@ObjectType()
export class RCloneBackupConfigForm {
@Field(() => ID)
@@ -59,12 +78,6 @@ export class RCloneBackupConfigForm {
@Field(() => GraphQLJSON)
uiSchema!: Layout;
@Field(() => String, { nullable: true })
providerType?: string;
@Field(() => GraphQLJSON, { nullable: true })
parameters?: Record<string, unknown>;
}
@ObjectType()
@@ -94,11 +107,14 @@ export class RCloneRemote {
@InputType()
export class CreateRCloneRemoteInput {
@Field(() => String)
@IsString()
name!: string;
@Field(() => String)
@IsString()
type!: string;
@Field(() => GraphQLJSON)
@IsObject()
parameters!: Record<string, unknown>;
}

View File

@@ -1,8 +1,6 @@
import { Logger } from '@nestjs/common';
import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { GraphQLJSON } from 'graphql-scalars';
import {
AuthActionVerb,
AuthPossession,
@@ -15,9 +13,11 @@ import {
CreateRCloneRemoteInput,
RCloneBackupConfigForm,
RCloneBackupSettings,
RCloneConfigFormInput,
RCloneRemote,
} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js';
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
@Resolver(() => RCloneBackupSettings)
export class RCloneBackupSettingsResolver {
@@ -42,20 +42,15 @@ export class RCloneBackupSettingsResolver {
@ResolveField(() => RCloneBackupConfigForm)
async configForm(
@Parent() _parent: RCloneBackupSettings,
@Args('providerType', { nullable: true }) providerType?: string,
@Args('parameters', { type: () => GraphQLJSON, nullable: true })
parameters?: Record<string, unknown>
@Args('formOptions', { type: () => RCloneConfigFormInput, nullable: true })
formOptions?: RCloneConfigFormInput
): Promise<RCloneBackupConfigForm> {
// Return form info with the provided arguments
const form = await this.rcloneFormService.getFormSchemas(providerType);
const form = await this.rcloneFormService.getFormSchemas(formOptions ?? {});
return {
id: 'rcloneBackupConfigForm',
dataSchema: form.dataSchema,
dataSchema: form.dataSchema as { properties: DataSlice; type: 'object' },
uiSchema: form.uiSchema,
providerType,
parameters,
} as RCloneBackupConfigForm;
};
}
@Mutation(() => RCloneRemote)

View File

@@ -71,13 +71,19 @@ const vueRules = {
const commonLanguageOptions = {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
browser: true,
window: true,
document: true,
es2022: true,
HTMLElement: true,
},
};
// Define globals separately
const commonGlobals = {
browser: true,
window: true,
document: true,
console: true,
Event: true,
HTMLElement: true,
HTMLInputElement: true,
CustomEvent: true,
es2022: true,
};
export default [
@@ -96,6 +102,9 @@ export default [
jsx: true,
},
},
globals: {
...commonGlobals
},
},
plugins: {
'no-relative-import-paths': noRelativeImportPaths,
@@ -119,6 +128,9 @@ export default [
jsx: true,
},
},
globals: {
...commonGlobals
},
},
plugins: {
'no-relative-import-paths': noRelativeImportPaths,

View File

@@ -17,4 +17,4 @@ export * from '@/components/common/tabs';
export * from '@/components/common/tooltip';
export * from '@/components/common/toast';
export * from '@/components/common/popover';
export * from '@/components/modals';
export * from '@/components/modals';

View File

@@ -1,4 +1,4 @@
import DropdownMenu from './DropdownMenu.vue';
import DropdownMenu from '@/components/common/dropdown-menu/DropdownMenu.vue';
export { DropdownMenu };
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue';

View File

@@ -6,7 +6,7 @@ const props = defineProps<{ class?: HTMLAttributes['class'] }>();
</script>
<template>
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-2', props.class)">
<slot />
</div>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
</script>
<script setup lang="ts"></script>
<template>
<div id="modals"></div>
</template>
<div id="modals" />
</template>

View File

@@ -1 +1 @@
export { default as Modals } from './ModalTarget.vue';
export { default as Modals } from './ModalTarget.vue';

View File

@@ -79,61 +79,47 @@ if (control.value.data !== undefined && control.value.data !== null) {
</script>
<template>
<div v-if="control.visible" class="flex flex-col gap-2">
<label v-if="control.label" :for="control.id" class="text-sm font-medium">
{{ control.label }}
</label>
<Combobox :open="isOpen" @update:open="handleOpenChange">
<ComboboxAnchor>
<ComboboxTrigger>
<ComboboxInput
:id="control.id"
:value="inputValue"
@input="handleInput"
:placeholder="control.uischema.options?.placeholder"
:disabled="!control.enabled"
/>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxList>
<ComboboxEmpty class="p-2 text-sm text-muted-foreground"> No suggestions found </ComboboxEmpty>
<Combobox :open="isOpen" @update:open="handleOpenChange">
<ComboboxAnchor>
<ComboboxTrigger>
<ComboboxInput
:id="control.id"
:value="inputValue"
@input="handleInput"
:placeholder="control.uischema.options?.placeholder"
:disabled="!control.enabled"
/>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxList>
<ComboboxEmpty class="p-2 text-sm text-muted-foreground"> No suggestions found </ComboboxEmpty>
<template v-for="suggestion in suggestions" :key="suggestion.value">
<TooltipProvider v-if="suggestion.tooltip">
<Tooltip :delay-duration="50">
<TooltipTrigger as-child>
<ComboboxItem
:value="suggestion.value"
@select="handleSelect"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
>
<span>{{ suggestion.label || suggestion.value }}</span>
</ComboboxItem>
</TooltipTrigger>
<TooltipContent :to="teleportTarget" side="right" :side-offset="5">
<p class="max-w-xs">{{ suggestion.tooltip }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<ComboboxItem
v-else
:value="suggestion.value"
@select="handleSelect"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
>
<span>{{ suggestion.label || suggestion.value }}</span>
</ComboboxItem>
</template>
</ComboboxList>
</Combobox>
<div v-if="control.errors" class="text-sm text-destructive">
{{ control.errors }}
</div>
<div v-if="control.description" class="text-sm text-muted-foreground">
{{ control.description }}
</div>
</div>
<template v-for="suggestion in suggestions" :key="suggestion.value">
<TooltipProvider v-if="suggestion.tooltip">
<Tooltip :delay-duration="50">
<TooltipTrigger as-child>
<ComboboxItem
:value="suggestion.value"
@select="handleSelect"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
>
<span>{{ suggestion.label || suggestion.value }}</span>
</ComboboxItem>
</TooltipTrigger>
<TooltipContent :to="teleportTarget" side="right" :side-offset="5">
<p class="max-w-xs">{{ suggestion.tooltip }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<ComboboxItem
v-else
:value="suggestion.value"
@select="handleSelect"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
>
<span>{{ suggestion.label || suggestion.value }}</span>
</ComboboxItem>
</template>
</ComboboxList>
</Combobox>
</template>

View File

@@ -1,23 +1,22 @@
<script setup lang="ts">
import FormErrors from '@/forms/FormErrors.vue';
import type { ControlElement } from '@jsonforms/core';
import { useJsonFormsControl } from '@jsonforms/vue';
import type { RendererProps } from '@jsonforms/vue';
import FormErrors from '@/forms/FormErrors.vue';
// Define props consistent with JsonForms renderers
const props = defineProps<RendererProps<ControlElement>>();
// Use the standard composable to get control state
const { control } = useJsonFormsControl(props);
</script>
<template>
<!-- Only render the wrapper if the control is visible -->
<div v-if="control.visible">
<div v-if="control.visible" class="flex-grow">
<!-- Render the actual control passed via the default slot -->
<slot />
<!-- Automatically display errors below the control -->
<FormErrors :errors="control.errors" />
</div>
</template>
</template>

View File

@@ -15,4 +15,4 @@ const normalizedErrors = computed(() => {
<div v-if="normalizedErrors.length > 0" class="mt-2 text-red-500 text-sm">
<p v-for="error in normalizedErrors" :key="error">{{ error }}</p>
</div>
</template>
</template>

View File

@@ -14,10 +14,10 @@
* @prop cells - Available cells
*/
import { useJsonFormsVisibility } from '@/forms/composables/useJsonFormsVisibility';
import type { HorizontalLayout } from '@jsonforms/core';
import { DispatchRenderer, type RendererProps } from '@jsonforms/vue';
import { computed } from 'vue';
import { useJsonFormsVisibility } from './composables/useJsonFormsVisibility';
const props = defineProps<RendererProps<HorizontalLayout>>();
@@ -28,14 +28,12 @@ const elements = computed(() => {
// Access elements from the layout object returned by the composable
return layout.layout.value.uischema.elements || [];
});
</script>
<template>
<div v-if="isVisible" class="flex flex-row gap-x-2">
<template v-for="(element, index) in elements" :key="index">
<div v-if="isVisible" class="flex flex-row gap-2 items-baseline">
<template v-for="(element, _i) in elements" :key="_i">
<DispatchRenderer
class="ml-10"
:schema="layout.layout.value.schema"
:uischema="element"
:path="layout.layout.value.path"

View File

@@ -28,12 +28,12 @@ const classOverride = computed(() => {
</script>
<template>
<Input
v-model="value"
:type="inputType"
:class="classOverride"
:disabled="!control.enabled"
:required="control.required"
:placeholder="control.schema.description"
/>
<Input
v-model="value"
:type="inputType"
:class="cn('flex-grow', classOverride)"
:disabled="!control.enabled"
:required="control.required"
:placeholder="control.schema.description"
/>
</template>

View File

@@ -52,26 +52,25 @@ const labelClass = computed(() => {
case 'title':
return 'text-xl font-semibold mb-2'; // Example styling for title
case 'heading':
return 'text-lg font-medium mt-4 mb-1'; // Example styling for heading
return 'text-lg font-semibold mt-4 mb-1'; // Example styling for heading
default:
return 'font-medium'; // Default label styling
return 'font-semibold'; // Default label styling
}
});
const descriptionClass = computed(() => {
switch (labelFormat.value) {
case 'documentation':
return 'text-sm text-gray-500 italic p-2 border-l-4 border-gray-300 bg-gray-50 my-2'; // Example styling for documentation
return 'text-sm text-gray-500 italic p-2 border-l-4 border-gray-300 bg-gray-50 my-2 font-bold'; // Example styling for documentation
default:
return 'text-sm text-gray-600 mt-1'; // Default description styling
}
});
</script>
<template>
<!-- Use the computed isVisible based on renderer.value.visible -->
<div class="my-2">
<div class="flex flex-col gap-2 max-w-lg flex-shrink-0">
<!-- Replace native label with the Label component -->
<Label v-if="labelText" :class="labelClass">{{ labelText }}</Label>
<!-- Use v-html with the parsedDescription ref -->

View File

@@ -33,20 +33,20 @@ const classOverride = computed(() => {
</script>
<template>
<NumberField
v-model="value"
:min="min"
:max="max"
:step="step"
:format-options="formatOptions"
:class="classOverride"
:disabled="!control.enabled"
:required="control.required"
blah="true"
:blah-2="true"
>
<NumberFieldDecrement v-if="stepperEnabled" />
<NumberFieldInput />
<NumberField
v-model="value"
:min="min"
:max="max"
:step="step"
:format-options="formatOptions"
:class="classOverride"
:disabled="!control.enabled"
:required="control.required"
blah="true"
:blah-2="true"
>
<NumberFieldDecrement v-if="stepperEnabled" />
<NumberFieldInput />
<NumberFieldIncrement v-if="stepperEnabled" />
</NumberField>
</template>

View File

@@ -27,13 +27,13 @@ const description = computed(() => props.uischema.options?.description);
</script>
<template>
<!-- Render each precondition as a list item with an icon bullet -->
<p v-if="description" class="mb-2">{{ description }}</p>
<ul class="list-none space-y-1">
<li v-for="(item, index) in items" :key="index" class="flex items-center">
<span v-if="item.status" class="text-green-500 mr-2 font-bold"></span>
<span v-else class="text-red-500 mr-2 font-extrabold"></span>
<span>{{ item.text }}</span>
</li>
</ul>
<!-- Render each precondition as a list item with an icon bullet -->
<p v-if="description" class="mb-2">{{ description }}</p>
<ul class="list-none space-y-1">
<li v-for="(item, index) in items" :key="index" class="flex items-center">
<span v-if="item.status" class="text-green-500 mr-2 font-bold"></span>
<span v-else class="text-red-500 mr-2 font-extrabold"></span>
<span>{{ item.text }}</span>
</li>
</ul>
</template>

View File

@@ -8,13 +8,15 @@ import StepperTitle from '@/components/common/stepper/StepperTitle.vue';
import StepperTrigger from '@/components/common/stepper/StepperTrigger.vue';
import { CheckIcon } from '@heroicons/vue/24/solid'; // Example icon
import {
Actions,
type CoreActions,
type JsonFormsSubStates,
type JsonSchema,
type Layout,
type UISchemaElement,
Actions, type CoreActions, type JsonFormsSubStates
} from '@jsonforms/core';
import { DispatchRenderer, useJsonFormsLayout, type RendererProps } from '@jsonforms/vue';
import { computed, inject } from 'vue';
import { computed, inject, ref, type Ref } from 'vue';
// Define props based on RendererProps<Layout>
const props = defineProps<RendererProps<Layout>>();
@@ -23,6 +25,14 @@ const props = defineProps<RendererProps<Layout>>();
const jsonforms = inject<JsonFormsSubStates>('jsonforms');
const dispatch = inject<(action: CoreActions) => void>('dispatch'); // Inject dispatch separately
// --- START: Inject submission logic from parent ---
const submitForm = inject<() => Promise<void>>('submitForm', () => {
console.warn('SteppedLayout: submitForm function not provided');
return Promise.resolve(); // Provide a default no-op function
});
const isSubmitting = inject<Ref<boolean>>('isSubmitting', ref(false)); // Provide a default non-reactive ref
// --- END: Inject submission logic from parent ---
if (!jsonforms || !dispatch) {
throw new Error("'jsonforms' or 'dispatch' context wasn't provided. Are you within JsonForms?");
}
@@ -37,10 +47,17 @@ const numSteps = computed(() => stepsConfig.value.length);
// --- 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 stepData = core!.data?.configStep;
// Handle both the new object format and the old number format
if (typeof stepData === 'object' && stepData !== null && typeof stepData.current === 'number') {
// Ensure step is within bounds
return Math.max(0, Math.min(stepData.current, numSteps.value - 1));
}
// Fallback for initial state or old number format
const numericStep = typeof stepData === 'number' ? stepData : 0;
return Math.max(0, Math.min(numericStep, numSteps.value - 1));
});
const isLastStep = computed(() => currentStep.value === numSteps.value - 1);
const isLastStep = computed(() => numSteps.value > 0 && currentStep.value === numSteps.value - 1);
// --- Step Update Logic ---
const updateStep = (newStep: number) => {
@@ -48,18 +65,22 @@ const updateStep = (newStep: number) => {
if (newStep < 0 || newStep >= numSteps.value) {
return;
}
// Update the 'configStep' property in the JSON Forms data
dispatch(Actions.update('configStep', () => newStep));
// Make total zero-indexed
const total = numSteps.value > 0 ? numSteps.value - 1 : 0;
// Update the 'configStep' property in the JSON Forms data with the new object structure
dispatch(Actions.update('configStep', () => ({ current: newStep, total })));
};
// --- Filtered Elements for Current Step ---
const currentStepElements = computed(() => {
const filtered = (props.uischema.elements || []).filter((element: UISchemaElement) => {
return element.options?.step === currentStep.value;
// Check if the element has an 'options' object and an 'step' property
return (
typeof element.options === 'object' &&
element.options !== null &&
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;
});
@@ -74,9 +95,13 @@ const getStepState = (stepIndex: number): StepState => {
</script>
<template>
<div v-if="layout.visible" :key="currentStep" class="stepped-layout space-y-6">
<div v-if="layout.visible" class="stepped-layout space-y-6">
<!-- Stepper Indicators -->
<Stepper :modelValue="currentStep + 1" class="text-foreground flex w-full items-start gap-2 text-sm">
<Stepper
v-if="numSteps > 0"
:modelValue="currentStep + 1"
class="text-foreground flex w-full items-start gap-2 text-sm"
>
<StepperItem
v-for="(step, index) in stepsConfig"
:key="index"
@@ -118,12 +143,9 @@ const getStepState = (stepIndex: number): StepState => {
</StepperItem>
</Stepper>
<!-- Add logging here -->
{{ console.log(`[SteppedLayout Template] Rendering step: ${currentStep}`) }}
{{ console.log(`[SteppedLayout Template] Elements for step ${currentStep}:`, JSON.stringify(currentStepElements.map(el => ({ type: el.type, scope: (el as any).scope })))) }}
<!-- Render elements for the current step -->
<div class="current-step-content rounded-md border p-4 shadow">
<!-- Added key to force re-render on step change, ensuring correct elements display -->
<div class="current-step-content rounded-md border p-4 shadow" :key="`step-content-${currentStep}`">
<DispatchRenderer
v-for="(element, index) in currentStepElements"
:key="`${layout.path}-${index}-step-${currentStep}`"
@@ -132,18 +154,27 @@ const getStepState = (stepIndex: number): StepState => {
:path="layout.path || ''"
:renderers="layout.renderers"
:cells="layout.cells"
:enabled="layout.enabled"
:enabled="layout.enabled && !isSubmitting"
/>
</div>
<!-- Navigation Buttons -->
<div class="mt-4 flex justify-end space-x-2">
<Button variant="outline" @click="updateStep(currentStep - 1)" :disabled="currentStep === 0">
<Button
variant="outline"
@click="updateStep(currentStep - 1)"
:disabled="currentStep === 0 || isSubmitting"
>
Previous
</Button>
<Button v-if="!isLastStep" @click="updateStep(currentStep + 1)">
<!-- Show Next button only if not the last step -->
<Button v-if="!isLastStep" @click="updateStep(currentStep + 1)" :disabled="isSubmitting">
Next
</Button>
<!-- Show Submit button only on the last step -->
<Button v-if="isLastStep" @click="submitForm" :loading="isSubmitting" :disabled="isSubmitting">
Submit Configuration
</Button>
</div>
</div>
</template>

View File

@@ -40,34 +40,34 @@ const placeholder = computed(() => control.value.uischema?.options?.placeholder
</script>
<template>
<div class="space-y-4">
<p v-if="control.description" v-html="control.description" />
<div v-for="(item, index) in items" :key="index" class="flex gap-2">
<Input
:type="inputType"
:model-value="item"
:placeholder="placeholder"
:disabled="!control.enabled"
class="flex-1"
@update:model-value="(value) => updateItem(index, String(value))"
/>
<Button
variant="ghost"
class="rounded underline underline-offset-4"
:disabled="!control.enabled"
@click="() => removeItem(index)"
>
Remove
</Button>
</div>
<Button
variant="outline"
size="md"
class="text-sm rounded-sm"
<div class="space-y-4">
<p v-if="control.description" v-html="control.description" />
<div v-for="(item, index) in items" :key="index" class="flex gap-2">
<Input
:type="inputType"
:model-value="item"
:placeholder="placeholder"
:disabled="!control.enabled"
@click="addItem"
class="flex-1"
@update:model-value="(value) => updateItem(index, String(value))"
/>
<Button
variant="ghost"
class="rounded underline underline-offset-4"
:disabled="!control.enabled"
@click="() => removeItem(index)"
>
Add Item
Remove
</Button>
</div>
<Button
variant="outline"
size="md"
class="text-sm rounded-sm"
:disabled="!control.enabled"
@click="addItem"
>
Add Item
</Button>
</div>
</template>

View File

@@ -3,18 +3,15 @@ import { Switch as UuiSwitch } from '@/components/form/switch';
import type { ControlElement } from '@jsonforms/core';
import { useJsonFormsControl } from '@jsonforms/vue';
import type { RendererProps } from '@jsonforms/vue';
import { computed } from 'vue';
const props = defineProps<RendererProps<ControlElement>>();
const { control, handleChange } = useJsonFormsControl(props);
const onChange = (checked: boolean) => {
handleChange(control.value.path, checked);
};
const description = computed(() => props.uischema.options?.description);
</script>
<template>
<p v-if="description" v-html="description" class="mb-2" />
<UuiSwitch
:id="control.id + '-input'"
:name="control.path"

View File

@@ -15,10 +15,10 @@
* @prop cells - Available cells
*/
import { useJsonFormsVisibility } from '@/forms/composables/useJsonFormsVisibility';
import type { HorizontalLayout } from '@jsonforms/core';
import { DispatchRenderer, type RendererProps } from '@jsonforms/vue';
import { computed } from 'vue';
import { useJsonFormsVisibility } from './composables/useJsonFormsVisibility';
const props = defineProps<RendererProps<HorizontalLayout>>();
@@ -29,12 +29,11 @@ const elements = computed(() => {
// Access elements from the layout object returned by the composable
return layout.layout.value.uischema.elements || [];
});
</script>
<template>
<div v-if="isVisible" class="grid grid-cols-settings items-baseline">
<template v-for="(element, index) in elements" :key="index">
<template v-for="(element, _i) in elements" :key="_i">
<DispatchRenderer
:schema="layout.layout.value.schema"
:uischema="element"

View File

@@ -14,25 +14,34 @@
* @prop cells - Available cells
*/
import { useJsonFormsVisibility } from '@/forms/composables/useJsonFormsVisibility';
import type { VerticalLayout } from '@jsonforms/core';
import { DispatchRenderer, type RendererProps } from '@jsonforms/vue';
import { computed } from 'vue';
import { useJsonFormsVisibility } from './composables/useJsonFormsVisibility';
const props = defineProps<RendererProps<VerticalLayout>>();
const { layout, isVisible } = useJsonFormsVisibility({ rendererProps: props });
const showDividers = computed(() => {
return !!layout.layout.value.uischema.options?.showDividers;
});
const elements = computed(() => {
return layout.layout.value.uischema.elements || [];
});
</script>
<template>
<div v-if="isVisible" class="flex flex-col gap-y-2">
<template v-for="(element, index) in elements" :key="index">
<div
v-if="isVisible"
class="flex flex-col items-stretch gap-4"
:class="{
'divide-y divide-gray-200 dark:divide-gray-700': showDividers,
}"
>
<template v-for="(element, _i) in elements" :key="_i">
<DispatchRenderer
class="ml-10"
:schema="layout.layout.value.schema"
:uischema="element"
:path="layout.layout.value.path"

View File

@@ -11,7 +11,9 @@ interface UseJsonFormsVisibilityReturn {
isVisible: ComputedRef<boolean>;
}
export function useJsonFormsVisibility<T extends Layout>(props: UseJsonFormsVisibilityProps<T>): UseJsonFormsVisibilityReturn {
export function useJsonFormsVisibility<T extends Layout>(
props: UseJsonFormsVisibilityProps<T>
): UseJsonFormsVisibilityReturn {
const layout = useJsonFormsLayout(props.rendererProps);
const isVisible = computed(() => {
@@ -24,4 +26,4 @@ export function useJsonFormsVisibility<T extends Layout>(props: UseJsonFormsVisi
layout,
isVisible,
};
}
}

View File

@@ -10,6 +10,7 @@ import selectRenderer from '@/forms/Select.vue';
import SteppedLayout from '@/forms/SteppedLayout.vue';
import StringArrayField from '@/forms/StringArrayField.vue';
import switchRenderer from '@/forms/Switch.vue';
import UnraidSettingsLayout from '@/forms/UnraidSettingsLayout.vue';
import VerticalLayout from '@/forms/VerticalLayout.vue';
import {
and,
@@ -28,11 +29,11 @@ import {
} from '@jsonforms/core';
import type { ControlElement, JsonFormsRendererRegistryEntry, JsonSchema } from '@jsonforms/core';
import type { RendererProps } from '@jsonforms/vue';
import { h, markRaw } from 'vue';
import { h, markRaw, type Component } from 'vue';
// Helper function to wrap control renderers with error display
// Returns a functional component
const withErrorWrapper = (RendererComponent: any) => {
const withErrorWrapper = (RendererComponent: Component) => {
return (props: RendererProps<ControlElement>) => {
return h(ControlWrapper, props, {
default: () => h(RendererComponent, props),
@@ -60,11 +61,19 @@ export const jsonFormsRenderers: JsonFormsRendererRegistryEntry[] = [
renderer: markRaw(SteppedLayout),
tester: rankWith(3, and(isLayout, uiTypeIs('SteppedLayout'))),
},
{
renderer: markRaw(UnraidSettingsLayout),
tester: rankWith(3, and(isLayout, uiTypeIs('UnraidSettingsLayout'))),
},
// Controls
{
renderer: markRaw(withErrorWrapper(switchRenderer)),
tester: rankWith(4, and(isBooleanControl, optionIs('toggle', true))),
},
{
renderer: markRaw(withErrorWrapper(switchRenderer)),
tester: rankWith(4, and(isBooleanControl, optionIs('format', 'toggle'))),
},
{
renderer: markRaw(withErrorWrapper(selectRenderer)),
tester: rankWith(4, and(isEnumControl)),

View File

@@ -14,4 +14,3 @@ export { default as tailwindConfig } from '../tailwind.config';
// Composables
export { default as useTeleport } from '@/composables/useTeleport';

View File

@@ -25,7 +25,7 @@ const defaultMarkedExtension: MarkedExtension = {
* - Use `Markdown.create` to extend or customize parsing functionality.
* - Use `Markdown.parse` to conveniently parse markdown to safe html.
*/
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Markdown {
private static instance = Markdown.create();

View File

@@ -13,14 +13,14 @@ type RegisterParams = {
// Type for our simplified Vue component representation
type CustomElementComponent = {
styles?: string[];
render?: Function;
setup?: Function;
[key: string]: any;
render?: () => unknown;
setup?: () => unknown;
[key: string]: unknown;
};
export function registerAllComponents(params: RegisterParams = {}) {
const { namePrefix = 'uui', pathToSharedCss = './src/styles/index.css' } = params;
Object.entries(Components).forEach(([name, originalComponent]) => {
// Use explicit type assertion instead of type predicates
try {
@@ -31,7 +31,7 @@ export function registerAllComponents(params: RegisterParams = {}) {
}
return;
}
// Skip function values
if (typeof originalComponent === 'function') {
if (debugImports) {
@@ -39,7 +39,7 @@ export function registerAllComponents(params: RegisterParams = {}) {
}
return;
}
// Skip if not a Vue component
if (!('render' in originalComponent || 'setup' in originalComponent)) {
if (debugImports) {
@@ -50,7 +50,7 @@ export function registerAllComponents(params: RegisterParams = {}) {
// Now we can safely use type assertion since we've validated the component
const component = originalComponent as CustomElementComponent;
// add our shared css to each web component
component.styles ??= [];
component.styles.unshift(`@import "${pathToSharedCss}"`);
@@ -67,9 +67,9 @@ export function registerAllComponents(params: RegisterParams = {}) {
if (debugImports) {
console.log(name, elementName, component.styles);
}
// Use appropriate casting for defineCustomElement
customElements.define(elementName, defineCustomElement(component as any));
customElements.define(elementName, defineCustomElement(component as object));
} catch (error) {
console.error(`[register components] Error registering component ${name}:`, error);
}

View File

@@ -3,4 +3,4 @@
declare module '*.css?raw' {
const content: string;
export default content;
}
}

View File

@@ -1,16 +1,11 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { computed, provide, ref, watch } from 'vue';
import { useMutation, useQuery } from '@vue/apollo-composable';
import { Button, jsonFormsRenderers } from '@unraid/ui';
import { JsonForms } from '@jsonforms/vue';
import type { CreateRCloneRemoteInput } from '~/composables/gql/graphql';
import {
CREATE_REMOTE,
GET_RCLONE_CONFIG_FORM,
} from '~/components/RClone/graphql/settings.query';
import { CREATE_REMOTE, GET_RCLONE_CONFIG_FORM } from '~/components/RClone/graphql/settings.query';
import { useUnraidApiStore } from '~/store/unraidApi';
const { offlineError: _offlineError, unraidApiStatus: _unraidApiStatus } = useUnraidApiStore();
@@ -31,31 +26,46 @@ const {
loading: formLoading,
refetch: updateFormSchema,
} = useQuery(GET_RCLONE_CONFIG_FORM, {
providerType: formState.value.type,
parameters: formState.value.parameters,
formOptions: {
providerType: formState.value.type,
parameters: formState.value.parameters,
showAdvanced: formState.value.showAdvanced,
},
});
// Watch for provider type changes to update schema
watch(providerType, async (newType) => {
if (newType) {
await updateFormSchema({
providerType: newType,
parameters: formState.value.parameters,
formOptions: {
providerType: newType,
parameters: formState.value.parameters,
showAdvanced: formState.value.showAdvanced,
},
});
}
});
// Watch for step changes to update schema if needed
watch(
() => formState.value.configStep,
async (newStep) => {
if (newStep > 0) {
formState,
async (previousValue, newValue) => {
// Always refetch when step changes to ensure schema matches
if (
previousValue.configStep !== newValue.configStep ||
previousValue.showAdvanced !== newValue.showAdvanced
) {
console.log('[RCloneConfig] Refetching form schema');
await updateFormSchema({
providerType: formState.value.type,
parameters: formState.value.parameters,
formOptions: {
providerType: formState.value.type,
parameters: formState.value.parameters,
showAdvanced: formState.value.showAdvanced,
},
});
}
}
},
{ deep: true }
);
/**
@@ -76,7 +86,7 @@ const submitForm = async () => {
name: formState.value.name,
type: formState.value.type,
parameters: formState.value.parameters,
} as CreateRCloneRemoteInput,
},
});
} catch (error) {
console.error('Error creating remote:', error);
@@ -134,6 +144,10 @@ const isLastStep = computed(() => {
if (numSteps.value === 0) return false;
return formState.value.configStep === numSteps.value - 1;
});
// --- Provide submission logic to SteppedLayout ---
provide('submitForm', submitForm);
provide('isSubmitting', isCreating); // Provide the loading state ref
</script>
<template>
@@ -162,12 +176,12 @@ const isLastStep = computed(() => {
</div>
<!-- Submit Button (visible only on the last step) -->
<div v-if="!formLoading && uiSchema && isLastStep" class="mt-6 flex justify-end border-t border-gray-200 pt-6">
<Button :loading="isCreating" @click="submitForm">
Submit Configuration
</Button>
<div
v-if="!formLoading && uiSchema && isLastStep"
class="mt-6 flex justify-end border-t border-gray-200 pt-6"
>
<Button :loading="isCreating" @click="submitForm"> Submit Configuration </Button>
</div>
</div>
</div>
</template>

View File

@@ -2,9 +2,10 @@ import { graphql } from "~/composables/gql/gql";
export const GET_RCLONE_CONFIG_FORM = graphql(/* GraphQL */ `
query GetRCloneConfigForm($providerType: String, $parameters: JSON) {
query GetRcloneConfigForm($formOptions: RCloneConfigFormInput) {
rcloneBackup {
configForm(providerType: $providerType, parameters: $parameters) {
configForm(formOptions: $formOptions) {
id
dataSchema
uiSchema
}
@@ -25,7 +26,7 @@ export const CREATE_REMOTE = graphql(/* GraphQL */ `
createRCloneRemote(input: $input) {
name
type
config
parameters
}
}
`);

View File

@@ -36,9 +36,9 @@ type Documents = {
"\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": typeof types.RecomputeOverviewDocument,
"\n subscription NotificationAddedSub {\n notificationAdded {\n ...NotificationFragment\n }\n }\n": typeof types.NotificationAddedSubDocument,
"\n subscription NotificationOverviewSub {\n notificationsOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": typeof types.NotificationOverviewSubDocument,
"\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n": typeof types.GetRCloneConfigFormDocument,
"\n query GetRcloneConfigForm($formOptions: RCloneConfigFormInput) {\n rcloneBackup {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n": typeof types.GetRcloneConfigFormDocument,
"\n query ListRCloneRemotes {\n rcloneBackup {\n remotes\n }\n }\n": typeof types.ListRCloneRemotesDocument,
"\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n config\n }\n }\n": typeof types.CreateRCloneRemoteDocument,
"\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n parameters\n }\n }\n": typeof types.CreateRCloneRemoteDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": typeof types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": typeof types.SignOutDocument,
"\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": typeof types.PartialCloudFragmentDoc,
@@ -72,9 +72,9 @@ const documents: Documents = {
"\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": types.RecomputeOverviewDocument,
"\n subscription NotificationAddedSub {\n notificationAdded {\n ...NotificationFragment\n }\n }\n": types.NotificationAddedSubDocument,
"\n subscription NotificationOverviewSub {\n notificationsOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": types.NotificationOverviewSubDocument,
"\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n": types.GetRCloneConfigFormDocument,
"\n query GetRcloneConfigForm($formOptions: RCloneConfigFormInput) {\n rcloneBackup {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n": types.GetRcloneConfigFormDocument,
"\n query ListRCloneRemotes {\n rcloneBackup {\n remotes\n }\n }\n": types.ListRCloneRemotesDocument,
"\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n config\n }\n }\n": types.CreateRCloneRemoteDocument,
"\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n parameters\n }\n }\n": types.CreateRCloneRemoteDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": types.SignOutDocument,
"\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": types.PartialCloudFragmentDoc,
@@ -191,7 +191,7 @@ export function graphql(source: "\n subscription NotificationOverviewSub {\n
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n"): (typeof documents)["\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n"];
export function graphql(source: "\n query GetRcloneConfigForm($formOptions: RCloneConfigFormInput) {\n rcloneBackup {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n"): (typeof documents)["\n query GetRcloneConfigForm($formOptions: RCloneConfigFormInput) {\n rcloneBackup {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -199,7 +199,7 @@ export function graphql(source: "\n query ListRCloneRemotes {\n rcloneBackup
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n config\n }\n }\n"): (typeof documents)["\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n config\n }\n }\n"];
export function graphql(source: "\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n parameters\n }\n }\n"): (typeof documents)["\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n parameters\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -1276,18 +1276,6 @@ export type RCloneBackupConfigForm = {
uiSchema: Scalars['JSON']['output'];
};
export type RCloneBackupConfigFormDataSchemaArgs = {
parameters?: InputMaybe<Scalars['JSON']['input']>;
providerType?: InputMaybe<Scalars['String']['input']>;
};
export type RCloneBackupConfigFormUiSchemaArgs = {
parameters?: InputMaybe<Scalars['JSON']['input']>;
providerType?: InputMaybe<Scalars['String']['input']>;
};
export type RCloneBackupSettings = {
__typename?: 'RCloneBackupSettings';
configForm: RCloneBackupConfigForm;
@@ -1297,8 +1285,13 @@ export type RCloneBackupSettings = {
export type RCloneBackupSettingsConfigFormArgs = {
formOptions?: InputMaybe<RCloneConfigFormInput>;
};
export type RCloneConfigFormInput = {
parameters?: InputMaybe<Scalars['JSON']['input']>;
providerType?: InputMaybe<Scalars['String']['input']>;
showAdvanced?: InputMaybe<Scalars['Boolean']['input']>;
};
export type RCloneDrive = {
@@ -1309,36 +1302,10 @@ export type RCloneDrive = {
options: Scalars['JSON']['output'];
};
export type RCloneProviderOption = {
__typename?: 'RCloneProviderOption';
advanced?: Maybe<Scalars['Boolean']['output']>;
default?: Maybe<Scalars['JSON']['output']>;
defaultStr?: Maybe<Scalars['String']['output']>;
examples?: Maybe<Array<RCloneProviderOptionExample>>;
help: Scalars['String']['output'];
hide?: Maybe<Scalars['Boolean']['output']>;
isPassword?: Maybe<Scalars['Boolean']['output']>;
name: Scalars['String']['output'];
noPrefix?: Maybe<Scalars['Boolean']['output']>;
provider: Scalars['String']['output'];
required?: Maybe<Scalars['Boolean']['output']>;
shortOpt?: Maybe<Scalars['String']['output']>;
type?: Maybe<Scalars['String']['output']>;
value?: Maybe<Scalars['JSON']['output']>;
valueStr?: Maybe<Scalars['String']['output']>;
};
export type RCloneProviderOptionExample = {
__typename?: 'RCloneProviderOptionExample';
help: Scalars['String']['output'];
provider: Scalars['String']['output'];
value: Scalars['String']['output'];
};
export type RCloneRemote = {
__typename?: 'RCloneRemote';
config: Scalars['JSON']['output'];
name: Scalars['String']['output'];
parameters: Scalars['JSON']['output'];
type: Scalars['String']['output'];
};
@@ -2076,13 +2043,12 @@ export type NotificationOverviewSubSubscription = { __typename?: 'Subscription',
& { ' $fragmentRefs'?: { 'NotificationCountFragmentFragment': NotificationCountFragmentFragment } }
) } };
export type GetRCloneConfigFormQueryVariables = Exact<{
providerType?: InputMaybe<Scalars['String']['input']>;
parameters?: InputMaybe<Scalars['JSON']['input']>;
export type GetRcloneConfigFormQueryVariables = Exact<{
formOptions?: InputMaybe<RCloneConfigFormInput>;
}>;
export type GetRCloneConfigFormQuery = { __typename?: 'Query', rcloneBackup: { __typename?: 'RCloneBackupSettings', configForm: { __typename?: 'RCloneBackupConfigForm', dataSchema: any, uiSchema: any } } };
export type GetRcloneConfigFormQuery = { __typename?: 'Query', rcloneBackup: { __typename?: 'RCloneBackupSettings', configForm: { __typename?: 'RCloneBackupConfigForm', id: string, dataSchema: any, uiSchema: any } } };
export type ListRCloneRemotesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2094,7 +2060,7 @@ export type CreateRCloneRemoteMutationVariables = Exact<{
}>;
export type CreateRCloneRemoteMutation = { __typename?: 'Mutation', createRCloneRemote: { __typename?: 'RCloneRemote', name: string, type: string, config: any } };
export type CreateRCloneRemoteMutation = { __typename?: 'Mutation', createRCloneRemote: { __typename?: 'RCloneRemote', name: string, type: string, parameters: any } };
export type ConnectSignInMutationVariables = Exact<{
input: ConnectSignInInput;
@@ -2170,9 +2136,9 @@ export const OverviewDocument = {"kind":"Document","definitions":[{"kind":"Opera
export const RecomputeOverviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RecomputeOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recalculateOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationCountFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationCounts"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}}]}}]} as unknown as DocumentNode<RecomputeOverviewMutation, RecomputeOverviewMutationVariables>;
export const NotificationAddedSubDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"NotificationAddedSub"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notificationAdded"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode<NotificationAddedSubSubscription, NotificationAddedSubSubscriptionVariables>;
export const NotificationOverviewSubDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"NotificationOverviewSub"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notificationsOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationCountFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationCounts"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}}]}}]} as unknown as DocumentNode<NotificationOverviewSubSubscription, NotificationOverviewSubSubscriptionVariables>;
export const GetRCloneConfigFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRCloneConfigForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"providerType"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"parameters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rcloneBackup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerType"}}},{"kind":"Argument","name":{"kind":"Name","value":"parameters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"parameters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}}]}}]}}]} as unknown as DocumentNode<GetRCloneConfigFormQuery, GetRCloneConfigFormQueryVariables>;
export const GetRcloneConfigFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRcloneConfigForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"formOptions"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"RCloneConfigFormInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rcloneBackup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"formOptions"},"value":{"kind":"Variable","name":{"kind":"Name","value":"formOptions"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}}]}}]}}]} as unknown as DocumentNode<GetRcloneConfigFormQuery, GetRcloneConfigFormQueryVariables>;
export const ListRCloneRemotesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListRCloneRemotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rcloneBackup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remotes"}}]}}]}}]} as unknown as DocumentNode<ListRCloneRemotesQuery, ListRCloneRemotesQueryVariables>;
export const CreateRCloneRemoteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRCloneRemote"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRCloneRemoteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createRCloneRemote"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"config"}}]}}]}}]} as unknown as DocumentNode<CreateRCloneRemoteMutation, CreateRCloneRemoteMutationVariables>;
export const CreateRCloneRemoteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRCloneRemote"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRCloneRemoteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createRCloneRemote"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}}]}}]}}]} as unknown as DocumentNode<CreateRCloneRemoteMutation, CreateRCloneRemoteMutationVariables>;
export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConnectSignIn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectSignInInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignIn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<ConnectSignInMutation, ConnectSignInMutationVariables>;
export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignOut"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignOut"}}]}}]} as unknown as DocumentNode<SignOutMutation, SignOutMutationVariables>;
export const ServerStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<ServerStateQuery, ServerStateQueryVariables>;