feat: substantial type cleanup

This commit is contained in:
Eli Bosley
2025-05-26 19:22:12 -04:00
parent 5fcb8da50b
commit 015c6e527b
11 changed files with 718 additions and 735 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[api]
version="4.4.1"
version="4.8.0"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox="yes"
+1 -17
View File
@@ -1,17 +1 @@
[
{
"id": "1ad3f9a9-f438-43b2-bdd5-976c7ca4b2f5",
"name": "test",
"sourcePath": "/Users/elibosley/Downloads",
"remoteName": "FlashBackup",
"destinationPath": "backup",
"schedule": "0 2 * * *",
"enabled": false,
"rcloneOptions": {},
"createdAt": "2025-05-24T12:19:29.150Z",
"updatedAt": "2025-05-26T16:14:13.977Z",
"lastRunStatus": "Started with job ID: 34",
"currentJobId": 34,
"lastRunAt": "2025-05-26T16:14:13.977Z"
}
]
[]
+43 -36
View File
@@ -916,46 +916,37 @@ type BackupMutations {
input CreateBackupJobConfigInput {
name: String!
backupMode: BackupMode! = PREPROCESSING
sourcePath: String!
backupType: BackupType! = RAW
remoteName: String!
destinationPath: String!
schedule: String!
schedule: String
enabled: Boolean! = true
rcloneOptions: JSON
"""Preprocessing configuration for this backup job"""
preprocessConfig: PreprocessConfigInput
"""Backup configuration for this backup job"""
backupConfig: BackupConfigInput
}
"""
The mode of backup to perform (Raw file backup or Preprocessing-based).
Type of backup to perform (ZFS snapshot, Flash backup, Custom script, or Raw file backup)
"""
enum BackupMode {
enum BackupType {
ZFS
FLASH
SCRIPT
RAW
PREPROCESSING
}
input PreprocessConfigInput {
"""Type of preprocessing to perform"""
type: PreprocessType!
zfsConfig: ZfsPreprocessConfigInput
flashConfig: FlashPreprocessConfigInput
scriptConfig: ScriptPreprocessConfigInput
"""Timeout for preprocessing in seconds"""
input BackupConfigInput {
"""Timeout for backup operation in seconds"""
timeout: Float! = 3600
"""Whether to cleanup on failure"""
cleanupOnFailure: Boolean! = true
}
"""Type of preprocessing to perform before backup"""
enum PreprocessType {
NONE
ZFS
FLASH
SCRIPT
zfsConfig: ZfsPreprocessConfigInput
flashConfig: FlashPreprocessConfigInput
scriptConfig: ScriptPreprocessConfigInput
rawConfig: RawBackupConfigInput
}
input ZfsPreprocessConfigInput {
@@ -1003,17 +994,28 @@ input ScriptPreprocessConfigInput {
outputPath: String!
}
input RawBackupConfigInput {
"""Source path to backup"""
sourcePath: String!
"""File patterns to exclude from backup"""
excludePatterns: [String!]
"""File patterns to include in backup"""
includePatterns: [String!]
}
input UpdateBackupJobConfigInput {
name: String
sourcePath: String
backupType: BackupType
remoteName: String
destinationPath: String
schedule: String
enabled: Boolean
rcloneOptions: JSON
"""Preprocessing configuration for this backup job"""
preprocessConfig: PreprocessConfigInput
"""Backup configuration for this backup job"""
backupConfig: BackupConfigInput
lastRunStatus: String
currentJobId: String
lastRunAt: String
@@ -1173,13 +1175,19 @@ type ScriptPreprocessConfig {
outputPath: String!
}
type PreprocessConfig {
type: PreprocessType!
type RawBackupConfig {
sourcePath: String!
excludePatterns: [String!]
includePatterns: [String!]
}
type BackupConfig {
timeout: Float!
cleanupOnFailure: Boolean!
zfsConfig: ZfsPreprocessConfig
flashConfig: FlashPreprocessConfig
scriptConfig: ScriptPreprocessConfig
timeout: Float!
cleanupOnFailure: Boolean!
rawConfig: RawBackupConfig
}
type RCloneDrive {
@@ -1377,10 +1385,9 @@ type BackupJobConfig implements Node {
"""Human-readable name for this backup job"""
name: String!
backupMode: BackupMode!
"""Source path to backup"""
sourcePath: String!
"""Type of backup to perform"""
backupType: BackupType!
"""Remote name from rclone config"""
remoteName: String!
@@ -1397,8 +1404,8 @@ type BackupJobConfig implements Node {
"""RClone options (e.g., --transfers, --checkers)"""
rcloneOptions: JSON
"""Preprocessing configuration for this backup job"""
preprocessConfig: PreprocessConfig
"""Backup configuration for this backup job"""
backupConfig: BackupConfig
"""When this config was created"""
createdAt: DateTime!
@@ -16,24 +16,15 @@ import {
import { GraphQLJSON } from 'graphql-scalars';
import {
PreprocessConfig,
PreprocessConfigInput,
BackupConfig,
BackupConfigInput,
BackupType,
} from '@app/unraid-api/graph/resolvers/backup/preprocessing/preprocessing.types.js';
import { Node } from '@app/unraid-api/graph/resolvers/base.model.js';
import { RCloneJob } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
export enum BackupMode {
RAW = 'RAW',
PREPROCESSING = 'PREPROCESSING',
}
registerEnumType(BackupMode, {
name: 'BackupMode',
description: 'The mode of backup to perform (Raw file backup or Preprocessing-based).',
});
@ObjectType({
implements: () => Node,
})
@@ -98,11 +89,8 @@ export class BackupJobConfig extends Node {
@Field(() => String, { description: 'Human-readable name for this backup job' })
name!: string;
@Field(() => BackupMode)
backupMode!: BackupMode;
@Field(() => String, { description: 'Source path to backup' })
sourcePath!: string;
@Field(() => BackupType, { description: 'Type of backup to perform' })
backupType!: BackupType;
@Field(() => String, { description: 'Remote name from rclone config' })
remoteName!: string;
@@ -124,11 +112,11 @@ export class BackupJobConfig extends Node {
})
rcloneOptions?: Record<string, unknown>;
@Field(() => PreprocessConfig, {
description: 'Preprocessing configuration for this backup job',
@Field(() => BackupConfig, {
description: 'Backup configuration for this backup job',
nullable: true,
})
preprocessConfig?: PreprocessConfig;
backupConfig?: BackupConfig;
@Field(() => Date, { description: 'When this config was created' })
createdAt!: Date;
@@ -150,77 +138,17 @@ export class BackupJobConfig extends Node {
}
@InputType()
export class CreateBackupJobConfigInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
name!: string;
@Field(() => BackupMode, { defaultValue: BackupMode.PREPROCESSING })
@IsEnum(BackupMode)
@IsNotEmpty()
backupMode?: BackupMode;
@Field(() => String)
@IsString()
@ValidateIf((o) => o.backupMode === BackupMode.RAW)
@IsNotEmpty({ message: 'sourcePath should not be empty when backupMode is RAW' })
sourcePath!: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
remoteName!: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
destinationPath!: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
@Matches(
/^(\*|[0-5]?\d)(\s+(\*|[0-1]?\d|2[0-3]))(\s+(\*|[1-2]?\d|3[0-1]))(\s+(\*|[1-9]|1[0-2]))(\s+(\*|[0-6]))$/,
{
message: 'schedule must be a valid cron expression',
}
)
schedule!: string;
@Field(() => Boolean, { defaultValue: true })
@IsBoolean()
enabled!: boolean;
@Field(() => GraphQLJSON, { nullable: true })
@IsOptional()
@IsObject()
rcloneOptions?: Record<string, unknown>;
@Field(() => PreprocessConfigInput, {
description: 'Preprocessing configuration for this backup job',
nullable: true,
})
@IsOptional()
@ValidateIf((o) => o.backupMode === BackupMode.PREPROCESSING)
@ValidateNested()
@Type(() => PreprocessConfigInput)
preprocessConfig?: PreprocessConfigInput;
}
@InputType()
export class UpdateBackupJobConfigInput {
export class BaseBackupJobConfigInput {
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@IsNotEmpty()
name?: string;
@Field(() => String, { nullable: true })
@Field(() => BackupType, { nullable: true })
@IsOptional()
@IsString()
@IsNotEmpty()
sourcePath?: string;
@IsEnum(BackupType)
backupType?: BackupType;
@Field(() => String, { nullable: true })
@IsOptional()
@@ -237,7 +165,7 @@ export class UpdateBackupJobConfigInput {
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@IsNotEmpty()
@ValidateIf((o) => o.schedule && o.schedule.length > 0)
@Matches(
/^(\*|[0-5]?\d)(\s+(\*|[0-1]?\d|2[0-3]))(\s+(\*|[1-2]?\d|3[0-1]))(\s+(\*|[1-9]|1[0-2]))(\s+(\*|[0-6]))$/,
{
@@ -256,15 +184,46 @@ export class UpdateBackupJobConfigInput {
@IsObject()
rcloneOptions?: Record<string, unknown>;
@Field(() => PreprocessConfigInput, {
description: 'Preprocessing configuration for this backup job',
@Field(() => BackupConfigInput, {
description: 'Backup configuration for this backup job',
nullable: true,
})
@IsOptional()
@ValidateNested()
@Type(() => PreprocessConfigInput)
preprocessConfig?: PreprocessConfigInput;
@Type(() => BackupConfigInput)
backupConfig?: BackupConfigInput;
}
@InputType()
export class CreateBackupJobConfigInput extends BaseBackupJobConfigInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
declare name: string;
@Field(() => BackupType, { defaultValue: BackupType.RAW })
@IsEnum(BackupType)
@IsNotEmpty()
declare backupType: BackupType;
@Field(() => String)
@IsString()
@IsNotEmpty()
declare remoteName: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
declare destinationPath: string;
@Field(() => Boolean, { defaultValue: true })
@IsBoolean()
@ValidateIf((o) => o.schedule && o.schedule.length > 0)
declare enabled: boolean;
}
@InputType()
export class UpdateBackupJobConfigInput extends BaseBackupJobConfigInput {
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@ import { Field, InputType, ObjectType, registerEnumType } from '@nestjs/graphql'
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
@@ -14,16 +15,17 @@ import {
} from 'class-validator';
import { GraphQLJSON } from 'graphql-scalars';
export enum PreprocessType {
NONE = 'none',
ZFS = 'zfs',
FLASH = 'flash',
SCRIPT = 'script',
export enum BackupType {
ZFS = 'ZFS',
FLASH = 'FLASH',
SCRIPT = 'SCRIPT',
RAW = 'RAW',
}
registerEnumType(PreprocessType, {
name: 'PreprocessType',
description: 'Type of preprocessing to perform before backup',
registerEnumType(BackupType, {
name: 'BackupType',
description:
'Type of backup to perform (ZFS snapshot, Flash backup, Custom script, or Raw file backup)',
});
@InputType()
@@ -151,46 +153,80 @@ export class ScriptPreprocessConfig {
}
@InputType()
export class PreprocessConfigInput {
@Field(() => PreprocessType, { description: 'Type of preprocessing to perform' })
@IsEnum(PreprocessType)
type!: PreprocessType;
export class RawBackupConfigInput {
@Field(() => String, { description: 'Source path to backup' })
@IsString()
@IsNotEmpty()
sourcePath!: string;
@Field(() => [String], { description: 'File patterns to exclude from backup', nullable: true })
@IsOptional()
@IsArray()
excludePatterns?: string[];
@Field(() => [String], { description: 'File patterns to include in backup', nullable: true })
@IsOptional()
@IsArray()
includePatterns?: string[];
}
@ObjectType()
export class RawBackupConfig {
@Field(() => String)
sourcePath!: string;
@Field(() => [String], { nullable: true })
excludePatterns?: string[];
@Field(() => [String], { nullable: true })
includePatterns?: string[];
}
@InputType()
export class BackupConfigInput {
@Field(() => Number, { description: 'Timeout for backup operation in seconds', defaultValue: 3600 })
@IsOptional()
@IsNumber()
@Min(1)
timeout?: number;
@Field(() => Boolean, { description: 'Whether to cleanup on failure', defaultValue: true })
@IsOptional()
@IsBoolean()
cleanupOnFailure?: boolean;
@Field(() => ZfsPreprocessConfigInput, { nullable: true })
@IsOptional()
@ValidateIf((o) => o.type === PreprocessType.ZFS)
@ValidateNested()
@Type(() => ZfsPreprocessConfigInput)
zfsConfig?: ZfsPreprocessConfigInput;
@Field(() => FlashPreprocessConfigInput, { nullable: true })
@IsOptional()
@ValidateIf((o) => o.type === PreprocessType.FLASH)
@ValidateNested()
@Type(() => FlashPreprocessConfigInput)
flashConfig?: FlashPreprocessConfigInput;
@Field(() => ScriptPreprocessConfigInput, { nullable: true })
@IsOptional()
@ValidateIf((o) => o.type === PreprocessType.SCRIPT)
@ValidateNested()
@Type(() => ScriptPreprocessConfigInput)
scriptConfig?: ScriptPreprocessConfigInput;
@Field(() => Number, { description: 'Timeout for preprocessing in seconds', defaultValue: 3600 })
@IsNumber()
@Min(1)
timeout!: number;
@Field(() => Boolean, { description: 'Whether to cleanup on failure', defaultValue: true })
@IsBoolean()
cleanupOnFailure!: boolean;
@Field(() => RawBackupConfigInput, { nullable: true })
@IsOptional()
@ValidateNested()
@Type(() => RawBackupConfigInput)
rawConfig?: RawBackupConfigInput;
}
@ObjectType()
export class PreprocessConfig {
@Field(() => PreprocessType)
type!: PreprocessType;
export class BackupConfig {
@Field(() => Number)
timeout!: number;
@Field(() => Boolean)
cleanupOnFailure!: boolean;
@Field(() => ZfsPreprocessConfig, { nullable: true })
zfsConfig?: ZfsPreprocessConfig;
@@ -201,11 +237,8 @@ export class PreprocessConfig {
@Field(() => ScriptPreprocessConfig, { nullable: true })
scriptConfig?: ScriptPreprocessConfig;
@Field(() => Number)
timeout!: number;
@Field(() => Boolean)
cleanupOnFailure!: boolean;
@Field(() => RawBackupConfig, { nullable: true })
rawConfig?: RawBackupConfig;
}
export interface PreprocessResult {
@@ -222,8 +255,14 @@ export interface StreamingJobInfo {
jobId: string;
processId: number;
startTime: Date;
type: PreprocessType;
type: BackupType;
status: 'running' | 'completed' | 'failed' | 'cancelled';
progress?: number;
error?: string;
}
// Type aliases for backward compatibility
export type PreprocessType = BackupType;
export const PreprocessType = BackupType;
export type PreprocessConfig = BackupConfig;
export type PreprocessConfigInput = BackupConfigInput;
@@ -11,7 +11,7 @@ import { RCloneJob, RCloneRemote } from '@app/unraid-api/graph/resolvers/rclone/
* Types for rclone backup configuration UI
*/
export interface RcloneBackupConfigValues {
configStep: number;
configStep: { current: number; total: number };
showAdvanced: boolean;
name?: string;
type?: string;
@@ -77,7 +77,7 @@ export class RCloneService {
*/
async getCurrentSettings(): Promise<RcloneBackupConfigValues> {
return {
configStep: 0,
configStep: { current: 0, total: 0 },
showAdvanced: false,
};
}
+22 -9
View File
@@ -16,7 +16,7 @@ import {
type UISchemaElement,
} from '@jsonforms/core';
import { DispatchRenderer, useJsonFormsLayout, type RendererProps } from '@jsonforms/vue';
import { computed, inject, ref, type Ref } from 'vue';
import { computed, inject, nextTick, onMounted, ref, type Ref } from 'vue';
// Define props based on RendererProps<Layout>
const props = defineProps<RendererProps<Layout>>();
@@ -48,15 +48,30 @@ const numSteps = computed(() => stepsConfig.value.length);
// --- Current Step Logic --- Use injected core.data
const currentStep = computed(() => {
const stepData = core!.data?.configStep;
// Handle both the new object format and the old number format
// Return current step if properly initialized
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));
// Return 0 as default if not initialized yet
return 0;
});
// Initialize configStep on mount
onMounted(async () => {
// Wait for next tick to ensure form data is available
await nextTick();
const stepData = core!.data?.configStep;
// Only initialize if configStep doesn't exist or is in wrong format
if (!stepData || typeof stepData !== 'object' || typeof stepData.current !== 'number') {
const initialStep = { current: 0, total: numSteps.value };
dispatch(Actions.update('configStep', () => initialStep));
}
});
const isLastStep = computed(() => numSteps.value > 0 && currentStep.value === numSteps.value - 1);
// --- Step Update Logic ---
@@ -81,6 +96,7 @@ const currentStepElements = computed(() => {
element.options.step === currentStep.value
);
});
return filtered;
});
@@ -142,9 +158,6 @@ const getStepState = (stepIndex: number): StepState => {
/>
</StepperItem>
</Stepper>
<!-- Render elements for the current step -->
<!-- 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"
+66 -48
View File
@@ -1,18 +1,19 @@
<script lang="ts" setup>
import { computed, provide, ref, watch } from 'vue';
import { useMutation, useQuery } from '@vue/apollo-composable';
import type { Ref } from 'vue';
import { Button, JsonForms } from '@unraid/ui';
import { CREATE_BACKUP_JOB_CONFIG_MUTATION, BACKUP_JOB_CONFIG_FORM_QUERY } from './backup-jobs.query';
import type { CreateBackupJobConfigInput, InputMaybe, PreprocessConfigInput, BackupMode } from '~/composables/gql/graphql';
import type { CreateBackupJobConfigInput } from '~/composables/gql/graphql';
import type { Ref } from 'vue';
import { BACKUP_JOB_CONFIG_FORM_QUERY, CREATE_BACKUP_JOB_CONFIG_MUTATION } from './backup-jobs.query';
// Define emit events
const emit = defineEmits<{
complete: []
cancel: []
}>()
complete: [];
cancel: [];
}>();
// Define types for form state
interface ConfigStep {
@@ -30,7 +31,8 @@ const {
refetch: updateFormSchema,
} = useQuery(BACKUP_JOB_CONFIG_FORM_QUERY, {
input: {
showAdvanced: typeof formState.value?.showAdvanced === 'boolean' ? formState.value.showAdvanced : false,
showAdvanced:
typeof formState.value?.showAdvanced === 'boolean' ? formState.value.showAdvanced : false,
},
});
@@ -39,21 +41,31 @@ let refetchTimeout: NodeJS.Timeout | null = null;
watch(
formState,
async (newValue, oldValue) => {
const newStepCurrent = typeof (newValue?.configStep) === 'object' ? (newValue.configStep as ConfigStep).current : (newValue?.configStep as number);
const oldStepCurrent = typeof (oldValue?.configStep) === 'object' ? (oldValue.configStep as ConfigStep).current : (oldValue?.configStep as number);
const newStepCurrent = (newValue?.configStep as ConfigStep)?.current ?? 0;
const oldStepCurrent = (oldValue?.configStep as ConfigStep)?.current ?? 0;
const newShowAdvanced = typeof newValue?.showAdvanced === 'boolean' ? newValue.showAdvanced : false;
const oldShowAdvanced = typeof oldValue?.showAdvanced === 'boolean' ? oldValue.showAdvanced : false;
const shouldRefetch = newShowAdvanced !== oldShowAdvanced || newStepCurrent !== oldStepCurrent;
if (shouldRefetch) {
if (newShowAdvanced !== oldShowAdvanced) {
console.log('[BackupJobConfigForm] showAdvanced changed:', newShowAdvanced);
}
if (newStepCurrent !== oldStepCurrent) {
console.log('[BackupJobConfigForm] configStep.current changed:', newStepCurrent, 'from:', oldStepCurrent, 'Refetching schema.');
console.log(
'[BackupJobConfigForm] configStep.current changed:',
newStepCurrent,
'from:',
oldStepCurrent,
'Refetching schema.'
);
}
// Debounce refetch to prevent multiple rapid calls
if (refetchTimeout) {
clearTimeout(refetchTimeout);
}
refetchTimeout = setTimeout(async () => {
await updateFormSchema({
input: {
@@ -80,22 +92,9 @@ const {
// Handle form submission
const submitForm = async () => {
try {
const value = formState.value as Record<string, unknown>;
console.log('value', value);
console.log('[BackupJobConfigForm] submitForm', value);
const input: CreateBackupJobConfigInput = {
name: value?.name as string,
destinationPath: value?.destinationPath as string,
schedule: (value?.schedule as string) || '',
enabled: value?.enabled as boolean,
remoteName: value?.remoteName as string,
sourcePath: (value?.sourcePath as string) || '',
rcloneOptions: value?.rcloneOptions as Record<string, unknown>,
preprocessConfig: value?.preprocessConfig as InputMaybe<PreprocessConfigInput> | undefined,
backupMode: (value?.backupMode as BackupMode) || 'RAW' as BackupMode,
};
const { configStep, ...input } = formState.value;
await createBackupJobConfig({
input,
input: input as CreateBackupJobConfigInput,
});
} catch (error) {
console.error('Error creating backup job config:', error);
@@ -116,14 +115,30 @@ onCreateDone(async ({ data }) => {
const parsedOriginalErrorMessage = computed(() => {
const originalError = createError.value?.graphQLErrors?.[0]?.extensions?.originalError;
if (originalError && typeof originalError === 'object' && originalError !== null && 'message' in originalError) {
if (
originalError &&
typeof originalError === 'object' &&
originalError !== null &&
'message' in originalError
) {
return (originalError as { message: string | string[] }).message;
}
return undefined;
});
let changeTimeout: NodeJS.Timeout | null = null;
const onChange = ({ data }: { data: unknown }) => {
console.log('[BackupJobConfigForm] onChange', data);
// Clear any pending timeout
if (changeTimeout) {
clearTimeout(changeTimeout);
}
// Debounce logging to reduce console spam
changeTimeout = setTimeout(() => {
console.log('[BackupJobConfigForm] onChange', data);
changeTimeout = null;
}, 300);
formState.value = data as Record<string, unknown>;
};
@@ -132,8 +147,8 @@ const uiSchema = computed(() => formResult.value?.backupJobConfigForm?.uiSchema)
// Handle both number and object formats of configStep
const getCurrentStep = computed(() => {
const step = formState.value?.configStep;
return typeof step === 'object' ? (step as ConfigStep).current : step as number;
const step = formState.value?.configStep as ConfigStep;
return step?.current ?? 0;
});
// Get total steps from UI schema
@@ -157,21 +172,33 @@ provide('isSubmitting', isCreating);
</script>
<template>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
<div
class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
>
<div class="p-6">
<h2 class="text-xl font-medium mb-4 text-gray-900 dark:text-white">Configure Backup Job</h2>
<div v-if="createError" class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 rounded-md">
<div
v-if="createError"
class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 rounded-md"
>
<p>{{ createError.message }}</p>
<ul v-if="Array.isArray(parsedOriginalErrorMessage)" class="list-disc list-inside mt-2">
<li v-for="(msg, index) in parsedOriginalErrorMessage" :key="index">{{ msg }}</li>
</ul>
<p v-else-if="typeof parsedOriginalErrorMessage === 'string' && parsedOriginalErrorMessage.length > 0" class="mt-2">
<p
v-else-if="
typeof parsedOriginalErrorMessage === 'string' && parsedOriginalErrorMessage.length > 0
"
class="mt-2"
>
{{ parsedOriginalErrorMessage }}
</p>
</div>
<div v-if="formLoading" class="py-8 text-center text-gray-500 dark:text-gray-400">Loading configuration form...</div>
<div v-if="formLoading" class="py-8 text-center text-gray-500 dark:text-gray-400">
Loading configuration form...
</div>
<!-- Form -->
<div v-else-if="formResult?.backupJobConfigForm" class="mt-6 [&_.vertical-layout]:space-y-6">
@@ -190,32 +217,23 @@ provide('isSubmitting', isCreating);
v-if="!formLoading && uiSchema && isLastStep"
class="mt-6 flex justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6"
>
<Button variant="outline" @click="emit('cancel')">
Cancel
</Button>
<Button :loading="isCreating" @click="submitForm">
Create Backup Job
</Button>
<Button variant="outline" @click="emit('cancel')"> Cancel </Button>
<Button :loading="isCreating" @click="submitForm"> Create Backup Job </Button>
</div>
<!-- If there's no stepped layout, show buttons at the bottom -->
<div
v-if="!formLoading && (!uiSchema || (numSteps === 0))"
v-if="!formLoading && (!uiSchema || numSteps === 0)"
class="mt-6 flex justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6"
>
<Button variant="outline" @click="emit('cancel')">
Cancel
</Button>
<Button :loading="isCreating" @click="submitForm">
Create Backup Job
</Button>
<Button variant="outline" @click="emit('cancel')"> Cancel </Button>
<Button :loading="isCreating" @click="submitForm"> Create Backup Job </Button>
</div>
</div>
</div>
</template>
<style lang="postcss">
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
</style>
</style>
+6 -11
View File
@@ -30,7 +30,7 @@ interface ConfigStep {
// Form state
const formState = ref(props.initialState || {
configStep: 0 as number | ConfigStep,
configStep: { current: 0, total: 0 },
showAdvanced: false,
name: '',
type: '',
@@ -56,13 +56,8 @@ watch(
formState,
async (newValue, oldValue) => {
// Get current step as number for comparison
const newStep = typeof newValue.configStep === 'object'
? (newValue.configStep as ConfigStep).current
: newValue.configStep as number;
const oldStep = typeof oldValue.configStep === 'object'
? (oldValue.configStep as ConfigStep).current
: oldValue.configStep as number;
const newStep = (newValue.configStep as ConfigStep)?.current ?? 0;
const oldStep = (oldValue.configStep as ConfigStep)?.current ?? 0;
// Check if we need to refetch
const shouldRefetch =
@@ -138,7 +133,7 @@ onCreateDone(async ({ data }) => {
// Reset form and emit complete event
formState.value = {
configStep: 0,
configStep: { current: 0, total: 0 },
showAdvanced: false,
name: '',
type: '',
@@ -179,8 +174,8 @@ const uiSchema = computed(() => formResult.value?.rclone?.configForm?.uiSchema);
// Handle both number and object formats of configStep
const getCurrentStep = computed(() => {
const step = formState.value.configStep;
return typeof step === 'object' ? (step as ConfigStep).current : step as number;
const step = formState.value.configStep as ConfigStep;
return step?.current ?? 0;
});
// Get total steps from UI schema
+2 -2
View File
@@ -10,7 +10,7 @@ import RCloneConfig from './RCloneConfig.vue';
import RemoteItem from './RemoteItem.vue';
interface FormState {
configStep: number;
configStep: { current: number; total: number };
showAdvanced: boolean;
name: string;
type: string;
@@ -63,7 +63,7 @@ const openCryptModal = (remote: { name: string, type: string }) => {
const entropy = Math.random().toString(36).substring(2, 8);
selectedRemote.value = remote;
initialFormState.value = {
configStep: 0,
configStep: { current: 0, total: 0 },
showAdvanced: false,
name: `${remote.name}-crypt-${entropy}`,
type: 'crypt',