mirror of
https://github.com/unraid/api.git
synced 2026-01-04 23:50:37 -06:00
chore: lint
This commit is contained in:
@@ -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!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
<template>
|
||||
<div id="modals"></div>
|
||||
</template>
|
||||
<div id="modals" />
|
||||
</template>
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as Modals } from './ModalTarget.vue';
|
||||
export { default as Modals } from './ModalTarget.vue';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -14,4 +14,3 @@ export { default as tailwindConfig } from '../tailwind.config';
|
||||
|
||||
// Composables
|
||||
export { default as useTeleport } from '@/composables/useTeleport';
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
2
unraid-ui/src/vite-env.d.ts
vendored
2
unraid-ui/src/vite-env.d.ts
vendored
@@ -3,4 +3,4 @@
|
||||
declare module '*.css?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user