diff --git a/includes/Dashboard/AI_Capabilities_Widget.php b/includes/Dashboard/AI_Capabilities_Widget.php new file mode 100644 index 00000000..f818f261 --- /dev/null +++ b/includes/Dashboard/AI_Capabilities_Widget.php @@ -0,0 +1,211 @@ +registry = $registry; + } + + /** + * Renders the widget content. + * + * Shows Ability statistics and per-provider capabilities. + * + * @since x.x.x + */ + public function render(): void { + ?> +
+ render_abilities_summary(); ?> + render_provider_capabilities(); ?> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + registry->get_feature( 'abilities-explorer' ); + if ( $feature && $feature->is_enabled() ) : + ?> + + + getRegisteredProviderIds(); + + if ( empty( $provider_ids ) ) { + return; + } + + ?> +

+ +

+
+ getProviderClassName( $provider_id ); + + /** @var \WordPress\AiClient\Providers\Contracts\ProviderInterface $provider_class */ + $metadata = $provider_class::metadata(); + $model_dir = $provider_class::modelMetadataDirectory(); + $models = $model_dir->listModelMetadata(); + $capabilities = array(); + + foreach ( $models as $model ) { + foreach ( $model->getSupportedCapabilities() as $capability ) { + $capabilities[ $capability->value ] = true; + } + } + + if ( empty( $capabilities ) ) { + continue; + } + + ?> +
+ + getName() ); ?> + + + $unused ) : ?> + + get_capability_label( (string) $cap_value ) ); ?> + + + +
+ +
+ __( 'Text Generation', 'ai' ), + CapabilityEnum::IMAGE_GENERATION => __( 'Image Generation', 'ai' ), + CapabilityEnum::TEXT_TO_SPEECH_CONVERSION => __( 'Text to Speech', 'ai' ), + CapabilityEnum::SPEECH_GENERATION => __( 'Speech Generation', 'ai' ), + CapabilityEnum::MUSIC_GENERATION => __( 'Music Generation', 'ai' ), + CapabilityEnum::VIDEO_GENERATION => __( 'Video Generation', 'ai' ), + CapabilityEnum::EMBEDDING_GENERATION => __( 'Embedding Generation', 'ai' ), + CapabilityEnum::CHAT_HISTORY => __( 'Chat History', 'ai' ), + ); + + return $labels[ $capability ] ?? $capability; + } +} diff --git a/includes/Dashboard/AI_Status_Widget.php b/includes/Dashboard/AI_Status_Widget.php new file mode 100644 index 00000000..68bceb93 --- /dev/null +++ b/includes/Dashboard/AI_Status_Widget.php @@ -0,0 +1,232 @@ +registry = $registry; + } + + /** + * Renders the widget content. + * + * Determines whether to show the getting-started checklist or + * the full status view based on setup completion. + * + * @since x.x.x + */ + public function render(): void { + $has_credentials = has_ai_credentials(); + $global_enabled = (bool) get_option( Settings_Registration::GLOBAL_OPTION, false ); + $any_feature_on = $this->has_any_enabled_feature(); + + if ( $has_credentials && $global_enabled && $any_feature_on ) { + $this->render_status(); + } else { + $this->render_getting_started( $has_credentials, $global_enabled, $any_feature_on ); + } + } + + /** + * Renders the getting-started checklist. + * + * @since x.x.x + * + * @param bool $has_credentials Whether any AI provider credentials are configured. + * @param bool $global_enabled Whether the global features toggle is on. + * @param bool $any_feature_on Whether at least one feature is enabled. + */ + private function render_getting_started( bool $has_credentials, bool $global_enabled, bool $any_feature_on ): void { + $steps = array( + array( + 'done' => $has_credentials, + 'label' => __( 'Configure an AI provider', 'ai' ), + 'url' => admin_url( 'options-connectors.php' ), + ), + array( + 'done' => $global_enabled, + 'label' => __( 'Globally enable AI Features', 'ai' ), + 'url' => admin_url( 'options-general.php?page=ai' ), + ), + array( + 'done' => $any_feature_on, + 'label' => __( 'Enable an individual feature', 'ai' ), + 'url' => admin_url( 'options-general.php?page=ai' ), + ), + array( + 'done' => false, + 'label' => __( 'Try it out', 'ai' ), + 'url' => admin_url( 'post-new.php' ), + ), + ); + ?> + +
+

+ +

+
    + +
  1. + + + + +
  2. + +
+
+ + get_ai_connectors(); + $features = $this->registry->get_all_features(); + ?> + +
+
+
+

+
    + +
  • + + + + + + +
  • + +
+ + + +
+ +
+

+
    + +
  • + is_enabled() ) : ?> + + + + + get_label() ); ?> +
  • + +
