mirror of
https://github.com/unraid/api.git
synced 2026-01-08 09:39:49 -06:00
- Introduced @layer directive to ensure base styles have lower priority than Tailwind utilities. - Organized CSS resets for box-sizing, figures, headings, paragraphs, and unordered lists under a single @layer base block for improved maintainability. These changes streamline the CSS structure and enhance compatibility with Tailwind CSS utilities. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Style - Wrapped core resets in a base style layer, adjusting cascade with utility classes. - Applied global box-sizing within the base layer. - Consolidated heading and paragraph resets into the layer. - Added a reset for unordered lists to remove default bullets and padding. - Retained the logo figure reset within the layer. - Updated formatting and header comments to reflect the layering approach. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
254 lines
8.6 KiB
Vue
254 lines
8.6 KiB
Vue
<script setup lang="ts">
|
||
import { Button } from '@/components/common/button';
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/common/tabs';
|
||
import { jsonFormsAjv } from '@/forms/config';
|
||
// Import the renderers and AJV directly
|
||
import { jsonFormsRenderers } from '@/forms/renderers';
|
||
import type { ControlElement, JsonSchema, UISchemaElement } from '@jsonforms/core';
|
||
import { JsonForms, useJsonFormsControl } from '@jsonforms/vue';
|
||
import type { RendererProps } from '@jsonforms/vue';
|
||
import { Plus, X } from 'lucide-vue-next';
|
||
import { computed, ref, watch } from 'vue';
|
||
|
||
const props = defineProps<RendererProps<ControlElement>>();
|
||
const { control, handleChange } = useJsonFormsControl(props);
|
||
|
||
// Use the imported renderers
|
||
const renderers = jsonFormsRenderers;
|
||
|
||
const items = computed({
|
||
get: () => {
|
||
const data = control.value.data ?? [];
|
||
return Array.isArray(data) ? data : [];
|
||
},
|
||
set: (newValue: unknown[]) => {
|
||
handleChange(control.value.path, newValue);
|
||
},
|
||
});
|
||
|
||
// Track active tab
|
||
const activeTab = ref<string>('0');
|
||
|
||
// Update active tab when items change
|
||
watch(
|
||
() => items.value.length,
|
||
(newLength, oldLength) => {
|
||
if (newLength > oldLength) {
|
||
// When adding a new item, switch to the new tab
|
||
activeTab.value = String(newLength - 1);
|
||
} else if (newLength < oldLength && Number(activeTab.value) >= newLength) {
|
||
// When removing an item, ensure active tab is valid
|
||
activeTab.value = String(Math.max(0, newLength - 1));
|
||
}
|
||
}
|
||
);
|
||
|
||
// Get the detail layout from options or create a default one
|
||
const detailLayout = computed(() => {
|
||
const options = control.value.uischema?.options;
|
||
if (options?.detail) {
|
||
return options.detail as UISchemaElement;
|
||
}
|
||
|
||
// Create a default vertical layout with all properties
|
||
const schema = control.value.schema;
|
||
if (schema?.items && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
|
||
const properties = schema.items.properties;
|
||
if (properties && typeof properties === 'object') {
|
||
return {
|
||
type: 'VerticalLayout',
|
||
elements: Object.keys(properties).map((key) => ({
|
||
type: 'Control',
|
||
scope: `#/properties/${key}`,
|
||
})),
|
||
} as UISchemaElement;
|
||
}
|
||
}
|
||
|
||
return { type: 'VerticalLayout', elements: [] } as UISchemaElement;
|
||
});
|
||
|
||
// Get the property to use as the item label
|
||
const elementLabelProp = computed(() => {
|
||
const options = control.value.uischema?.options as Record<string, unknown> | undefined;
|
||
return (options?.elementLabelProp as string) ?? 'name';
|
||
});
|
||
|
||
// Get the item type name (e.g., "Provider", "Rule", etc.)
|
||
const itemTypeName = computed(() => {
|
||
const options = control.value.uischema?.options as Record<string, unknown> | undefined;
|
||
return (options?.itemTypeName as string) ?? 'Provider';
|
||
});
|
||
|
||
const getItemLabel = (item: unknown, index: number) => {
|
||
if (item && typeof item === 'object' && item !== null && elementLabelProp.value in item) {
|
||
const itemObj = item as Record<string, unknown>;
|
||
return String(itemObj[elementLabelProp.value] || `${itemTypeName.value} ${index + 1}`);
|
||
}
|
||
return `${itemTypeName.value} ${index + 1}`;
|
||
};
|
||
|
||
// Check if an item is protected based on options configuration
|
||
const isItemProtected = (item: unknown): boolean => {
|
||
const options = control.value.uischema?.options as Record<string, unknown> | undefined;
|
||
const protectedItems = options?.protectedItems as Array<{ field: string; value: unknown }> | undefined;
|
||
|
||
if (!protectedItems || !item || typeof item !== 'object') {
|
||
return false;
|
||
}
|
||
|
||
const itemObj = item as Record<string, unknown>;
|
||
return protectedItems.some((rule) => rule.field in itemObj && itemObj[rule.field] === rule.value);
|
||
};
|
||
|
||
// Get warning message for an item if it matches warning conditions
|
||
const getItemWarning = (item: unknown): { title: string; description: string } | null => {
|
||
const options = control.value.uischema?.options as Record<string, unknown> | undefined;
|
||
const itemWarnings = options?.itemWarnings as
|
||
| Array<{
|
||
condition: { field: string; value: unknown };
|
||
title: string;
|
||
description: string;
|
||
}>
|
||
| undefined;
|
||
|
||
if (!itemWarnings || !item || typeof item !== 'object') {
|
||
return null;
|
||
}
|
||
|
||
const itemObj = item as Record<string, unknown>;
|
||
const warning = itemWarnings.find(
|
||
(w) => w.condition.field in itemObj && itemObj[w.condition.field] === w.condition.value
|
||
);
|
||
|
||
return warning ? { title: warning.title, description: warning.description } : null;
|
||
};
|
||
|
||
const addItem = () => {
|
||
const schema = control.value.schema;
|
||
const newItem: Record<string, unknown> = {};
|
||
|
||
// Initialize with default values if available
|
||
if (schema?.items && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
|
||
const properties = schema.items.properties;
|
||
if (properties && typeof properties === 'object') {
|
||
Object.entries(properties).forEach(([key, propSchema]) => {
|
||
const schema = propSchema as JsonSchema;
|
||
if (schema.default !== undefined) {
|
||
newItem[key] = schema.default;
|
||
} else if (schema.type === 'array') {
|
||
newItem[key] = [];
|
||
} else if (schema.type === 'string') {
|
||
newItem[key] = '';
|
||
} else if (schema.type === 'number' || schema.type === 'integer') {
|
||
newItem[key] = 0;
|
||
} else if (schema.type === 'boolean') {
|
||
newItem[key] = false;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
items.value = [...items.value, newItem];
|
||
};
|
||
|
||
const removeItem = (index: number) => {
|
||
// Create a completely new array by filtering out the item
|
||
const newItems = items.value.filter((_, i) => i !== index);
|
||
items.value = newItems;
|
||
};
|
||
|
||
const updateItem = (index: number, newValue: unknown) => {
|
||
const newItems = [...items.value];
|
||
newItems[index] = newValue;
|
||
items.value = newItems;
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<div class="w-full">
|
||
<Tabs v-if="items.length > 0" v-model="activeTab" class="w-full">
|
||
<div class="mb-4 flex items-center gap-2">
|
||
<TabsList class="flex-1 flex-wrap">
|
||
<TabsTrigger
|
||
v-for="(item, index) in items"
|
||
:key="index"
|
||
:value="String(index)"
|
||
class="flex items-center gap-2"
|
||
>
|
||
{{ getItemLabel(item, index) }}
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
class="h-9 w-9"
|
||
:disabled="!control.enabled"
|
||
@click="addItem"
|
||
>
|
||
<Plus class="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
<TabsContent
|
||
v-for="(item, index) in items"
|
||
:key="index"
|
||
:value="String(index)"
|
||
class="mt-0 w-full"
|
||
>
|
||
<div class="border-muted w-full rounded-lg border p-1 sm:p-6">
|
||
<div class="mb-4 flex justify-end">
|
||
<Button
|
||
v-if="!isItemProtected(item)"
|
||
variant="ghost"
|
||
size="sm"
|
||
class="text-destructive hover:text-destructive"
|
||
:disabled="!control.enabled"
|
||
@click="removeItem(index)"
|
||
>
|
||
<X class="mr-2 h-4 w-4" />
|
||
Remove {{ getItemLabel(item, index) }}
|
||
</Button>
|
||
</div>
|
||
<div class="w-full max-w-none">
|
||
<!-- Show warning if item matches protected condition -->
|
||
<div
|
||
v-if="getItemWarning(item)"
|
||
class="bg-warning/10 border-warning/20 border-muted mb-4 rounded-lg border p-3"
|
||
>
|
||
<div class="flex items-start gap-2">
|
||
<span class="text-warning">⚠️</span>
|
||
<div>
|
||
<div class="text-warning font-medium">{{ getItemWarning(item)?.title }}</div>
|
||
<div class="text-muted-foreground mt-1 text-sm">
|
||
{{ getItemWarning(item)?.description }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<JsonForms
|
||
:data="item"
|
||
:schema="control.schema.items as JsonSchema"
|
||
:uischema="detailLayout"
|
||
:renderers="renderers"
|
||
:ajv="jsonFormsAjv"
|
||
:readonly="!control.enabled"
|
||
@change="({ data }) => updateItem(index, data)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
<div v-else class="border-muted rounded-lg border-2 border-dashed py-8 text-center">
|
||
<p class="text-muted-foreground mb-4 text-center">
|
||
No {{ itemTypeName.toLowerCase() }}s configured
|
||
</p>
|
||
<Button variant="outline" size="md" :disabled="!control.enabled" @click="addItem">
|
||
<Plus class="mr-2 h-4 w-4" />
|
||
Add First {{ itemTypeName }}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</template>
|