Phase 1 - Vendor Router Integration: - Wire up vendor module routers in app/api/v1/vendor/__init__.py - Use lazy imports via __getattr__ to avoid circular dependencies Phase 2 - Extract Remaining Modules: - Create 6 new module directories: customers, cms, analytics, messaging, dev_tools, monitoring - Each module has definition.py and route wrappers - Update registry to import from extracted modules Phase 3 - Database Table Migration: - Add PlatformModule junction table for auditable module tracking - Add migration zc2m3n4o5p6q7_add_platform_modules_table.py - Add modules relationship to Platform model - Update ModuleService with JSON-to-junction-table migration Phase 4 - Module-Specific Configuration UI: - Add /api/v1/admin/module-config/* endpoints - Add module-config.html template and JS Phase 5 - Integration Tests: - Add tests/fixtures/module_fixtures.py - Add tests/integration/api/v1/admin/test_modules.py - Add tests/integration/api/v1/modules/test_module_access.py Architecture fixes: - Fix JS-003 errors: use ...data() directly in Alpine components - Fix JS-005 warnings: add init() guards to prevent duplicate init - Fix API-001 errors: add MenuActionResponse Pydantic model - Add FE-008 noqa for dynamic number input in template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
149 lines
8.0 KiB
HTML
149 lines
8.0 KiB
HTML
{# app/templates/admin/module-config.html #}
|
|
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
|
{% from 'shared/macros/headers.html' import page_header %}
|
|
|
|
{% block title %}Module Configuration{% endblock %}
|
|
|
|
{% block alpine_data %}adminModuleConfig('{{ platform_code }}', '{{ module_code }}'){% endblock %}
|
|
|
|
{% block content %}
|
|
{{ page_header('Module Configuration', back_url='/admin/platforms/' + platform_code + '/modules') }}
|
|
|
|
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
|
|
{{ error_state('Error', show_condition='error') }}
|
|
|
|
<!-- Module Info -->
|
|
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleInfo?.module_name || 'Loading...'"></h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Configure settings for this module on <span x-text="platformName" class="font-medium"></span>.
|
|
</p>
|
|
</div>
|
|
<span class="px-3 py-1 text-sm font-medium rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" x-text="moduleInfo?.module_code?.toUpperCase()"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div x-show="loading" class="flex items-center justify-center py-12 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<span x-html="$icon('refresh', 'w-8 h-8 animate-spin text-purple-600')"></span>
|
|
<span class="ml-3 text-gray-500 dark:text-gray-400">Loading configuration...</span>
|
|
</div>
|
|
|
|
<!-- Configuration Form -->
|
|
<div x-show="!loading" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Configuration Options</h3>
|
|
</div>
|
|
|
|
<!-- Config Fields -->
|
|
<div class="p-4 space-y-6">
|
|
<template x-if="moduleInfo?.schema_info?.length > 0">
|
|
<div class="space-y-6">
|
|
<template x-for="field in moduleInfo.schema_info" :key="field.key">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2" x-text="field.label"></label>
|
|
|
|
<!-- Boolean field -->
|
|
<template x-if="field.type === 'boolean'">
|
|
<div class="flex items-center">
|
|
<button
|
|
@click="config[field.key] = !config[field.key]"
|
|
:class="{
|
|
'bg-purple-600': config[field.key],
|
|
'bg-gray-200 dark:bg-gray-600': !config[field.key]
|
|
}"
|
|
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
|
|
role="switch"
|
|
:aria-checked="config[field.key]"
|
|
>
|
|
<span
|
|
:class="{
|
|
'translate-x-5': config[field.key],
|
|
'translate-x-0': !config[field.key]
|
|
}"
|
|
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
|
></span>
|
|
</button>
|
|
<span class="ml-3 text-sm text-gray-500 dark:text-gray-400" x-text="config[field.key] ? 'Enabled' : 'Disabled'"></span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Number field (dynamic Alpine.js template - cannot use static macro) --> {# noqa: FE-008 #}
|
|
<template x-if="field.type === 'number'">
|
|
<input
|
|
type="number"
|
|
x-model.number="config[field.key]"
|
|
:min="field.min"
|
|
:max="field.max"
|
|
class="block w-full max-w-xs px-3 py-2 text-sm leading-5 text-gray-700 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring focus:ring-purple-400"
|
|
>
|
|
</template>
|
|
|
|
<!-- String field -->
|
|
<template x-if="field.type === 'string'">
|
|
<input
|
|
type="text"
|
|
x-model="config[field.key]"
|
|
class="block w-full max-w-md px-3 py-2 text-sm leading-5 text-gray-700 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring focus:ring-purple-400"
|
|
>
|
|
</template>
|
|
|
|
<!-- Select field -->
|
|
<template x-if="field.type === 'select'">
|
|
<select
|
|
x-model="config[field.key]"
|
|
class="block w-full max-w-xs px-3 py-2 text-sm leading-5 text-gray-700 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring focus:ring-purple-400"
|
|
>
|
|
<template x-for="option in field.options" :key="option">
|
|
<option :value="option" x-text="option"></option>
|
|
</template>
|
|
</select>
|
|
</template>
|
|
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="field.description"></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- No config options -->
|
|
<template x-if="!moduleInfo?.schema_info?.length">
|
|
<div class="text-center py-8">
|
|
<span x-html="$icon('cog', 'w-12 h-12 mx-auto text-gray-400')"></span>
|
|
<p class="mt-4 text-gray-500 dark:text-gray-400">No configuration options available for this module.</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<button
|
|
@click="resetToDefaults()"
|
|
:disabled="saving"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600 disabled:opacity-50"
|
|
>
|
|
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
|
Reset to Defaults
|
|
</button>
|
|
|
|
<button
|
|
@click="saveConfig()"
|
|
:disabled="saving"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none focus:ring focus:ring-purple-400 disabled:opacity-50"
|
|
>
|
|
<span x-show="saving" x-html="$icon('refresh', 'w-4 h-4 mr-2 animate-spin')"></span>
|
|
<span x-show="!saving" x-html="$icon('check', 'w-4 h-4 mr-2')"></span>
|
|
<span x-text="saving ? 'Saving...' : 'Save Configuration'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="{{ url_for('static', path='admin/js/module-config.js') }}"></script>
|
|
{% endblock %}
|