+ + + +
+
+
+ + Connector info. + */ + private function get_ai_connectors(): array { + $connectors = array(); + + if ( ! function_exists( 'wp_get_connectors' ) ) { + return $connectors; + } + + foreach ( wp_get_connectors() as $slug => $connector_data ) { + if ( 'ai_provider' !== $connector_data['type'] ) { + continue; + } + + $auth = $connector_data['authentication']; + $configured = ( 'api_key' === $auth['method'] + && ! empty( $auth['setting_name'] ) + && '' !== get_option( $auth['setting_name'], '' ) ); + + $connectors[] = array( + 'name' => $connector_data['name'] ?? $slug, + 'configured' => $configured, + ); + } + + return $connectors; + } + + /** + * Checks whether any registered feature is individually enabled. + * + * @since x.x.x + * + * @return bool True if at least one feature is enabled. + */ + private function has_any_enabled_feature(): bool { + foreach ( $this->registry->get_all_features() as $feature ) { + if ( $feature->is_enabled() ) { + return true; + } + } + + return false; + } +} diff --git a/includes/Dashboard/Dashboard_Widgets.php b/includes/Dashboard/Dashboard_Widgets.php new file mode 100644 index 00000000..70bbc859 --- /dev/null +++ b/includes/Dashboard/Dashboard_Widgets.php @@ -0,0 +1,90 @@ +registry = $registry; + } + + /** + * Hooks into WordPress to register dashboard widgets. + * + * @since x.x.x + */ + public function init(): void { + add_action( 'wp_dashboard_setup', array( $this, 'register_widgets' ) ); + } + + /** + * Registers the dashboard widgets and enqueues styles. + * + * Only registers widgets for users with the `manage_options` capability. + * + * @since x.x.x + */ + public function register_widgets(): void { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $status_widget = new AI_Status_Widget( $this->registry ); + + wp_add_dashboard_widget( + 'wpai_status', + __( 'AI Status', 'ai' ), + array( $status_widget, 'render' ) + ); + + $capabilities_widget = new AI_Capabilities_Widget( $this->registry ); + + wp_add_dashboard_widget( + 'wpai_capabilities', + __( 'AI Capabilities', 'ai' ), + array( $capabilities_widget, 'render' ) + ); + + Asset_Loader::enqueue_style( 'dashboard-widgets', 'admin/dashboard' ); + } +} diff --git a/includes/bootstrap.php b/includes/bootstrap.php index fc5ff406..0718bac2 100644 --- a/includes/bootstrap.php +++ b/includes/bootstrap.php @@ -208,10 +208,13 @@ function initialize_features(): void { $settings_registration = new Settings_Registration( $registry ); $settings_registration->init(); - // Initialize admin settings page. + // Initialize admin settings page and dashboard widgets. if ( is_admin() ) { $settings_page = new Settings_Page( $registry ); $settings_page->init(); + + $dashboard_widgets = new Dashboard\Dashboard_Widgets( $registry ); + $dashboard_widgets->init(); } // Register our post-related WordPress Abilities. diff --git a/src/admin/dashboard/index.scss b/src/admin/dashboard/index.scss new file mode 100644 index 00000000..daae5d92 --- /dev/null +++ b/src/admin/dashboard/index.scss @@ -0,0 +1,173 @@ +/** + * AI Dashboard Widget Styles + * + * Uses BEM (Block Element Modifier) naming convention. + * + * @package WordPress\AI + * @since x.x.x + */ + +/* Block: ai-dashboard-status */ +.ai-dashboard-status { + + &__checklist { + margin: 0; + list-style: none; + } + + &__step { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + + a { + text-decoration: none; + + &:hover { + text-decoration: underline !important; + } + } + } + + /* Two-column layout for status mode */ + &__columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + } + + &__column { + display: flex; + flex-direction: column; + } + + &__section-title { + font-weight: 600 !important; + letter-spacing: 0.05em; + } + + &__list { + margin: 0; + list-style: none; + } + + &__list-item { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.1875rem 0; + + .dashicons { + font-size: 1rem; + width: 1rem; + height: 1rem; + } + } + + &__column-link { + display: inline-block; + margin-top: 0.5rem; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + /* Icon modifiers */ + &__icon--success { + color: #46b450; + } + + &__icon--error { + color: #dc3232; + } + + &__icon--neutral { + color: #a7aaad; + } +} + +/* Block: ai-dashboard-capabilities */ +.ai-dashboard-capabilities { + + &__stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.5rem; + margin-bottom: 1rem; + } + + &__stat-card { + display: flex; + flex-direction: column; + padding: 0.5rem 0.25rem; + border: 1px solid #dcdcde; + border-radius: 0.25rem; + text-align: center; + } + + &__stat-value { + font-size: 1.5rem; + font-weight: 600; + line-height: 1.2; + color: #3858e9; + } + + &__stat-label { + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + margin-top: 0.125rem; + } + + &__section-title { + font-weight: 600 !important; + letter-spacing: 0.05em; + } + + &__providers { + display: flex; + flex-direction: column; + gap: 1rem; + } + + &__provider { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + &__provider-name { + font-weight: 600; + } + + &__provider-caps { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + &__cap-tag { + display: inline-block; + padding: 0.125rem 0.5rem; + font-size: 0.6875rem; + background: #f0f0f1; + border-radius: 1rem; + } + + &__links { + padding-bottom: 0.75rem; + border-bottom: 1px solid #f0f0f0; + + a { + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } +} diff --git a/tests/Integration/Includes/Dashboard/AI_Capabilities_WidgetTest.php b/tests/Integration/Includes/Dashboard/AI_Capabilities_WidgetTest.php new file mode 100644 index 00000000..5bab093f --- /dev/null +++ b/tests/Integration/Includes/Dashboard/AI_Capabilities_WidgetTest.php @@ -0,0 +1,233 @@ + 'Abilities Explorer', + 'description' => 'Test abilities explorer', + ); + } + + /** + * {@inheritDoc} + */ + public function register(): void {} +} + +/** + * AI_Capabilities_Widget test case. + * + * @since x.x.x + */ +class AI_Capabilities_WidgetTest extends WP_UnitTestCase { + + /** + * Tear down after each test. + * + * @since x.x.x + */ + public function tearDown(): void { + delete_option( 'wpai_features_enabled' ); + delete_option( 'wpai_feature_abilities-explorer_enabled' ); + parent::tearDown(); + } + + /** + * Tests that the widget renders the wrapper div. + * + * @since x.x.x + */ + public function test_render_outputs_wrapper() { + $registry = new Registry(); + $widget = new AI_Capabilities_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'ai-dashboard-capabilities', + $output, + 'Should render the capabilities wrapper div' + ); + } + + /** + * Tests that abilities summary renders stat cards when abilities API is available. + * + * @since x.x.x + */ + public function test_render_abilities_summary_stat_cards() { + if ( ! function_exists( 'wp_get_abilities' ) ) { + $this->markTestSkipped( 'WordPress Abilities API not available.' ); + } + + $registry = new Registry(); + $widget = new AI_Capabilities_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'ai-dashboard-capabilities__stat-card', + $output, + 'Should render stat cards' + ); + $this->assertStringContainsString( 'Total Abilities', $output ); + $this->assertStringContainsString( 'Core', $output ); + $this->assertStringContainsString( 'Plugins', $output ); + $this->assertStringContainsString( 'Theme', $output ); + } + + /** + * Tests that Abilities Explorer link shows when feature is enabled. + * + * @since x.x.x + */ + public function test_abilities_explorer_link_shown_when_enabled() { + if ( ! function_exists( 'wp_get_abilities' ) ) { + $this->markTestSkipped( 'WordPress Abilities API not available.' ); + } + + update_option( 'wpai_features_enabled', true ); + update_option( 'wpai_feature_abilities-explorer_enabled', true ); + + $registry = new Registry(); + $registry->register_feature( new Capabilities_Test_Feature() ); + + $widget = new AI_Capabilities_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'ai-abilities-explorer', + $output, + 'Should link to Abilities Explorer' + ); + } + + /** + * Tests that Abilities Explorer link is hidden when feature is disabled. + * + * @since x.x.x + */ + public function test_abilities_explorer_link_hidden_when_disabled() { + if ( ! function_exists( 'wp_get_abilities' ) ) { + $this->markTestSkipped( 'WordPress Abilities API not available.' ); + } + + $registry = new Registry(); + $registry->register_feature( new Capabilities_Test_Feature() ); + + $widget = new AI_Capabilities_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringNotContainsString( + 'ai-abilities-explorer', + $output, + 'Should not link to Abilities Explorer when disabled' + ); + } + + /** + * Tests that provider capabilities section renders when AiClient is available. + * + * @since x.x.x + */ + public function test_render_provider_capabilities() { + if ( ! class_exists( \WordPress\AiClient\AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $registry = \WordPress\AiClient\AiClient::defaultRegistry(); + $provider_ids = $registry->getRegisteredProviderIds(); + + if ( empty( $provider_ids ) ) { + $this->markTestSkipped( 'No AI providers registered.' ); + } + + $exp_registry = new Registry(); + $widget = new AI_Capabilities_Widget( $exp_registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'Provider Capabilities', + $output, + 'Should render the Provider Capabilities heading' + ); + $this->assertStringContainsString( + 'ai-dashboard-capabilities__cap-tag', + $output, + 'Should render capability tags' + ); + } + + /** + * Tests that capability labels are human-readable. + * + * @since x.x.x + */ + public function test_capability_labels_are_human_readable() { + if ( ! class_exists( \WordPress\AiClient\AiClient::class ) ) { + $this->markTestSkipped( 'AiClient not available.' ); + } + + $registry = \WordPress\AiClient\AiClient::defaultRegistry(); + $provider_ids = $registry->getRegisteredProviderIds(); + + if ( empty( $provider_ids ) ) { + $this->markTestSkipped( 'No AI providers registered.' ); + } + + $registry = new Registry(); + $widget = new AI_Capabilities_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + // Should contain human-readable labels, not raw enum values. + $this->assertStringNotContainsString( + 'text_generation', + $output, + 'Should not show raw enum values' + ); + } +} diff --git a/tests/Integration/Includes/Dashboard/AI_Status_WidgetTest.php b/tests/Integration/Includes/Dashboard/AI_Status_WidgetTest.php new file mode 100644 index 00000000..d1d71d9f --- /dev/null +++ b/tests/Integration/Includes/Dashboard/AI_Status_WidgetTest.php @@ -0,0 +1,269 @@ + 'First Feature', + 'description' => 'A test feature', + ); + } + + /** + * {@inheritDoc} + */ + public function register(): void {} +} + +/** + * Stub feature B for status widget tests. + * + * @since x.x.x + */ +class Status_Test_Feature_B extends Abstract_Feature { + + /** + * {@inheritDoc} + */ + public static function get_id(): string { + return 'test-feature-b'; + } + + /** + * {@inheritDoc} + */ + protected function load_metadata(): array { + return array( + 'label' => 'Second Feature', + 'description' => 'Another test feature', + ); + } + + /** + * {@inheritDoc} + */ + public function register(): void {} +} + +/** + * AI_Status_Widget test case. + * + * @since x.x.x + */ +class AI_Status_WidgetTest extends WP_UnitTestCase { + + /** + * Tear down after each test. + * + * @since x.x.x + */ + public function tearDown(): void { + delete_option( Settings_Registration::GLOBAL_OPTION ); + delete_option( 'wpai_feature_test-feature-a_enabled' ); + delete_option( 'wpai_feature_test-feature-b_enabled' ); + parent::tearDown(); + } + + /** + * Tests that getting-started mode is rendered when setup is incomplete. + * + * Without any credentials or enabled features, the widget should + * always show the getting-started checklist. + * + * @since x.x.x + */ + public function test_render_getting_started_mode() { + $registry = new Registry(); + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'ai-dashboard-status__checklist', + $output, + 'Should render the getting-started checklist' + ); + } + + /** + * Tests that getting-started mode shows all four checklist steps. + * + * @since x.x.x + */ + public function test_getting_started_has_all_steps() { + $registry = new Registry(); + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Configure an AI provider', $output ); + $this->assertStringContainsString( 'Globally enable AI Features', $output ); + $this->assertStringContainsString( 'Enable an individual feature', $output ); + $this->assertStringContainsString( 'Try it out', $output ); + } + + /** + * Tests that incomplete steps show error icons. + * + * @since x.x.x + */ + public function test_getting_started_shows_error_icons_for_incomplete_steps() { + $registry = new Registry(); + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'dashicons-dismiss', + $output, + 'Should show error icons for incomplete steps' + ); + } + + /** + * Tests that the global enabled step shows a success icon when enabled. + * + * @since x.x.x + */ + public function test_getting_started_shows_success_for_global_enabled() { + update_option( Settings_Registration::GLOBAL_OPTION, true ); + + $registry = new Registry(); + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'dashicons-yes-alt', + $output, + 'Should show success icon for the enabled global toggle step' + ); + } + + /** + * Tests that enabling an feature shows its step as complete. + * + * @since x.x.x + */ + public function test_getting_started_shows_success_for_enabled_feature() { + update_option( Settings_Registration::GLOBAL_OPTION, true ); + update_option( 'wpai_feature_test-feature-a_enabled', true ); + + $registry = new Registry(); + $registry->register_feature( new Status_Test_Feature_A() ); + + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + // Still in getting-started mode (no credentials), but feature step is green. + $this->assertStringContainsString( 'ai-dashboard-status__checklist', $output ); + + // Count success icons — global enabled + feature enabled = at least 2. + $success_count = substr_count( $output, 'dashicons-yes-alt' ); + $this->assertGreaterThanOrEqual( + 2, + $success_count, + 'Should have at least 2 success icons (global + feature enabled)' + ); + } + + /** + * Tests that the checklist links to the correct admin pages. + * + * @since x.x.x + */ + public function test_getting_started_links_to_admin_pages() { + $registry = new Registry(); + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'options-connectors.php', $output, 'Should link to connectors page' ); + $this->assertStringContainsString( 'page=ai', $output, 'Should link to features settings' ); + $this->assertStringContainsString( 'post-new.php', $output, 'Should link to new post page' ); + } + + /** + * Tests that the widget renders without errors when the registry has features. + * + * @since x.x.x + */ + public function test_render_with_multiple_features_in_registry() { + $registry = new Registry(); + $registry->register_feature( new Status_Test_Feature_A() ); + $registry->register_feature( new Status_Test_Feature_B() ); + + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'ai-dashboard-status', + $output, + 'Should render without errors with multiple features' + ); + } + + /** + * Tests that the widget renders without errors when the registry is empty. + * + * @since x.x.x + */ + public function test_render_with_empty_registry() { + $registry = new Registry(); + $widget = new AI_Status_Widget( $registry ); + + ob_start(); + $widget->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( + 'ai-dashboard-status', + $output, + 'Should render cleanly with empty registry' + ); + } +} diff --git a/tests/Integration/Includes/Dashboard/Dashboard_WidgetsTest.php b/tests/Integration/Includes/Dashboard/Dashboard_WidgetsTest.php new file mode 100644 index 00000000..f12bc0c7 --- /dev/null +++ b/tests/Integration/Includes/Dashboard/Dashboard_WidgetsTest.php @@ -0,0 +1,111 @@ +init(); + + $this->assertIsInt( + has_action( 'wp_dashboard_setup', array( $widgets, 'register_widgets' ) ), + 'register_widgets should be hooked to wp_dashboard_setup' + ); + } + + /** + * Tests that register_widgets requires manage_options capability. + * + * @since x.x.x + */ + public function test_register_widgets_requires_manage_options() { + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + } + + global $wp_meta_boxes; + + $subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber' ) ); + wp_set_current_user( $subscriber_id ); + + $registry = new Registry(); + $widgets = new Dashboard_Widgets( $registry ); + $widgets->register_widgets(); + + $status_registered = isset( $wp_meta_boxes['dashboard']['normal']['core']['wpai_status'] ); + + $this->assertFalse( + $status_registered, + 'Widgets should not be registered for subscribers' + ); + + // Clean up. + unset( $wp_meta_boxes['dashboard'] ); + } + + /** + * Tests that register_widgets registers both widgets for admin users. + * + * @since x.x.x + */ + public function test_register_widgets_for_admin() { + if ( ! function_exists( 'wp_add_dashboard_widget' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard.php'; + } + + global $wp_meta_boxes; + + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + // Set the current screen to the dashboard so wp_add_dashboard_widget works. + set_current_screen( 'dashboard' ); + + $registry = new Registry(); + $widgets = new Dashboard_Widgets( $registry ); + $widgets->register_widgets(); + + // wp_add_dashboard_widget may place widgets in different priority levels. + $all_widgets = array(); + foreach ( $wp_meta_boxes['dashboard'] ?? array() as $context ) { + foreach ( $context as $priority_widgets ) { + $all_widgets = array_merge( $all_widgets, array_keys( $priority_widgets ) ); + } + } + + $this->assertContains( + 'wpai_status', + $all_widgets, + 'AI Status widget should be registered' + ); + $this->assertContains( + 'wpai_capabilities', + $all_widgets, + 'AI Capabilities widget should be registered' + ); + + // Clean up. + unset( $wp_meta_boxes['dashboard'] ); + } +} diff --git a/webpack.config.js b/webpack.config.js index bb929257..24d2026e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,6 +19,11 @@ module.exports = { 'src/admin/settings', 'index.scss' ), + 'admin/dashboard': path.resolve( + process.cwd(), + 'src/admin/dashboard', + 'index.scss' + ), 'experiments/abilities-explorer': path.resolve( process.cwd(), 'src/experiments/abilities-explorer',