From a2701f5071d35a4e155f64f19a47f74876abe809 Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Thu, 17 Oct 2024 13:01:00 +0600 Subject: [PATCH 01/16] feat: React Skeleton --- base-tailwind.config.js | 66 +++++++++++++++++++++++++++++ includes/Assets.php | 12 ++++++ package.json | 12 +++++- src/Dashboard/index.tsx | 36 ++++++++++++++++ src/Dashboard/tailwind.scss | 2 + src/Layout/404.tsx | 9 ++++ src/Layout/ContentArea.tsx | 17 ++++++++ src/Layout/Footer.tsx | 7 ++++ src/Layout/Header.tsx | 20 +++++++++ src/Layout/Sidebar.tsx | 5 +++ src/Layout/index.tsx | 74 ++++++++++++++++++++++++++++++++ src/Routing/index.tsx | 84 +++++++++++++++++++++++++++++++++++++ src/base-tailwind.scss | 21 ++++++++++ webpack.config.js | 2 + 14 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 base-tailwind.config.js create mode 100644 src/Dashboard/index.tsx create mode 100644 src/Dashboard/tailwind.scss create mode 100644 src/Layout/404.tsx create mode 100644 src/Layout/ContentArea.tsx create mode 100644 src/Layout/Footer.tsx create mode 100644 src/Layout/Header.tsx create mode 100644 src/Layout/Sidebar.tsx create mode 100644 src/Layout/index.tsx create mode 100644 src/Routing/index.tsx create mode 100644 src/base-tailwind.scss diff --git a/base-tailwind.config.js b/base-tailwind.config.js new file mode 100644 index 0000000000..2047d53595 --- /dev/null +++ b/base-tailwind.config.js @@ -0,0 +1,66 @@ +import { + scopedPreflightStyles, + isolateInsideOfContainer, +} from 'tailwindcss-scoped-preflight'; + +const rootClass = '.dokan-layout'; //We will use this class to scope the styles. + +/** @type {import('tailwindcss').Config} */ +const baseConfig = { + important: rootClass, + content: [ './src/**/*.{js,jsx,ts,tsx}', '!./**/*.asset.php' ], + theme: { + extend: { + backgroundColor: { + dokan: { + sidebar: { + DEFAULT: + 'var(--dokan-sidebar-background-color, #F05025)', + hover: 'var(--dokan-sidebar-hover-background-color, #F05025)', + }, + btn: { + DEFAULT: + 'var(--dokan-button-background-color, #F05025)', + hover: 'var(--dokan-button-hover-background-color, #F05025)', + }, + }, + }, + textColor: { + dokan: { + sidebar: { + DEFAULT: 'var(--dokan-sidebar-text-color, #CFCFCF)', + hover: 'var(--dokan-sidebar-hover-text-color, #ffffff)', + }, + btn: { + DEFAULT: 'var(--dokan-button-text-color, #ffffff)', + hover: 'var(--dokan-button-hover-text-color, #ffffff)', + }, + }, + }, + borderColor: { + dokan: { + btn: { + DEFAULT: 'var(--dokan-button-border-color, #F05025)', + hover: 'var(--dokan-button-hover-border-color, #F05025)', + }, + }, + }, + colors: { + primary: 'var(--dokan-button-background-color, #F05025)', + dokan: { + sidebar: 'var(--dokan-button-background-color, #1B233B)', + btn: 'var(--dokan-button-background-color, #F05025)', + }, + }, + }, + }, + plugins: [ + scopedPreflightStyles( { + isolationStrategy: isolateInsideOfContainer( rootClass, {} ), + } ), + require( '@tailwindcss/typography' ), + require( '@tailwindcss/forms' ), + ], +}; + +module.exports = baseConfig; diff --git a/includes/Assets.php b/includes/Assets.php index 913cf2872d..b83b20dda1 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -352,6 +352,10 @@ public function get_styles() { 'src' => DOKAN_PLUGIN_ASSEST . '/css/dokan-tailwind.css', 'version' => filemtime( DOKAN_DIR . '/assets/css/dokan-tailwind.css' ), ], + 'dokan-react-frontend' => [ + 'src' => DOKAN_PLUGIN_ASSEST . '/css/frontend.css', + 'version' => filemtime( DOKAN_DIR . '/assets/css/frontend.css' ), + ], ]; return $styles; @@ -365,6 +369,7 @@ public function get_styles() { public function get_scripts() { global $wp_version; + $frontend_shipping_asset = require DOKAN_DIR . '/assets/js/frontend.asset.php'; $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; $asset_url = DOKAN_PLUGIN_ASSEST; $asset_path = DOKAN_DIR . '/assets/'; @@ -554,6 +559,11 @@ public function get_scripts() { 'deps' => [ 'jquery' ], 'version' => filemtime( $asset_path . 'js/dokan-frontend.js' ), ], + 'dokan-react-frontend' => [ + 'src' => $asset_url . '/js/frontend.js', + 'deps' => $frontend_shipping_asset['dependencies'], + 'version' => $frontend_shipping_asset['version'], + ], ]; return $scripts; @@ -856,6 +866,8 @@ public function dokan_dashboard_scripts() { self::load_form_validate_script(); $this->load_gmap_script(); + wp_enqueue_script( 'dokan-react-frontend' ); + wp_enqueue_style( 'dokan-react-frontend' ); wp_enqueue_script( 'jquery' ); wp_enqueue_script( 'jquery-ui' ); wp_enqueue_script( 'jquery-ui-autocomplete' ); diff --git a/package.json b/package.json index 686dad4c93..ceeae0228c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "release:dev": "npm install && npm run build && npm run clean-files && npm run makepot && npm run zip" }, "devDependencies": { + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", "@wordpress/scripts": "^27.9.0", "chartjs-adapter-moment": "^1.0.1", "debounce": "^1.2.1", @@ -34,6 +36,7 @@ "papaparse": "^5.4.1", "replace-in-file": "^6.3.5", "tailwindcss": "^3.3.3", + "tailwindcss-scoped-preflight": "^3.4.5", "vue": "^2.7.14", "vue-chartjs": "^3.5.1", "vue-color": "^2.8.1", @@ -50,6 +53,13 @@ "wp-readme-to-markdown": "^1.0.1" }, "dependencies": { - "@wordpress/i18n": "^5.8.0" + "@wordpress/components": "^28.9.0", + "@wordpress/data": "^10.9.0", + "@wordpress/dom-ready": "^4.9.0", + "@wordpress/element": "^6.9.0", + "@wordpress/hooks": "^4.9.0", + "@wordpress/i18n": "^5.8.0", + "@wordpress/plugins": "^7.10.0", + "react-router-dom": "^6.27.0" } } diff --git a/src/Dashboard/index.tsx b/src/Dashboard/index.tsx new file mode 100644 index 0000000000..7050cc979c --- /dev/null +++ b/src/Dashboard/index.tsx @@ -0,0 +1,36 @@ +import {createRoot} from "@wordpress/element"; +import domReady from "@wordpress/dom-ready"; +import Layout from "../Layout"; +import getRoutes from "../Routing"; +import { + createBrowserRouter, createHashRouter, + RouterProvider, +} from "react-router-dom"; +import './tailwind.scss'; + +const App = () => { + const routes = getRoutes(); + + const mapedRoutes = routes.map((route) => { + + + // TODO add Parent route support. + + return { + path: route.path, + element: {route.element}, + } + }); + + const router = createHashRouter(mapedRoutes); + + return ; +} + +domReady( function () { + const rootElement = document.querySelector( '.dashboard-content-area' ); + const root = createRoot( rootElement! ); + root.render( + + ); +} ); diff --git a/src/Dashboard/tailwind.scss b/src/Dashboard/tailwind.scss new file mode 100644 index 0000000000..0a10673143 --- /dev/null +++ b/src/Dashboard/tailwind.scss @@ -0,0 +1,2 @@ +@config './../../base-tailwind.config.js'; +@import '../base-tailwind'; diff --git a/src/Layout/404.tsx b/src/Layout/404.tsx new file mode 100644 index 0000000000..53a2e17550 --- /dev/null +++ b/src/Layout/404.tsx @@ -0,0 +1,9 @@ +const NotFound = () => { + return ( +
+

404 - Not Found!

+
+ ); +} + +export default NotFound; diff --git a/src/Layout/ContentArea.tsx b/src/Layout/ContentArea.tsx new file mode 100644 index 0000000000..21c178bf61 --- /dev/null +++ b/src/Layout/ContentArea.tsx @@ -0,0 +1,17 @@ +import Sidebar from './Sidebar'; +import {Slot} from "@wordpress/components"; + +const ContentArea = ( { children } ) => { + return ( + <> + +
+ + { children } + +
+ + ); +}; + +export default ContentArea; diff --git a/src/Layout/Footer.tsx b/src/Layout/Footer.tsx new file mode 100644 index 0000000000..0ba155f3ef --- /dev/null +++ b/src/Layout/Footer.tsx @@ -0,0 +1,7 @@ +import {Slot} from "@wordpress/components"; + +const Footer = () => { + return <>; +}; + +export default Footer; diff --git a/src/Layout/Header.tsx b/src/Layout/Header.tsx new file mode 100644 index 0000000000..82e2ce4b3c --- /dev/null +++ b/src/Layout/Header.tsx @@ -0,0 +1,20 @@ +import {Slot} from "@wordpress/components"; + +const Header = ( { title = '' } ) => { + + return ( + <> + +
+

{title}

+
+
+ +
+ + + ); +}; + +export default Header; diff --git a/src/Layout/Sidebar.tsx b/src/Layout/Sidebar.tsx new file mode 100644 index 0000000000..cf785e0ec7 --- /dev/null +++ b/src/Layout/Sidebar.tsx @@ -0,0 +1,5 @@ +const Sidebar = () => { + return <>; +}; + +export default Sidebar; diff --git a/src/Layout/index.tsx b/src/Layout/index.tsx new file mode 100644 index 0000000000..5988428e3a --- /dev/null +++ b/src/Layout/index.tsx @@ -0,0 +1,74 @@ +import { createContext, useContext, useState } from '@wordpress/element'; +import Header from './Header'; +import Footer from './Footer'; +import ContentArea from './ContentArea'; +import { + SlotFillProvider +} from '@wordpress/components'; +import { PluginArea } from '@wordpress/plugins'; + +// Create a ThemeContext +const ThemeContext = createContext( null ); + +// Create a ThemeProvider component +const ThemeProvider = ( { children } ) => { + const [ theme, setTheme ] = useState( 'light' ); // Example theme state + + return ( + + { children } + + ); +}; + +export type DokanRoute = { + id: string; + title?: string; + icon?: JSX.Element | React.ReactNode; + element: JSX.Element | React.ReactNode; + path: string; + exact?: boolean; + order?: number; + parent?: string; +}; + +interface LayoutProps { + children: React.ReactNode; + route: DokanRoute; + title?: string; + headerComponent?: JSX.Element; + footerComponent?: JSX.Element; +} + +// Create a Layout component that uses the ThemeProvider +const Layout = ( { + children, + route, + title = '', + headerComponent, + footerComponent, +}: LayoutProps ) => { + return ( + + +
+ { headerComponent ? ( + headerComponent + ) : ( +
+ ) } + { children } + { footerComponent ? footerComponent :
} +
+ +
+
+ ); +}; + +// Custom hook to use the ThemeContext +export const useTheme = () => { + return useContext( ThemeContext ); +}; + +export default Layout; diff --git a/src/Routing/index.tsx b/src/Routing/index.tsx new file mode 100644 index 0000000000..27f128544f --- /dev/null +++ b/src/Routing/index.tsx @@ -0,0 +1,84 @@ +import NotFound from "../Layout/404"; +import {__} from "@wordpress/i18n"; +import {DokanRoute} from "../Layout"; + +const getRoutes = () => { + let routes : Array = []; + + // routes.push( + // { + // id: 'dokan-seller-settings', + // title: __( 'Settings', 'dokan-lite' ), + // icon: , + // element: + // path: '/settings', + // exact: true, + // order: 1, + // parent: 'dokan-dashboard', + // } + // ); + + routes.push( + { + id: 'dokan-kola', + title: __( 'Kola bagan', 'dokan-lite' ), + element:

asdasd

, + path: '/kola', + exact: true, + order: 10, + } + ); + routes.push( + { + id: 'dokan-base', + title: __( 'Dashboard', 'dokan-lite' ), + element:

Dashboard body

, + path: '/', + exact: true, + order: 10, + } + ); + routes.push( + { + id: 'dokan-settings', + title: __( 'Settings', 'dokan-lite' ), + element:

Settings body

, + path: 'settings', + exact: true, + order: 10, + } + ); + + routes.push( + { + id: 'dokan-store-settings', + title: __( 'Store Settings', 'dokan-lite' ), + element:

Store Settings body

, + path: 'store', + exact: true, + order: 10, + parent: 'settings', + } + ); + + + + + + + // @ts-ignore + routes = wp.hooks.applyFilters('dokan-frontend-routes', routes) as Array; + routes.push( + { + id: 'dokan-404', + element: , + path: '*', + } + ); + + console.log(routes); + + return routes; +} + +export default getRoutes; diff --git a/src/base-tailwind.scss b/src/base-tailwind.scss new file mode 100644 index 0000000000..4cb24cb3cc --- /dev/null +++ b/src/base-tailwind.scss @@ -0,0 +1,21 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +// Reset unwanted table styles but keep structure intact +@layer base { + .dokan-layout { + table, th, td { + margin: 0; + padding: 0; + border: 0; + border-spacing: 0; + border-collapse: collapse; + font-size: inherit; + font-weight: inherit; + text-align: inherit; + vertical-align: inherit; + box-sizing: border-box; + } + } +} diff --git a/webpack.config.js b/webpack.config.js index d0c8433caa..7227dea5d2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ const entryPoint = { // Dokan tailwind css 'dokan-tailwind': './src/tailwind.css', + 'frontend': './src/Dashboard/index.tsx', 'vue-frontend': './src/frontend/main.js', 'vue-admin': './src/admin/main.js', 'vue-bootstrap': './src/utils/Bootstrap.js', @@ -66,6 +67,7 @@ const updatedConfig = { }, resolve: { + ...defaultConfig.resolve, alias: { 'vue$': 'vue/dist/vue.esm.js', '@': path.resolve('./src/'), From e931d06c29083947f7bbfd1f369284baf717da68 Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Fri, 18 Oct 2024 09:37:52 +0600 Subject: [PATCH 02/16] update: header footer customization support added --- src/Dashboard/index.tsx | 6 +---- src/Layout/Header.tsx | 3 +-- src/Layout/index.tsx | 6 +++-- src/Routing/index.tsx | 56 ++--------------------------------------- 4 files changed, 8 insertions(+), 63 deletions(-) diff --git a/src/Dashboard/index.tsx b/src/Dashboard/index.tsx index 7050cc979c..c3f6134365 100644 --- a/src/Dashboard/index.tsx +++ b/src/Dashboard/index.tsx @@ -12,13 +12,9 @@ const App = () => { const routes = getRoutes(); const mapedRoutes = routes.map((route) => { - - - // TODO add Parent route support. - return { path: route.path, - element: {route.element}, + element: {route.element}, } }); diff --git a/src/Layout/Header.tsx b/src/Layout/Header.tsx index 82e2ce4b3c..53370df240 100644 --- a/src/Layout/Header.tsx +++ b/src/Layout/Header.tsx @@ -6,8 +6,7 @@ const Header = ( { title = '' } ) => { <>
-

{title}

+

{title}

diff --git a/src/Layout/index.tsx b/src/Layout/index.tsx index 5988428e3a..f71a2f330e 100644 --- a/src/Layout/index.tsx +++ b/src/Layout/index.tsx @@ -26,6 +26,8 @@ export type DokanRoute = { title?: string; icon?: JSX.Element | React.ReactNode; element: JSX.Element | React.ReactNode; + header?: JSX.Element | React.ReactNode; + footer?: JSX.Element | React.ReactNode; path: string; exact?: boolean; order?: number; @@ -36,8 +38,8 @@ interface LayoutProps { children: React.ReactNode; route: DokanRoute; title?: string; - headerComponent?: JSX.Element; - footerComponent?: JSX.Element; + headerComponent?: JSX.Element|React.ReactNode; + footerComponent?: JSX.Element|React.ReactNode; } // Create a Layout component that uses the ThemeProvider diff --git a/src/Routing/index.tsx b/src/Routing/index.tsx index 27f128544f..f1339d4234 100644 --- a/src/Routing/index.tsx +++ b/src/Routing/index.tsx @@ -5,29 +5,6 @@ import {DokanRoute} from "../Layout"; const getRoutes = () => { let routes : Array = []; - // routes.push( - // { - // id: 'dokan-seller-settings', - // title: __( 'Settings', 'dokan-lite' ), - // icon: , - // element: - // path: '/settings', - // exact: true, - // order: 1, - // parent: 'dokan-dashboard', - // } - // ); - - routes.push( - { - id: 'dokan-kola', - title: __( 'Kola bagan', 'dokan-lite' ), - element:

asdasd

, - path: '/kola', - exact: true, - order: 10, - } - ); routes.push( { id: 'dokan-base', @@ -38,36 +15,9 @@ const getRoutes = () => { order: 10, } ); - routes.push( - { - id: 'dokan-settings', - title: __( 'Settings', 'dokan-lite' ), - element:

Settings body

, - path: 'settings', - exact: true, - order: 10, - } - ); - - routes.push( - { - id: 'dokan-store-settings', - title: __( 'Store Settings', 'dokan-lite' ), - element:

Store Settings body

, - path: 'store', - exact: true, - order: 10, - parent: 'settings', - } - ); - - - - - // @ts-ignore - routes = wp.hooks.applyFilters('dokan-frontend-routes', routes) as Array; + routes = wp.hooks.applyFilters('dokan-dashboard-routes', routes) as Array; routes.push( { id: 'dokan-404', @@ -76,9 +26,7 @@ const getRoutes = () => { } ); - console.log(routes); - - return routes; + return routes; } export default getRoutes; From 057a92f2050c09280c5de699af5baf6b180d247f Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Fri, 18 Oct 2024 09:41:47 +0600 Subject: [PATCH 03/16] update: blank title markup support added --- src/Layout/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Layout/Header.tsx b/src/Layout/Header.tsx index 53370df240..5a10d1af56 100644 --- a/src/Layout/Header.tsx +++ b/src/Layout/Header.tsx @@ -6,7 +6,7 @@ const Header = ( { title = '' } ) => { <>
-

{title}

+ { title && (

{title}

)}
From 0077f7881a00ed06b7fefb0402fd642290ca0900 Mon Sep 17 00:00:00 2001 From: Al Amin Ahamed <34349365+mralaminahamed@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:47:27 +0600 Subject: [PATCH 04/16] Feat: Add REST API endpoint for vendor-specific customer search This commit introduces a new REST API endpoint for searching customers specific to a vendor's orders. Key changes include: - Add new route `/dokan/v1/customers/search` in CustomersController - Implement `search_customers` method with the following features: * Search customers by ID or term (name/email) * Limit results to customers who have ordered from the current vendor * Support excluding specific customer IDs * Limit results to 20 when search term is less than 3 characters * Apply `dokan_json_search_found_customers` filter for extensibility - Add permission check to ensure only users with 'edit_shop_orders' capability can access - Include detailed PHPDoc for the new filter This feature allows vendors to efficiently search their customer base through the REST API, mirroring the functionality of the existing AJAX action while providing a standardized API interface. --- includes/REST/CustomersController.php | 260 ++++++++++++++++++++++++++ includes/REST/Manager.php | 1 + 2 files changed, 261 insertions(+) create mode 100644 includes/REST/CustomersController.php diff --git a/includes/REST/CustomersController.php b/includes/REST/CustomersController.php new file mode 100644 index 0000000000..ca2442c655 --- /dev/null +++ b/includes/REST/CustomersController.php @@ -0,0 +1,260 @@ +namespace, '/' . $this->rest_base . '/search', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'search_customers' ), + 'permission_callback' => array( $this, 'search_customers_permissions_check' ), + 'args' => array( + 'search' => array( + 'description' => __( 'Search string.', 'dokan-lite' ), + 'type' => 'string', + 'required' => true, + ), + 'exclude' => array( + 'description' => __( 'Comma-separated list of customer IDs to exclude.', 'dokan-lite' ), + 'type' => 'string', + ), + ), + ), + ) ); + } + + /** + * Check if a given request has access to perform an action. + * + * @param WP_REST_Request $request Full details about the request. + * @param string $action The action to check (view, create, edit, delete). + * + * @return WP_Error|boolean + */ + protected function check_permission( $request, $action ) { + if ( ! $this->check_vendor_permission() ) { + $messages = [ + 'view' => __( 'Sorry, you cannot list resources.', 'dokan-lite' ), + 'create' => __( 'Sorry, you are not allowed to create resources.', 'dokan-lite' ), + 'edit' => __( 'Sorry, you are not allowed to edit this resource.', 'dokan-lite' ), + 'delete' => __( 'Sorry, you are not allowed to delete this resource.', 'dokan-lite' ), + ]; + return new WP_Error( "dokan_rest_cannot_$action", $messages[ $action ], [ 'status' => rest_authorization_required_code() ] ); + } + return true; + } + + /** + * Check if the current user has vendor permissions. + * + * @return bool + */ + public function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } + + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::get_items( $request ); + } + ); + } + + /** + * Get a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::get_item( $request ); + } + ); + } + + /** + * Search customers for the current vendor. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function search_customers( $request ) { + if ( ! current_user_can( 'edit_shop_orders' ) ) { + return new WP_Error( 'dokan_rest_cannot_search', __( 'You do not have permission to search customers.', 'dokan-lite' ), [ 'status' => rest_authorization_required_code() ] ); + } + + $term = $request->get_param( 'search' ); + $exclude = $request->get_param( 'exclude' ) ? explode( ',', $request->get_param( 'exclude' ) ) : []; + $limit = ''; + + if ( empty( $term ) ) { + return new WP_Error( 'dokan_rest_empty_search', __( 'Search term is required.', 'dokan-lite' ), [ 'status' => 400 ] ); + } + + $ids = []; + // Search by ID. + if ( is_numeric( $term ) ) { + $customer = new WC_Customer( intval( $term ) ); + + // Customer exists. + if ( 0 !== $customer->get_id() ) { + $ids = [ $customer->get_id() ]; + } + } + + // Usernames can be numeric so we first check that no users was found by ID before searching for numeric username, this prevents performance issues with ID lookups. + if ( empty( $ids ) ) { + $data_store = WC_Data_Store::load( 'customer' ); + + // If search is smaller than 3 characters, limit result set to avoid + // too many rows being returned. + if ( 3 > strlen( $term ) ) { + $limit = 20; + } + $ids = $data_store->search_customers( $term, $limit ); + } + + $found_customers = []; + + $ids = array_diff( $ids, $exclude ); + + foreach ( $ids as $id ) { + if ( ! dokan_customer_has_order_from_this_seller( $id ) ) { + continue; + } + + $customer = new WC_Customer( $id ); + $found_customers[ $id ] = [ + 'id' => $id, + 'name' => sprintf( + '%s', + $customer->get_first_name() . ' ' . $customer->get_last_name() + ), + 'email' => $customer->get_email(), + ]; + } + + /** + * Filter the found customers for Dokan REST API search. + * + * This filter allows you to modify the list of customers found during a search + * before it is returned by the REST API. + * + * @since DOKAN_SINCE + * + * @param array $found_customers An array of found customers. Each customer is an array containing: + * 'id' => (int) The customer's ID. + * 'name' => (string) The customer's full name. + * 'email' => (string) The customer's email address. + * @param string $term The search term used to find customers. + * @param array $exclude An array of customer IDs to exclude from the search results. + * @param int $limit The maximum number of results to return (if any). + * + * @return array The filtered array of found customers. + */ + $found_customers = apply_filters( 'dokan_json_search_found_customers', $found_customers, $term, $exclude, $limit ); + + return rest_ensure_response( array_values( $found_customers ) ); + } + + /** + * Prepare a single customer for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $customer = parent::prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $customer ) ) { + return $customer; + } + + if ( ! $customer instanceof WC_Customer ) { + return new WP_Error( 'dokan_rest_invalid_customer', __( 'Invalid customer.', 'dokan-lite' ), [ 'status' => 400 ] ); + } + + // Add any Dokan-specific customer preparation here + + return apply_filters( "dokan_rest_pre_insert_{$this->post_type}_object", $customer, $request, $creating ); + } + + /** + * Perform an action with vendor permission check. + * + * @param callable $action The action to perform. + * + * @return mixed The result of the action. + */ + private function perform_vendor_action( callable $action ) { + add_filter( 'woocommerce_rest_check_permissions', [ $this, 'check_vendor_permission' ] ); + $result = $action(); + remove_filter( 'woocommerce_rest_check_permissions', [ $this, 'check_vendor_permission' ] ); + return $result; + } + + /** + * Check if a given request has access to get items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + return $this->check_permission( $request, 'view' ); + } + + /** + * Check if a given request has access to get a specific item. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + return $this->check_permission( $request, 'view' ); + } + + /** + * Check if a given request has access to search customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function search_customers_permissions_check( $request ) { + return $this->check_permission( $request, 'view' ); + } +} diff --git a/includes/REST/Manager.php b/includes/REST/Manager.php index ce0d1f936b..9dd96c6620 100644 --- a/includes/REST/Manager.php +++ b/includes/REST/Manager.php @@ -234,6 +234,7 @@ private function get_rest_api_class_map() { DOKAN_DIR . '/includes/REST/StoreSettingControllerV2.php' => '\WeDevs\Dokan\REST\StoreSettingControllerV2', DOKAN_DIR . '/includes/REST/VendorDashboardController.php' => '\WeDevs\Dokan\REST\VendorDashboardController', DOKAN_DIR . '/includes/REST/ProductBlockController.php' => '\WeDevs\Dokan\REST\ProductBlockController', + DOKAN_DIR . '/includes/REST/CustomersController.php' => '\WeDevs\Dokan\REST\CustomersController', ) ); } From 3d4d19c981368694fe7f415e9fa7b27f058c60af Mon Sep 17 00:00:00 2001 From: Aunshon Date: Mon, 21 Oct 2024 14:49:27 +0600 Subject: [PATCH 05/16] Added router context support. (#2407) --- src/Dashboard/index.tsx | 15 ++++++++++++--- src/Routing/index.tsx | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/Dashboard/index.tsx b/src/Dashboard/index.tsx index c3f6134365..4bf0eadc89 100644 --- a/src/Dashboard/index.tsx +++ b/src/Dashboard/index.tsx @@ -1,9 +1,9 @@ import {createRoot} from "@wordpress/element"; import domReady from "@wordpress/dom-ready"; import Layout from "../Layout"; -import getRoutes from "../Routing"; +import getRoutes, { withRouter } from "../Routing"; import { - createBrowserRouter, createHashRouter, + createHashRouter, RouterProvider, } from "react-router-dom"; import './tailwind.scss'; @@ -12,9 +12,18 @@ const App = () => { const routes = getRoutes(); const mapedRoutes = routes.map((route) => { + const WithRouterComponent = withRouter( route.element ); + return { path: route.path, - element: {route.element}, + element: + + , } }); diff --git a/src/Routing/index.tsx b/src/Routing/index.tsx index f1339d4234..eb4e3ef895 100644 --- a/src/Routing/index.tsx +++ b/src/Routing/index.tsx @@ -1,6 +1,44 @@ import NotFound from "../Layout/404"; import {__} from "@wordpress/i18n"; import {DokanRoute} from "../Layout"; +import { isValidElement, cloneElement, createElement } from '@wordpress/element'; +import { useNavigate, useParams, useLocation, redirect, replace, useMatches, useNavigation, } from 'react-router-dom'; + +export function withRouter(Component) { + function ComponentWithRouterProp(props) { + let navigate = useNavigate(); + let params = useParams(); + let location = useLocation(); + let matches = useMatches(); + const navigation = useNavigation(); + + const routerProps = { + router: { + navigate, + params, + location, + redirect, + replace, + matches, + navigation + } + }; + + // Check if Component is a valid element + if (isValidElement(Component)) { + // If it's a valid element, clone it and pass the router props + return cloneElement(Component, { ...props, ...routerProps }); + } + + // If it's a function component, render it with the router props + return createElement(Component, { + ...props, + ...routerProps + }); + } + + return ComponentWithRouterProp; +} const getRoutes = () => { let routes : Array = []; From 9beb3b3325896b172f139fa2e7fa7fae8be0e7d7 Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Tue, 22 Oct 2024 11:15:51 +0600 Subject: [PATCH 06/16] update: header layout. --- src/Layout/Header.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Layout/Header.tsx b/src/Layout/Header.tsx index 5a10d1af56..89d2164fbc 100644 --- a/src/Layout/Header.tsx +++ b/src/Layout/Header.tsx @@ -3,16 +3,16 @@ import {Slot} from "@wordpress/components"; const Header = ( { title = '' } ) => { return ( - <> +
-
+
{ title && (

{title}

)}
-
+
- +
); }; From c27f504efea038ff27f7afa5d986bf4767d69f86 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:29:36 +0600 Subject: [PATCH 07/16] Updated react router props --- src/Routing/index.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Routing/index.tsx b/src/Routing/index.tsx index eb4e3ef895..e2ad48b45d 100644 --- a/src/Routing/index.tsx +++ b/src/Routing/index.tsx @@ -13,15 +13,13 @@ export function withRouter(Component) { const navigation = useNavigation(); const routerProps = { - router: { - navigate, - params, - location, - redirect, - replace, - matches, - navigation - } + navigate, + params, + location, + redirect, + replace, + matches, + navigation }; // Check if Component is a valid element From 0b1ce0249c1458e20a596025e6b62fbcd017fbc2 Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Tue, 22 Oct 2024 16:24:04 +0600 Subject: [PATCH 08/16] update: `useNavigate` send as fillProps for `dokan-header-actions` --- src/Layout/Header.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Layout/Header.tsx b/src/Layout/Header.tsx index 89d2164fbc..b8303c345f 100644 --- a/src/Layout/Header.tsx +++ b/src/Layout/Header.tsx @@ -1,6 +1,8 @@ import {Slot} from "@wordpress/components"; +import {useNavigate} from "react-router-dom"; const Header = ( { title = '' } ) => { + const navigate = useNavigate(); return (
@@ -9,7 +11,7 @@ const Header = ( { title = '' } ) => { { title && (

{title}

)}
- +
From 25b4a1acfe2c42848f927d67f88d0f08fbf09edc Mon Sep 17 00:00:00 2001 From: Al Amin Ahamed <34349365+mralaminahamed@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:48:50 +0600 Subject: [PATCH 09/16] feat: test case for Customer API --- composer.lock | 100 ++++---- includes/REST/CustomersController.php | 42 ++-- .../php/src/REST/CustomersControllerTest.php | 220 ++++++++++++++++++ 3 files changed, 294 insertions(+), 68 deletions(-) create mode 100644 tests/php/src/REST/CustomersControllerTest.php diff --git a/composer.lock b/composer.lock index 64fa4ef4f1..ca8d2b0c9f 100644 --- a/composer.lock +++ b/composer.lock @@ -138,20 +138,20 @@ "packages-dev": [ { "name": "antecedent/patchwork", - "version": "2.1.28", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d" + "reference": "b07d4fb37c3c723c8755122160c089e077d5de65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/6b30aff81ebadf0f2feb9268d3e08385cebcc08d", - "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/b07d4fb37c3c723c8755122160c089e077d5de65", + "reference": "b07d4fb37c3c723c8755122160c089e077d5de65", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { "phpunit/phpunit": ">=4" @@ -180,9 +180,9 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.28" + "source": "https://github.com/antecedent/patchwork/tree/2.2.0" }, - "time": "2024-02-06T09:26:11+00:00" + "time": "2024-09-27T16:59:55+00:00" }, { "name": "brain/monkey", @@ -406,12 +406,12 @@ "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "dd03e528e461e059e21cdd2ce50fdb842efe5a91" + "reference": "4f0f997f2a84e694e658c29a1f22e8a34377f1aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/dd03e528e461e059e21cdd2ce50fdb842efe5a91", - "reference": "dd03e528e461e059e21cdd2ce50fdb842efe5a91", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/4f0f997f2a84e694e658c29a1f22e8a34377f1aa", + "reference": "4f0f997f2a84e694e658c29a1f22e8a34377f1aa", "shasum": "" }, "require": { @@ -450,7 +450,7 @@ "issues": "https://github.com/hamcrest/hamcrest-php/issues", "source": "https://github.com/hamcrest/hamcrest-php/tree/master" }, - "time": "2024-07-08T08:13:58+00:00" + "time": "2024-10-19T14:00:42+00:00" }, { "name": "mockery/mockery", @@ -597,16 +597,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.2.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -649,9 +649,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-09-15T16:40:33+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "phar-io/manifest", @@ -912,12 +912,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" + "reference": "fd4181347ee2b93e6e581acb48f8c271b763a738" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/fd4181347ee2b93e6e581acb48f8c271b763a738", + "reference": "fd4181347ee2b93e6e581acb48f8c271b763a738", "shasum": "" }, "require": { @@ -975,7 +975,7 @@ "type": "open_collective" } ], - "time": "2024-04-24T21:37:59+00:00" + "time": "2024-10-16T19:34:28+00:00" }, { "name": "phpcsstandards/phpcsextra", @@ -983,12 +983,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "de3789a59ee046291bb196c03862c871a692dc75" + "reference": "d0304203dd7c6f8eab21ed8adabe5e3be380bdbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/de3789a59ee046291bb196c03862c871a692dc75", - "reference": "de3789a59ee046291bb196c03862c871a692dc75", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/d0304203dd7c6f8eab21ed8adabe5e3be380bdbe", + "reference": "d0304203dd7c6f8eab21ed8adabe5e3be380bdbe", "shasum": "" }, "require": { @@ -1054,7 +1054,7 @@ "type": "open_collective" } ], - "time": "2024-07-23T06:25:32+00:00" + "time": "2024-10-14T00:01:10+00:00" }, { "name": "phpcsstandards/phpcsutils", @@ -1062,18 +1062,18 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "dd8f54106f37cd8c6b681d5c659b76a1d309c67f" + "reference": "a6a0a723bc4efce5b8a12646f28bb83c4610dfac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/dd8f54106f37cd8c6b681d5c659b76a1d309c67f", - "reference": "dd8f54106f37cd8c6b681d5c659b76a1d309c67f", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/a6a0a723bc4efce5b8a12646f28bb83c4610dfac", + "reference": "a6a0a723bc4efce5b8a12646f28bb83c4610dfac", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.10.1 || 4.0.x-dev@dev" }, "require-dev": { "ext-filter": "*", @@ -1143,7 +1143,7 @@ "type": "open_collective" } ], - "time": "2024-09-16T11:24:55+00:00" + "time": "2024-10-13T23:57:40+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1470,12 +1470,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "bc0b86cf861901c60c8a7e62aa0b17f3260d3640" + "reference": "fcd1efebc9510d2e1cff8fcf1b13ad2a7827d675" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bc0b86cf861901c60c8a7e62aa0b17f3260d3640", - "reference": "bc0b86cf861901c60c8a7e62aa0b17f3260d3640", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fcd1efebc9510d2e1cff8fcf1b13ad2a7827d675", + "reference": "fcd1efebc9510d2e1cff8fcf1b13ad2a7827d675", "shasum": "" }, "require": { @@ -1565,7 +1565,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T10:23:44+00:00" + "time": "2024-10-19T09:36:22+00:00" }, { "name": "sebastian/cli-parser", @@ -2537,12 +2537,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "92c8ef54357ea09d93f8efa043d7c9c0a71ae094" + "reference": "9e60f9f8748736d3d06fea8ef26dccbdc5ba4902" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/92c8ef54357ea09d93f8efa043d7c9c0a71ae094", - "reference": "92c8ef54357ea09d93f8efa043d7c9c0a71ae094", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/9e60f9f8748736d3d06fea8ef26dccbdc5ba4902", + "reference": "9e60f9f8748736d3d06fea8ef26dccbdc5ba4902", "shasum": "" }, "require": { @@ -2610,7 +2610,7 @@ "type": "open_collective" } ], - "time": "2024-09-18T16:46:38+00:00" + "time": "2024-10-16T22:25:17+00:00" }, { "name": "tareq1988/wp-php-cs-fixer", @@ -2706,12 +2706,12 @@ "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "7f766304b0654cee7c1dfa8e548f65fce197e05c" + "reference": "2133137c33fa898df70b5f879a65d83af4dbb97d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7f766304b0654cee7c1dfa8e548f65fce197e05c", - "reference": "7f766304b0654cee7c1dfa8e548f65fce197e05c", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/2133137c33fa898df70b5f879a65d83af4dbb97d", + "reference": "2133137c33fa898df70b5f879a65d83af4dbb97d", "shasum": "" }, "require": { @@ -2765,7 +2765,7 @@ "type": "custom" } ], - "time": "2024-08-28T05:05:46+00:00" + "time": "2024-10-05T00:46:24+00:00" }, { "name": "wp-phpunit/wp-phpunit", @@ -2822,12 +2822,12 @@ "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "e87abf2352b5f6736cab7c8d23c581ef6aaeaf3f" + "reference": "a4a261f6fb8ed016a9e9ea287ead829f232bc3f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/e87abf2352b5f6736cab7c8d23c581ef6aaeaf3f", - "reference": "e87abf2352b5f6736cab7c8d23c581ef6aaeaf3f", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/a4a261f6fb8ed016a9e9ea287ead829f232bc3f7", + "reference": "a4a261f6fb8ed016a9e9ea287ead829f232bc3f7", "shasum": "" }, "require": { @@ -2877,15 +2877,15 @@ "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2024-09-16T19:21:25+00:00" + "time": "2024-10-13T18:33:27+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { - "wp-coding-standards/wpcs": 20, - "tareq1988/wp-php-cs-fixer": 20, "phpcompatibility/phpcompatibility-wp": 20, + "tareq1988/wp-php-cs-fixer": 20, + "wp-coding-standards/wpcs": 20, "wp-phpunit/wp-phpunit": 20, "yoast/phpunit-polyfills": 20 }, @@ -2894,9 +2894,9 @@ "platform": { "php": ">=7.4" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/includes/REST/CustomersController.php b/includes/REST/CustomersController.php index ca2442c655..62c2e53321 100644 --- a/includes/REST/CustomersController.php +++ b/includes/REST/CustomersController.php @@ -4,10 +4,12 @@ use WC_Data; use WC_Customer; +use WC_Data_Store; use WC_REST_Customers_Controller; use WP_Error; use WP_REST_Request; use WP_REST_Response; +use WP_REST_Server; class CustomersController extends WC_REST_Customers_Controller { @@ -25,24 +27,26 @@ public function register_routes() { parent::register_routes(); // Add new route for searching customers - register_rest_route( $this->namespace, '/' . $this->rest_base . '/search', array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'search_customers' ), - 'permission_callback' => array( $this, 'search_customers_permissions_check' ), - 'args' => array( - 'search' => array( - 'description' => __( 'Search string.', 'dokan-lite' ), - 'type' => 'string', - 'required' => true, - ), - 'exclude' => array( - 'description' => __( 'Comma-separated list of customer IDs to exclude.', 'dokan-lite' ), - 'type' => 'string', - ), - ), - ), - ) ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/search', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'search_customers' ), + 'permission_callback' => array( $this, 'search_customers_permissions_check' ), + 'args' => array( + 'search' => array( + 'description' => __( 'Search string.', 'dokan-lite' ), + 'type' => 'string', + 'required' => true, + ), + 'exclude' => array( + 'description' => __( 'Comma-separated list of customer IDs to exclude.', 'dokan-lite' ), + 'type' => 'string', + ), + ), + ), + ) + ); } /** @@ -107,7 +111,9 @@ function () use ( $request ) { * Search customers for the current vendor. * * @param WP_REST_Request $request Full details about the request. + * * @return WP_Error|WP_REST_Response + * @throws \Exception */ public function search_customers( $request ) { if ( ! current_user_can( 'edit_shop_orders' ) ) { diff --git a/tests/php/src/REST/CustomersControllerTest.php b/tests/php/src/REST/CustomersControllerTest.php new file mode 100644 index 0000000000..f5693effe1 --- /dev/null +++ b/tests/php/src/REST/CustomersControllerTest.php @@ -0,0 +1,220 @@ +controller = new CustomersController(); + } + + /** + * Test registering REST routes + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( "/{$this->namespace}/customers", $routes ); + $this->assertArrayHasKey( "/{$this->namespace}/customers/(?P[\\d]+)", $routes ); + $this->assertArrayHasKey( "/{$this->namespace}/customers/search", $routes ); + } + + /** + * Test get_items permission check for non-vendor users + */ + public function test_get_items_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->get_request( 'customers' ); + + $this->assertEquals( 401, $response->get_status() ); + $this->assertEquals( 'dokan_rest_cannot_view', $response->get_data()['code'] ); + } + + /** + * Test get_items for vendor + */ + public function test_get_items_as_vendor() { + wp_set_current_user( $this->seller_id1 ); + + // Create orders for the vendor with customers + $customer_ids = []; + for ( $i = 0; $i < 3; $i++ ) { + $customer_id = $this->factory()->customer->create(); + $customer_ids[] = $customer_id; + + // Create order for this customer with the vendor + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ 'customer_id' => $customer_id ] + ); + } + + $response = $this->get_request( 'customers' ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 4, $data ); + + // Verify only customers with orders from this vendor are returned + $returned_ids = array_map( + function ( $customer ) { + return $customer['id']; + }, $data + ); + + $this->assertEquals( sort( $customer_ids ), sort( $returned_ids ) ); + } + + /** + * Test get_item permission check + */ + public function test_get_item_without_permission() { + wp_set_current_user( 0 ); + + $customer_id = $this->factory()->customer->create(); + + $response = $this->get_request( "customers/{$customer_id}" ); + + $this->assertEquals( 401, $response->get_status() ); + $this->assertEquals( 'dokan_rest_cannot_view', $response->get_data()['code'] ); + } + + /** + * Test get_item for vendor + */ + public function test_get_item_as_vendor() { + wp_set_current_user( $this->seller_id1 ); + + $customer_id = $this->factory()->customer->create(); + + // Create order for customer with the vendor + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ 'customer_id' => $customer_id ] + ); + + $response = $this->get_request( "customers/{$customer_id}" ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $customer_id, $data['id'] ); + } + + /** + * Test customer search functionality + */ + public function test_search_customers() { + wp_set_current_user( $this->seller_id1 ); + + // Create test customers with orders + for ( $i = 0; $i < 3; $i++ ) { + $customer_id = $this->factory()->customer->create( + [ + 'first_name' => "Test{$i}", + 'last_name' => 'Customer', + 'email' => "test{$i}@example.com", + 'username' => "test{$i}", + ] + ); + + // Create order for this customer + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ 'customer_id' => $customer_id ] + ); + } + + // Test search by name + $response = $this->get_request( 'customers/search', [ 'search' => 'Test1' ] ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $data ); + $this->assertEquals( 'Test1 Customer', $data[0]['name'] ); + + // Test search by email + $response = $this->get_request( 'customers/search', [ 'search' => 'test2@example' ] ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $data ); + $this->assertEquals( 'test2@example.com', $data[0]['email'] ); + } + + /** + * Test customer search with exclude parameter + */ + public function test_search_customers_with_exclude() { + wp_set_current_user( $this->seller_id1 ); + + // Create test customers + $exclude_customer_id = $this->factory()->customer->create( + [ + 'first_name' => 'Exclude', + 'last_name' => 'Customer', + 'username' => 'exclude_customer', + ] + ); + + $keep_customer_id = $this->factory()->customer->create( + [ + 'first_name' => 'Keep', + 'last_name' => 'Customer', + 'username' => 'keep_customer', + ] + ); + + // Create orders for both customers + foreach ( [ $exclude_customer_id, $keep_customer_id ] as $customer_id ) { + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ 'customer_id' => $customer_id ] + ); + } + + // Search with exclude parameter + $response = $this->get_request( + 'customers/search', [ + 'search' => (string) $keep_customer_id, + 'exclude' => (string) $exclude_customer_id, + ] + ); + + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $data ); + $this->assertEquals( 'Keep Customer', $data[0]['name'] ); + } + + /** + * Test search validation + */ + public function test_search_customers_validation() { + wp_set_current_user( $this->seller_id1 ); + + // Test empty search term + $response = $this->get_request( 'customers/search', [ 'search' => '' ] ); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'dokan_rest_empty_search', $response->get_data()['code'] ); + } +} From c56dacbfa747dacb84a72bbec283e6290c06bcdf Mon Sep 17 00:00:00 2001 From: Al Amin Ahamed <34349365+mralaminahamed@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:32:22 +0600 Subject: [PATCH 10/16] feat: and more callback and permission for crud endpoints and test cases for Customer API --- includes/REST/CustomersController.php | 99 ++- .../php/src/REST/CustomersControllerTest.php | 616 +++++++++++++++--- 2 files changed, 610 insertions(+), 105 deletions(-) diff --git a/includes/REST/CustomersController.php b/includes/REST/CustomersController.php index 62c2e53321..af72481175 100644 --- a/includes/REST/CustomersController.php +++ b/includes/REST/CustomersController.php @@ -64,6 +64,8 @@ protected function check_permission( $request, $action ) { 'create' => __( 'Sorry, you are not allowed to create resources.', 'dokan-lite' ), 'edit' => __( 'Sorry, you are not allowed to edit this resource.', 'dokan-lite' ), 'delete' => __( 'Sorry, you are not allowed to delete this resource.', 'dokan-lite' ), + 'batch' => __( 'Sorry, you are not allowed to batch update resources.', 'dokan-lite' ), + 'search' => __( 'Sorry, you are not allowed to search customers.', 'dokan-lite' ), ]; return new WP_Error( "dokan_rest_cannot_$action", $messages[ $action ], [ 'status' => rest_authorization_required_code() ] ); } @@ -107,6 +109,56 @@ function () use ( $request ) { ); } + /** + * Create a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::create_item( $request ); + } + ); + } + + /** + * Update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::update_item( $request ); + } + ); + } + + /** + * Delete a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::delete_item( $request ); + } + ); + } + + public function batch_items( $request ) { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::batch_items( $request ); + } + ); + } + /** * Search customers for the current vendor. * @@ -254,13 +306,58 @@ public function get_item_permissions_check( $request ) { return $this->check_permission( $request, 'view' ); } + /** + * Check if a given request has access to create a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + return $this->check_permission( $request, 'create' ); + } + + /** + * Check if a given request has access to update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + return $this->check_permission( $request, 'edit' ); + } + + /** + * Check if a given request has access to delete a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + return $this->check_permission( $request, 'delete' ); + } + + /** + * Check if a given request has access to batch items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function batch_items_permissions_check( $request ) { + return $this->check_permission( $request, 'batch' ); + } + /** * Check if a given request has access to search customers. * * @param WP_REST_Request $request Full details about the request. + * * @return WP_Error|boolean */ public function search_customers_permissions_check( $request ) { - return $this->check_permission( $request, 'view' ); + return $this->check_permission( $request, 'search' ); } } diff --git a/tests/php/src/REST/CustomersControllerTest.php b/tests/php/src/REST/CustomersControllerTest.php index f5693effe1..0121dfd473 100644 --- a/tests/php/src/REST/CustomersControllerTest.php +++ b/tests/php/src/REST/CustomersControllerTest.php @@ -2,8 +2,12 @@ namespace WeDevs\Dokan\Test\REST; +use Exception; +use WC_Customer; use WeDevs\Dokan\REST\CustomersController; use WeDevs\Dokan\Test\DokanTestCase; +use WP_REST_Request; +use WP_REST_Response; /** * @group dokan-customers @@ -18,7 +22,12 @@ class CustomersControllerTest extends DokanTestCase { /** * @var CustomersController */ - protected $controller; + protected CustomersController $controller; + + /** + * @var array + */ + protected $customers = []; /** * Setup test environment @@ -27,194 +36,593 @@ protected function setUp(): void { parent::setUp(); $this->controller = new CustomersController(); + + // Create test customers with specific data + $this->customer_data = [ + [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john.doe@example.com', + 'username' => 'johndoe', + ], + [ + 'first_name' => 'Jane', + 'last_name' => 'Smith', + 'email' => 'jane.smith@example.com', + 'username' => 'janesmith', + ], + ]; + + foreach ( $this->customer_data as $data ) { + $this->customers[] = $this->factory()->customer->create( $data ); + } } /** - * Test registering REST routes + * Test route registration */ public function test_register_routes() { $routes = $this->server->get_routes(); - $this->assertArrayHasKey( "/{$this->namespace}/customers", $routes ); - $this->assertArrayHasKey( "/{$this->namespace}/customers/(?P[\\d]+)", $routes ); - $this->assertArrayHasKey( "/{$this->namespace}/customers/search", $routes ); + $base_endpoints = [ + "/$this->namespace/customers", + "/$this->namespace/customers/(?P[\\d]+)", + "/$this->namespace/customers/search", + "/$this->namespace/customers/batch", + ]; + + foreach ( $base_endpoints as $endpoint ) { + $this->assertArrayHasKey( $endpoint, $routes ); + } + + // Verify HTTP methods for each endpoint + $this->assertCount( 2, $routes[ "/$this->namespace/customers" ] ); + $this->assertCount( 3, $routes[ "/$this->namespace/customers/(?P[\\d]+)" ] ); + $this->assertCount( 1, $routes[ "/$this->namespace/customers/search" ] ); + $this->assertCount( 1, $routes[ "/$this->namespace/customers/batch" ] ); } /** - * Test get_items permission check for non-vendor users + * Test permission checks for each endpoint */ - public function test_get_items_without_permission() { - wp_set_current_user( 0 ); - - $response = $this->get_request( 'customers' ); - - $this->assertEquals( 401, $response->get_status() ); - $this->assertEquals( 'dokan_rest_cannot_view', $response->get_data()['code'] ); + public function test_endpoint_permissions() { + $test_cases = [ + [ 'GET', 'customers', 401, 'dokan_rest_cannot_view' ], + [ 'POST', 'customers', 400, 'rest_missing_callback_param' ], + [ 'GET', "customers/{$this->customers[0]}", 401, 'dokan_rest_cannot_view' ], + [ 'PUT', "customers/{$this->customers[0]}", 401, 'dokan_rest_cannot_edit' ], + [ 'DELETE', "customers/{$this->customers[0]}", 401, 'dokan_rest_cannot_delete' ], + [ 'POST', 'customers/batch', 401, 'dokan_rest_cannot_batch' ], + [ 'GET', 'customers/search', 400, 'rest_missing_callback_param' ], + ]; + + foreach ( $test_cases as [ $method, $endpoint, $expected_status, $expected_code ] ) { + wp_set_current_user( 0 ); + + $response = $this->request( $method, $endpoint ); + + $this->assertEquals( $expected_status, $response->get_status() ); + $this->assertEquals( $expected_code, $response->get_data()['code'] ); + } } /** - * Test get_items for vendor + * Test get_items functionality */ - public function test_get_items_as_vendor() { + public function test_get_items() { wp_set_current_user( $this->seller_id1 ); - // Create orders for the vendor with customers - $customer_ids = []; - for ( $i = 0; $i < 3; $i++ ) { - $customer_id = $this->factory()->customer->create(); - $customer_ids[] = $customer_id; - - // Create order for this customer with the vendor + // Create orders for customers with the vendor + foreach ( $this->customers as $customer_id ) { $this->factory()->order->set_seller_id( $this->seller_id1 )->create( - [ 'customer_id' => $customer_id ] + [ + 'customer_id' => $customer_id, + ] ); } + // Test default listing $response = $this->get_request( 'customers' ); - $data = $response->get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertCount( 4, $data ); + $this->assertCount( 3, $response->get_data() ); - // Verify only customers with orders from this vendor are returned - $returned_ids = array_map( - function ( $customer ) { - return $customer['id']; - }, $data - ); + // Test with per_page parameter + $response = $this->get_request( 'customers', [ 'per_page' => 1 ] ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $response->get_data() ); - $this->assertEquals( sort( $customer_ids ), sort( $returned_ids ) ); + // Test with ordering + $response = $this->get_request( + 'customers', [ + 'orderby' => 'registered_date', + 'order' => 'desc', + ] + ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertTrue( + strtotime( $data[0]['date_created'] ) >= strtotime( $data[1]['date_created'] ) + ); } /** - * Test get_item permission check + * Test get_item functionality */ - public function test_get_item_without_permission() { - wp_set_current_user( 0 ); + public function test_get_item() { + wp_set_current_user( $this->seller_id1 ); - $customer_id = $this->factory()->customer->create(); + // Create order to establish vendor-customer relationship + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ + 'customer_id' => $this->customers[0], + ] + ); - $response = $this->get_request( "customers/{$customer_id}" ); + // Test valid customer fetch + $response = $this->get_request( "customers/{$this->customers[0]}" ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( $this->customers[0], $data['id'] ); + $this->assertEquals( 'john.doe@example.com', $data['email'] ); - $this->assertEquals( 401, $response->get_status() ); - $this->assertEquals( 'dokan_rest_cannot_view', $response->get_data()['code'] ); + // Test invalid customer ID + $response = $this->get_request( 'customers/999999' ); + $this->assertEquals( 404, $response->get_status() ); } /** - * Test get_item for vendor + * Test create_item functionality */ - public function test_get_item_as_vendor() { + public function test_create_item() { wp_set_current_user( $this->seller_id1 ); - $customer_id = $this->factory()->customer->create(); + $test_cases = [ + // Valid customer data + [ + 'data' => [ + 'email' => 'new.customer@example.com', + 'first_name' => 'New', + 'last_name' => 'Customer', + 'username' => 'newcustomer', + 'password' => 'password123', + ], + 'expected_status' => 201, + 'assertions' => function ( $response ) { + $data = $response->get_data(); + $this->assertEquals( 'new.customer@example.com', $data['email'] ); + $this->assertEquals( 'New', $data['first_name'] ); + }, + ], + // Missing required fields + [ + 'data' => [ + 'first_name' => 'Invalid', + ], + 'expected_status' => 400, + 'assertions' => function ( $response ) { + $this->assertEquals( 'rest_missing_callback_param', $response->get_data()['code'] ); + }, + ], + // Invalid email + [ + 'data' => [ + 'email' => 'invalid-email', + 'first_name' => 'Invalid', + 'username' => 'invalid', + ], + 'expected_status' => 400, + 'assertions' => function ( $response ) { + $this->assertEquals( 'customer_invalid_email', $response->get_data()['code'] ); + }, + ], + ]; + + foreach ( $test_cases as $test_case ) { + $response = $this->post_request( 'customers', $test_case['data'] ); + $this->assertEquals( $test_case['expected_status'], $response->get_status() ); + $test_case['assertions']( $response ); + } + } + + /** + * Test update_item functionality + */ + public function test_update_item() { + wp_set_current_user( $this->seller_id1 ); - // Create order for customer with the vendor + // Create order to establish vendor-customer relationship $this->factory()->order->set_seller_id( $this->seller_id1 )->create( - [ 'customer_id' => $customer_id ] + [ + 'customer_id' => $this->customers[0], + ] ); - $response = $this->get_request( "customers/{$customer_id}" ); - $data = $response->get_data(); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( $customer_id, $data['id'] ); + $test_cases = [ + // Valid update + [ + 'data' => [ + 'first_name' => 'Updated', + 'last_name' => 'Name', + 'email' => 'updated.email@example.com', + ], + 'expected_status' => 200, + 'assertions' => function ( $response ) { + $data = $response->get_data(); + $this->assertEquals( 'Updated', $data['first_name'] ); + $this->assertEquals( 'updated.email@example.com', $data['email'] ); + }, + ], + // Invalid email update + [ + 'data' => [ + 'email' => 'invalid-email', + ], + 'expected_status' => 400, + 'assertions' => function ( $response ) { + $this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] ); + }, + ], + ]; + + foreach ( $test_cases as $test_case ) { + $response = $this->put_request( "customers/{$this->customers[0]}", $test_case['data'] ); + $this->assertEquals( $test_case['expected_status'], $response->get_status() ); + $test_case['assertions']( $response ); + } } /** - * Test customer search functionality + * Test batch operations */ - public function test_search_customers() { + public function test_batch_operations() { wp_set_current_user( $this->seller_id1 ); - // Create test customers with orders - for ( $i = 0; $i < 3; $i++ ) { - $customer_id = $this->factory()->customer->create( + // Create vendor-customer relationships + foreach ( $this->customers as $customer_id ) { + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( [ - 'first_name' => "Test{$i}", - 'last_name' => 'Customer', - 'email' => "test{$i}@example.com", - 'username' => "test{$i}", + 'customer_id' => $customer_id, ] ); - - // Create order for this customer - $this->factory()->order->set_seller_id( $this->seller_id1 )->create( - [ 'customer_id' => $customer_id ] - ); } - // Test search by name - $response = $this->get_request( 'customers/search', [ 'search' => 'Test1' ] ); - $data = $response->get_data(); - + $batch_data = [ + 'create' => [ + [ + 'email' => 'batch.new@example.com', + 'first_name' => 'Batch', + 'last_name' => 'New', + 'username' => 'batchnew', + ], + ], + 'update' => [ + [ + 'id' => $this->customers[0], + 'first_name' => 'Batch', + 'last_name' => 'Updated', + ], + ], + 'delete' => [ + $this->customers[1], + ], + ]; + + $response = $this->post_request( 'customers/batch', $batch_data ); $this->assertEquals( 200, $response->get_status() ); - $this->assertCount( 1, $data ); - $this->assertEquals( 'Test1 Customer', $data[0]['name'] ); - // Test search by email - $response = $this->get_request( 'customers/search', [ 'search' => 'test2@example' ] ); $data = $response->get_data(); - $this->assertEquals( 200, $response->get_status() ); - $this->assertCount( 1, $data ); - $this->assertEquals( 'test2@example.com', $data[0]['email'] ); + // Verify creation + $this->assertCount( 1, $data['create'] ); + $this->assertEquals( 'batch.new@example.com', $data['create'][0]['email'] ); + + // Verify update + $this->assertCount( 1, $data['update'] ); + $this->assertEquals( 'Batch', $data['update'][0]['first_name'] ); + + // Verify deletion + $this->assertCount( 1, $data['delete'] ); + $this->assertEquals( $this->customers[1], $data['delete'][0]['id'] ); + + // Verify database state + $this->assertFalse( get_user_by( 'id', $this->customers[1] ) ); } /** - * Test customer search with exclude parameter + * Test search functionality */ - public function test_search_customers_with_exclude() { + public function test_search_functionality() { wp_set_current_user( $this->seller_id1 ); - // Create test customers - $exclude_customer_id = $this->factory()->customer->create( + // Create orders for test customers + foreach ( $this->customers as $customer_id ) { + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ 'customer_id' => $customer_id ] + ); + } + + $test_cases = [ + // Search by email + [ + 'params' => [ 'search' => 'john.doe@example.com' ], + 'expected_count' => 1, + 'assertions' => function ( $data ) { + $this->assertEquals( 'john.doe@example.com', $data[0]['email'] ); + }, + ], + // Search by partial email [ - 'first_name' => 'Exclude', - 'last_name' => 'Customer', - 'username' => 'exclude_customer', + 'params' => [ 'search' => '@example.com' ], + 'expected_count' => 2, + 'assertions' => function ( $data ) { + $this->assertContains( 'john.doe@example.com', wp_list_pluck( $data, 'email' ) ); + $this->assertContains( 'jane.smith@example.com', wp_list_pluck( $data, 'email' ) ); + }, + ], + // Search by name + [ + 'params' => [ 'search' => 'John' ], + 'expected_count' => 1, + 'assertions' => function ( $data ) { + $this->assertEquals( 'John Doe', $data[0]['name'] ); + }, + ], + // Search with exclude + [ + 'params' => [ + 'search' => '@example.com', + 'exclude' => (string) $this->customers[0], + ], + 'expected_count' => 1, + 'assertions' => function ( $data ) { + $this->assertEquals( 'jane.smith@example.com', $data[0]['email'] ); + }, + ], + ]; + + foreach ( $test_cases as $test_case ) { + $response = $this->get_request( 'customers/search', $test_case['params'] ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( $test_case['expected_count'], $data ); + $test_case['assertions']( $data ); + } + } + + /** + * Test vendor specific customer filtering (continued) + */ + public function test_vendor_customer_filtering() { + // Create customers with orders from different vendors + $customer_id = $this->factory()->customer->create( + [ + 'email' => 'multi.vendor@example.com', + 'first_name' => 'Multi', + 'last_name' => 'Vendor', ] ); - $keep_customer_id = $this->factory()->customer->create( + // Create orders for both vendors + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ + 'customer_id' => $customer_id, + ] + ); + $this->factory()->order->set_seller_id( $this->seller_id2 )->create( [ - 'first_name' => 'Keep', - 'last_name' => 'Customer', - 'username' => 'keep_customer', + 'customer_id' => $customer_id, ] ); - // Create orders for both customers - foreach ( [ $exclude_customer_id, $keep_customer_id ] as $customer_id ) { - $this->factory()->order->set_seller_id( $this->seller_id1 )->create( - [ 'customer_id' => $customer_id ] - ); - } + // Test as first vendor + wp_set_current_user( $this->seller_id1 ); + $response = $this->get_request( 'customers' ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( $customer_id, wp_list_pluck( $response->get_data(), 'id' ) ); - // Search with exclude parameter - $response = $this->get_request( - 'customers/search', [ - 'search' => (string) $keep_customer_id, - 'exclude' => (string) $exclude_customer_id, + // Test as second vendor + wp_set_current_user( $this->seller_id2 ); + $response = $this->get_request( 'customers' ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( $customer_id, wp_list_pluck( $response->get_data(), 'id' ) ); + + // Verify customer details match for both vendors + $response1 = $this->get_request( "customers/$customer_id" ); + $response2 = $this->get_request( "customers/$customer_id" ); + $this->assertEquals( $response1->get_data(), $response2->get_data() ); + } + + /** + * Test customer meta data handling + */ + public function test_customer_meta_data() { + wp_set_current_user( $this->seller_id1 ); + + // Create customer with meta + $customer_data = [ + 'email' => 'meta.test@example.com', + 'first_name' => 'Meta', + 'last_name' => 'Test', + 'username' => 'metatest', + 'meta_data' => [ + [ + 'key' => 'dokan_test_meta', + 'value' => 'test_value', + ], + ], + ]; + + $response = $this->post_request( 'customers', $customer_data ); + $this->assertEquals( 201, $response->get_status() ); + $customer_id = $response->get_data()['id']; + + // Create order to establish vendor relationship + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + [ + 'customer_id' => $customer_id, ] ); + // Test meta data retrieval + $response = $this->get_request( "customers/$customer_id" ); $data = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + + // WooCommerce api does not support meta data retrieval + $this->assertArrayNotHasKey( 'meta_data', $data ); + } + + /** + * Test customer search validation and edge cases + */ + public function test_search_validation_and_edge_cases() { + wp_set_current_user( $this->seller_id1 ); + + $test_cases = [ + // Empty search term + [ + 'params' => [ 'search' => '' ], + 'expected_status' => 400, + 'expected_code' => 'dokan_rest_empty_search', + ], + // Very short search term + [ + 'params' => [ 'search' => 'a' ], + 'expected_status' => 200, + 'assertions' => function ( $response ) { + $this->assertLessThanOrEqual( 20, count( $response->get_data() ) ); + }, + ], + // Multiple excludes + [ + 'params' => [ + 'search' => 'test', + 'exclude' => implode( ',', $this->customers ), + ], + 'expected_status' => 200, + 'assertions' => function ( $response ) { + $excluded_ids = wp_list_pluck( $response->get_data(), 'id' ); + foreach ( $this->customers as $customer_id ) { + $this->assertNotContains( $customer_id, $excluded_ids ); + } + }, + ], + ]; + + foreach ( $test_cases as $test_case ) { + $response = $this->get_request( 'customers/search', $test_case['params'] ); + + if ( isset( $test_case['expected_status'] ) ) { + $this->assertEquals( $test_case['expected_status'], $response->get_status() ); + } + + if ( isset( $test_case['expected_code'] ) ) { + $this->assertEquals( $test_case['expected_code'], $response->get_data()['code'] ); + } + + if ( isset( $test_case['assertions'] ) ) { + $test_case['assertions']( $response ); + } + } + } + + /** + * Test customer role handling + * @throws Exception + */ + public function test_customer_role_handling() { + wp_set_current_user( $this->seller_id1 ); + // Test creating customer with additional roles + $customer_data = [ + 'email' => 'role.test@example.com', + 'first_name' => 'Role', + 'last_name' => 'Test', + 'username' => 'roletest', + 'password' => 'password123', + 'roles' => [ 'customer', 'subscriber' ], + ]; + + $response = $this->post_request( 'customers', $customer_data ); + $this->assertEquals( 201, $response->get_status() ); + $customer_id = $response->get_data()['id']; + + // Verify roles + $customer = new WC_Customer( $customer_id ); + $customer_role = $customer->get_role(); + $this->assertEquals( 'customer', $customer_role ); + + // Test updating roles + $update_data = [ + 'roles' => [ 'customer' ], + ]; + + $response = $this->put_request( "customers/$customer_id", $update_data ); $this->assertEquals( 200, $response->get_status() ); - $this->assertCount( 1, $data ); - $this->assertEquals( 'Keep Customer', $data[0]['name'] ); + + // Verify updated roles + $customer = new WC_Customer( $customer_id ); + $this->assertEquals( 'customer', $customer->get_role() ); } /** - * Test search validation + * Test error responses format */ - public function test_search_customers_validation() { + public function test_error_response_format() { wp_set_current_user( $this->seller_id1 ); - // Test empty search term - $response = $this->get_request( 'customers/search', [ 'search' => '' ] ); + $test_cases = [ + // Invalid email format + [ + 'endpoint' => 'customers', + 'method' => 'POST', + 'data' => [ + 'email' => 'invalid-email', + 'username' => 'test', + ], + 'assertions' => function ( $response ) { + $data = $response->get_data(); + $this->assertEquals( 400, $response->get_status() ); + $this->assertArrayHasKey( 'code', $data ); + $this->assertArrayHasKey( 'message', $data ); + $this->assertArrayHasKey( 'data', $data ); + }, + ], + // Duplicate username + [ + 'endpoint' => 'customers', + 'method' => 'POST', + 'data' => [ + 'email' => 'unique@example.com', + 'username' => 'admin', + ], + 'assertions' => function ( $response ) { + $data = $response->get_data(); + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'registration-error-username-exists', $data['code'] ); + }, + ], + ]; + + foreach ( $test_cases as $test_case ) { + $response = $this->request( + $test_case['method'], + $test_case['endpoint'], + $test_case['data'] + ); + $test_case['assertions']( $response ); + } + } - $this->assertEquals( 400, $response->get_status() ); - $this->assertEquals( 'dokan_rest_empty_search', $response->get_data()['code'] ); + /** + * Helper method for making requests + */ + protected function request( $method, $endpoint, $data = [] ): WP_REST_Response { + $request = new WP_REST_Request( $method, "/$this->namespace/$endpoint" ); + if ( ! empty( $data ) ) { + $request->set_body_params( $data ); + } + return $this->server->dispatch( $request ); } } From e59521e8f6d556bf5cbb62301612cbf576981a56 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:13:44 +0600 Subject: [PATCH 11/16] Country and Continent api added. --- .../REST/DokanDataContinentsController.php | 123 ++++++++++++++++++ .../REST/DokanDataCountriesController.php | 123 ++++++++++++++++++ includes/REST/Manager.php | 2 + 3 files changed, 248 insertions(+) create mode 100644 includes/REST/DokanDataContinentsController.php create mode 100644 includes/REST/DokanDataCountriesController.php diff --git a/includes/REST/DokanDataContinentsController.php b/includes/REST/DokanDataContinentsController.php new file mode 100644 index 0000000000..783396ae59 --- /dev/null +++ b/includes/REST/DokanDataContinentsController.php @@ -0,0 +1,123 @@ +set_woocommerce_rest_check_permissions(); + + return parent::get_items( $request ); + } + + /** + * Return the list of states for a given country. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $this->set_woocommerce_rest_check_permissions(); + return parent::get_item( $request ); + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $this->set_woocommerce_rest_check_permissions(); + $item_permission = $this->get_items_permissions_check( $request ); + + if ( is_wp_error( $item_permission ) ) { + return $item_permission; + } + + return parent::get_item_permissions_check( $request ); + } + + /** + * Check if a given request has access to read items. + * + * @since DOKAN_SINCE + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + // phpcs:ignore WordPress.WP.Capabilities.Unknown + if ( current_user_can( dokan_admin_menu_capability() ) || current_user_can( 'dokandar' ) ) { + return true; + } + + return new WP_Error( + 'dokan_pro_permission_failure', + __( 'You are not allowed to do this action.', 'dokan-lite' ), + [ + 'status' => rest_authorization_required_code(), + ] + ); + } + + private function set_woocommerce_rest_check_permissions() { + add_filter( 'woocommerce_rest_check_permissions', [ $this, 'add_subscriptions_read_permission_to_vendors' ], 10, 4 ); + } + + /** + * Add permissions. + * + * @since DOKAN_PRO_SINCE + * + * @param $permission + * @param $context + * @param $object_id + * @param $obj + * + * @return true + */ + public function add_subscriptions_read_permission_to_vendors( $permission, $context, $object_id, $obj ) { + if ( 'read' === $context ) { + return true; + } + + return $permission; + } +} diff --git a/includes/REST/DokanDataCountriesController.php b/includes/REST/DokanDataCountriesController.php new file mode 100644 index 0000000000..908f00311d --- /dev/null +++ b/includes/REST/DokanDataCountriesController.php @@ -0,0 +1,123 @@ +set_woocommerce_rest_check_permissions(); + + return parent::get_items( $request ); + } + + /** + * Return the list of states for a given country. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $this->set_woocommerce_rest_check_permissions(); + return parent::get_item( $request ); + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $this->set_woocommerce_rest_check_permissions(); + $item_permission = $this->get_items_permissions_check( $request ); + + if ( is_wp_error( $item_permission ) ) { + return $item_permission; + } + + return parent::get_item_permissions_check( $request ); + } + + /** + * Check if a given request has access to read items. + * + * @since DOKAN_SINCE + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + // phpcs:ignore WordPress.WP.Capabilities.Unknown + if ( current_user_can( dokan_admin_menu_capability() ) || current_user_can( 'dokandar' ) ) { + return true; + } + + return new WP_Error( + 'dokan_pro_permission_failure', + __( 'You are not allowed to do this action.', 'dokan-lite' ), + [ + 'status' => rest_authorization_required_code(), + ] + ); + } + + private function set_woocommerce_rest_check_permissions() { + add_filter( 'woocommerce_rest_check_permissions', [ $this, 'add_subscriptions_read_permission_to_vendors' ], 10, 4 ); + } + + /** + * Add permissions. + * + * @since DOKAN_PRO_SINCE + * + * @param $permission + * @param $context + * @param $object_id + * @param $obj + * + * @return true + */ + public function add_subscriptions_read_permission_to_vendors( $permission, $context, $object_id, $obj ) { + if ( 'read' === $context ) { + return true; + } + + return $permission; + } +} diff --git a/includes/REST/Manager.php b/includes/REST/Manager.php index ce0d1f936b..ebec1769b1 100644 --- a/includes/REST/Manager.php +++ b/includes/REST/Manager.php @@ -234,6 +234,8 @@ private function get_rest_api_class_map() { DOKAN_DIR . '/includes/REST/StoreSettingControllerV2.php' => '\WeDevs\Dokan\REST\StoreSettingControllerV2', DOKAN_DIR . '/includes/REST/VendorDashboardController.php' => '\WeDevs\Dokan\REST\VendorDashboardController', DOKAN_DIR . '/includes/REST/ProductBlockController.php' => '\WeDevs\Dokan\REST\ProductBlockController', + DOKAN_DIR . '/includes/REST/DokanDataCountriesController.php' => '\WeDevs\Dokan\REST\DokanDataCountriesController', + DOKAN_DIR . '/includes/REST/DokanDataContinentsController.php' => '\WeDevs\Dokan\REST\DokanDataContinentsController', ) ); } From e6bf1ec8897b39a866ac0ba000e129bd1d0f4e49 Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Thu, 24 Oct 2024 10:40:49 +0600 Subject: [PATCH 12/16] update: DokanToaster Support Added --- package.json | 1 + src/Layout/index.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/package.json b/package.json index ceeae0228c..4dd78aab27 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "wp-readme-to-markdown": "^1.0.1" }, "dependencies": { + "@getdokan/dokan-ui": "^1.0.14", "@wordpress/components": "^28.9.0", "@wordpress/data": "^10.9.0", "@wordpress/dom-ready": "^4.9.0", diff --git a/src/Layout/index.tsx b/src/Layout/index.tsx index f71a2f330e..0de5d0ed26 100644 --- a/src/Layout/index.tsx +++ b/src/Layout/index.tsx @@ -6,6 +6,7 @@ import { SlotFillProvider } from '@wordpress/components'; import { PluginArea } from '@wordpress/plugins'; +import { DokanToaster } from "@getdokan/dokan-ui"; // Create a ThemeContext const ThemeContext = createContext( null ); @@ -63,6 +64,7 @@ const Layout = ( { { footerComponent ? footerComponent :
}
+ ); From a064fe761252cdff4703e5ea335250e50355ec54 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:22:05 +0600 Subject: [PATCH 13/16] Add new endpoints to order and product api --- includes/REST/Manager.php | 1 + includes/REST/OrderControllerV3.php | 63 +++++++++++++++++++++++++++++ includes/REST/ProductController.php | 52 ++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 includes/REST/OrderControllerV3.php diff --git a/includes/REST/Manager.php b/includes/REST/Manager.php index 468d37e53a..4637513ac6 100644 --- a/includes/REST/Manager.php +++ b/includes/REST/Manager.php @@ -237,6 +237,7 @@ private function get_rest_api_class_map() { DOKAN_DIR . '/includes/REST/CustomersController.php' => '\WeDevs\Dokan\REST\CustomersController', DOKAN_DIR . '/includes/REST/DokanDataCountriesController.php' => '\WeDevs\Dokan\REST\DokanDataCountriesController', DOKAN_DIR . '/includes/REST/DokanDataContinentsController.php' => '\WeDevs\Dokan\REST\DokanDataContinentsController', + DOKAN_DIR . '/includes/REST/OrderControllerV3.php' => '\WeDevs\Dokan\REST\OrderControllerV3', ) ); } diff --git a/includes/REST/OrderControllerV3.php b/includes/REST/OrderControllerV3.php new file mode 100644 index 0000000000..74167afca9 --- /dev/null +++ b/includes/REST/OrderControllerV3.php @@ -0,0 +1,63 @@ +get_data(); + $downloads = $data['downloads']; + $updated_response = []; + + foreach ( $downloads as $download ) { + $product = dokan()->product->get( $download->product_id ); + $download->product = [ + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'slug' => $product->get_slug(), + 'link' => $product->get_permalink(), + ]; + + /** + * @var $file \WC_Product_Download + */ + $file = $product->get_file( $download->download_id ); + $download->file_data = $file->get_data(); + $download->file_data['file_title'] = wc_get_filename_from_url( $product->get_file_download_path( $download->download_id ) ); + + $updated_response[] = $download; + } + + return rest_ensure_response( $updated_response ); + } +} diff --git a/includes/REST/ProductController.php b/includes/REST/ProductController.php index ee78d383e0..d0f1f970f1 100644 --- a/includes/REST/ProductController.php +++ b/includes/REST/ProductController.php @@ -54,6 +54,58 @@ class ProductController extends DokanRESTController { */ protected $post_status = [ 'publish', 'pending', 'draft' ]; + /** + * Class constructor. + * + * @since DOKAN_SINCE + */ + public function __construct() { + add_filter( "dokan_rest_{$this->post_type}_object_query", [ $this, 'add_only_downloadable_query' ], 10, 2 ); + } + + /** + * Add only downloadable meta query. + * + * @since DOKAN_SINCE + * + * @param array $args + * + * @param \WP_REST_Request $request + */ + public function add_only_downloadable_query( $args, $request ) { + if ( true === dokan_string_to_bool( $request->get_param( 'only_downloadable' ) ) ) { + $args['meta_query'][] = [ + 'key' => '_downloadable', + 'value' => 'yes', + 'compare' => '=', + ]; + } + + return $args; + } + + /** + * Product API query parameters collections. + * + * @since DOKAN_SINCE + * + * @return array Query parameters. + */ + public function get_product_collection_params() { + $schema = parent::get_product_collection_params(); + + $schema['only_downloadable'] = [ + 'description' => __( 'If truthy value then only downloadable products will be returned', 'dokan' ), + 'type' => [ 'boolean', 'string' ], + 'enum' => [ true, false, 0, 1 ], + 'sanitize_callback' => 'dokan_string_to_bool', + 'validate_callback' => 'dokan_string_to_bool', + 'default' => false, + ]; + + return $schema; + } + /** * Register all routes related with stores * From 46bcda59704c2029186949a6ad61ef3ced7cf140 Mon Sep 17 00:00:00 2001 From: Aunshon <32583103+Aunshon@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:18:27 +0600 Subject: [PATCH 14/16] Add register woocommerce script --- includes/Assets.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/Assets.php b/includes/Assets.php index 037716ed8e..48a935ee85 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -2,6 +2,7 @@ namespace WeDevs\Dokan; +use Automattic\WooCommerce\Internal\Admin\WCAdminAssets; use WeDevs\Dokan\Admin\Notices\Helper; use WeDevs\Dokan\ReverseWithdrawal\SettingsHelper; use WeDevs\Dokan\ProductCategory\Helper as CategoryHelper; @@ -877,6 +878,9 @@ public function dokan_dashboard_scripts() { self::load_form_validate_script(); $this->load_gmap_script(); + $wc_instance = WCAdminAssets::get_instance(); + $wc_instance->register_scripts(); + wp_enqueue_script( 'dokan-react-frontend' ); wp_enqueue_style( 'dokan-react-frontend' ); wp_enqueue_script( 'jquery' ); From c8719b59db2579cffd9a4162973e886c73edd64a Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Fri, 6 Dec 2024 11:49:49 +0600 Subject: [PATCH 15/16] update: Vendor dashboard menu override for react templates based on Template override conditions (#2443) * feat: Template detection for react menu rendering * doc: How to define a menu is available in `React` and its `PHP` override information. * doc: Table of content added * refactor: code review suggestion implemented --- docs/feature-override/readme.md | 164 +++++++++ includes/Assets.php | 5 + .../Providers/CommonServiceProvider.php | 4 + includes/VendorNavMenuChecker.php | 214 +++++++++++ src/Dashboard/index.tsx | 51 +-- src/Dashboard/tailwind.scss | 6 + tests/php/src/VendorNavMenuCheckerTest.php | 347 ++++++++++++++++++ 7 files changed, 766 insertions(+), 25 deletions(-) create mode 100644 docs/feature-override/readme.md create mode 100644 includes/VendorNavMenuChecker.php create mode 100644 tests/php/src/VendorNavMenuCheckerTest.php diff --git a/docs/feature-override/readme.md b/docs/feature-override/readme.md new file mode 100644 index 0000000000..95d18e41fc --- /dev/null +++ b/docs/feature-override/readme.md @@ -0,0 +1,164 @@ +# How to define a menu is available in `React` and its `PHP` override information. + +- [Introduction](#introduction) +- [1. Declare a menu is available in `React`.](#1-declare-a-menu-is-available-in-react) + - [Declare `React` menu in **Dokan Lite.**](#declare-react-menu-in-dokan-lite) + - [Declare `React` menu in **Dokan Pro** or **External Plugin**.](#declare-react-menu-in-dokan-pro-or-external-plugin) +- [2. Declare the Override templates for a React route.](#2-declare-the-override-templates-for-a-react-route) + - [Define the override templates for a React route in Dokan Lite.](#define-the-override-templates-for-a-react-route-in-dokan-lite) + - [Define the override templates for a React route in **Dokan Pro** or **External Plugin**.](#define-the-override-templates-for-a-react-route-in-dokan-pro-or-external-plugin) + - [Define the override templates array structure.](#define-the-override-templates-array-structure) +- [Manual Override from External Plugin](#manual-override-from-external-plugin) + +## Introduction +This document will help you to define a menu is available in `React` and its `PHP` override information. + + +## 1. Declare a menu is available in `React`. +To declare a menu is available in `React`, you need to define `route` property during the menu registration. + +### Declare `React` menu in **Dokan Lite**. +```php +// includes/functions-dashboard-navigation.php#L27-L66 +$menus = [ + 'dashboard' => [ + 'title' => __( 'Dashboard', 'dokan-lite' ), + 'icon' => '', + 'url' => dokan_get_navigation_url(), + 'pos' => 10, + 'permission' => 'dokan_view_overview_menu', + 'react_route' => '/', // <-- Define the route here + ], + 'products' => [ + 'title' => __( 'Products', 'dokan-lite' ), + 'icon' => '', + 'url' => dokan_get_navigation_url( 'products' ), + 'pos' => 30, + 'permission' => 'dokan_view_product_menu', + 'react_route' => 'products', // <-- Define the route here + ], + 'orders' => [ + 'title' => __( 'Orders', 'dokan-lite' ), + 'icon' => '', + 'url' => dokan_get_navigation_url( 'orders' ), + 'pos' => 50, + 'permission' => 'dokan_view_order_menu', + 'react_route' => 'orders', // <-- Define the route here + ], + 'withdraw' => [ + 'title' => __( 'Withdraw', 'dokan-lite' ), + 'icon' => '', + 'url' => dokan_get_navigation_url( 'withdraw' ), + 'pos' => 70, + 'permission' => 'dokan_view_withdraw_menu', + ], + 'settings' => [ + 'title' => __( 'Settings', 'dokan-lite' ), + 'icon' => '', + 'url' => dokan_get_navigation_url( 'settings/store' ), + 'pos' => 200, + ], + ]; +``` +In the above example, the `route` property is defined for each menu which we are indicating that the react route is available. +This will be used to determine if the menu is pointing to the react Application or to the Legacy PHP Route. + +The `route` property should be the same as the route defined in the `React` application in Router Array. + +It is important to note that the `route` property should be defined for the menu which is available in the `React` application. +If the `route` key is not defined for the menu, then the menu will be considered as a legacy menu and will be rendered using the PHP template. + + +### Declare `React` menu in **Dokan Pro** or **External Plugin**. + +```php +add_filter( 'dokan_get_dashboard_nav', function ( $menus ) { + $menus['products'] = [ + 'title' => __( 'Products', 'dokan-lite' ), + 'icon' => '', + 'url' => dokan_get_navigation_url( 'products' ), + 'pos' => 30, + 'permission' => 'dokan_view_product_menu', + 'react_route' => 'products', // <-- Define the route here + ]; + + return $menus; +} ); +``` + + +## 2. Declare the Override templates for a React route. +If you are writing a new feature or modifying an existing feature in the `React` application, you do not need to define the override templates for the `React` route. +But if you are modifying or migrating an existing feature written in PHP to the `React` application and you want that if some of the PHP template is overridden by the existing PHP template then the legacy PHP page will be displayed, then you need to define the override templates for the `React` route. +### Define the override templates for a React route in Dokan Lite. +```php +// VendorNavMenuChecker.php#L13-L26 +protected array $template_dependencies = [ + '' => [ + [ 'slug' => 'dashboard/dashboard' ], + [ 'slug' => 'dashboard/orders-widget' ], + [ 'slug' => 'dashboard/products-widget' ], + ], + 'products' => [ + [ 'slug' => 'products/products' ], + [ + 'slug' => 'products/products', + 'name' => 'listing', + ], + ], + ]; +``` + +In the above example, the `template_dependencies` property is defined for each route which we are indicating that the override templates are available for the route. This will be used to determine if the override templates are available for the route or not. + +### Define the override templates for a React route in **Dokan Pro** or **External Plugin**. +From Dokan Pro, we can add dependencies by using the filter `dokan_get_dashboard_nav_template_dependency`. + +```php +add_filter( 'dokan_get_dashboard_nav_template_dependency', function ( array $dependencies ) { + $dependencies['products'] = [ + [ 'slug' => 'products/products' ], + [ + 'slug' => 'products/products', + 'name' => 'listing', + ], + ]; + + return $dependencies; +} ); +``` +### Define the override templates array structure. +```php +/** +* @var array $template_dependencies Array of template dependencies for the route. + */ + +[ + 'route_name' => [ + [ + 'slug' => 'template-slug', + 'name' => 'template-name' // (Optional), + 'args' = [] // (Optional) + ], + ] +] +``` + +- **Slug:** The slug of the template file which is used to display the php content. +- **Name:** The name of the template file which is used to display the php content. (Optional) +- **Args:** The arguments which are passed to the template file in `dokan_get_template_part()` function. (Optional) + +## Manual Override from External Plugin +If you did not override any of the template file directly but you have override functionality by using `add_action` or `add_filter` then you can forcefully override the php route and template rendering for the route by using the `dokan_is_dashboard_nav_dependency_cleared` filter hook. + +```php + +add_filter( 'dokan_is_dashboard_nav_dependency_cleared', function ( $is_cleared, $route ) { + if ( 'products' === $route ) { + return true; + } + + return $is_cleared; +}, 10, 2 ); + +``` diff --git a/includes/Assets.php b/includes/Assets.php index f87825b08a..d567f1e1b2 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -16,6 +16,11 @@ class Assets { public function __construct() { add_action( 'init', [ $this, 'register_all_scripts' ], 10 ); add_filter( 'dokan_localized_args', [ $this, 'conditional_localized_args' ] ); + add_action( + 'wp_footer', function () { + echo '
'; + } + ); if ( is_admin() ) { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ], 10 ); diff --git a/includes/DependencyManagement/Providers/CommonServiceProvider.php b/includes/DependencyManagement/Providers/CommonServiceProvider.php index 03d0fb2858..149aada700 100644 --- a/includes/DependencyManagement/Providers/CommonServiceProvider.php +++ b/includes/DependencyManagement/Providers/CommonServiceProvider.php @@ -3,6 +3,7 @@ namespace WeDevs\Dokan\DependencyManagement\Providers; use WeDevs\Dokan\DependencyManagement\BaseServiceProvider; +use WeDevs\Dokan\VendorNavMenuChecker; class CommonServiceProvider extends BaseServiceProvider { /** @@ -65,5 +66,8 @@ public function register(): void { $this->getContainer() ->addShared( \WeDevs\Dokan\Privacy::class, \WeDevs\Dokan\Privacy::class ) ->addTag( self::TAG ); + $this->getContainer() + ->addShared( VendorNavMenuChecker::class, VendorNavMenuChecker::class ) + ->addTag( self::TAG ); } } diff --git a/includes/VendorNavMenuChecker.php b/includes/VendorNavMenuChecker.php new file mode 100644 index 0000000000..feb21b825a --- /dev/null +++ b/includes/VendorNavMenuChecker.php @@ -0,0 +1,214 @@ + [ ['slug' => 'template-slug', 'name' => 'template-name' (Optional), 'args' = [] (Optional) ] ] ] + */ + protected array $template_dependencies = []; + + /** + * Constructor. + */ + + public function __construct() { + add_filter( 'dokan_get_dashboard_nav', [ $this, 'convert_to_react_menu' ], 999 ); + add_filter( 'dokan_admin_notices', [ $this, 'display_notice' ] ); + } + + /** + * Get template dependencies. + * + * @since DOKAN_SINCE + * + * @return array + */ + public function get_template_dependencies(): array { + return apply_filters( 'dokan_get_dashboard_nav_template_dependency', $this->template_dependencies ); + } + + /** + * Convert menu items to react menu items + * + * @since DOKAN_SINCE + * + * @param array $menu_items Menu items. + * + * @return array + */ + + public function convert_to_react_menu( array $menu_items ): array { + return array_map( + function ( $item ) { + if ( ! empty( $item['react_route'] ) && $this->is_dependency_cleared( $item['react_route'] ) ) { + $item['url'] = $this->get_url_for_route( $item['react_route'] ); + } + if ( isset( $item['submenu'] ) ) { + $item['submenu'] = $this->convert_to_react_menu( $item['submenu'] ); + } + + return $item; + }, $menu_items + ); + } + + /** + * Check if the dependency is cleared or not. + * + * @since DOKAN_SINCE + * + * @param string $route Route. + * + * @return bool + */ + protected function is_dependency_cleared( string $route ): bool { + $clear = true; + $dependencies = $this->get_template_dependencies_resolutions(); + + if ( ! empty( $dependencies[ trim( $route, '/' ) ] ) ) { + $clear = false; + } + + return apply_filters( 'dokan_is_dashboard_nav_dependency_cleared', $clear, $route ); + } + + /** + * Get URL for the route. + * + * @since DOKAN_SINCE + * + * @param string $route Route. + * + * @return string + */ + protected function get_url_for_route( string $route ): string { + $route = apply_filters( 'dokan_get_url_for_react_route', $route ); + + return dokan_get_navigation_url() . '#' . trim( $route, '/' ); + } + + /** + * Get template dependencies resolutions. + * + * @since DOKAN_SINCE + * + * @return array + */ + protected function get_template_dependencies_resolutions(): array { + $dependencies = $this->get_template_dependencies(); + + $resolved_dependencies = array_map( + fn( $dependency_array ): array => array_filter( + array_map( + fn( $dependency ) => $this->get_overridden_template( + $dependency['slug'], + $dependency['name'] ?? '', + $dependency['args'] ?? [] + ), + $dependency_array + ) + ), + $dependencies + ); + + return apply_filters( 'dokan_get_dashboard_nav_template_dependency_resolutions', $resolved_dependencies ); + } + + /** + * Get overridden template part path. + * + * @since DOKAN_SINCE + * + * @param string $slug Template slug. + * @param string $name Template name. + * @param array $args Arguments. + * + * @return false|string Returns the template file if found otherwise false. + */ + protected function get_overridden_template( string $slug, string $name = '', array $args = [] ) { + $defaults = [ 'pro' => false ]; + $args = wp_parse_args( $args, $defaults ); + $template = ''; + $default_template = ''; + + // Look in yourtheme/dokan/slug-name.php and yourtheme/dokan/slug.php + $template_path = ! empty( $name ) ? "{$slug}-{$name}.php" : "{$slug}.php"; + $template = locate_template( [ dokan()->template_path() . $template_path ] ); + + /** + * Change template directory path filter + * + * @since 2.5.3 + */ + $template_path = apply_filters( 'dokan_set_template_path', dokan()->plugin_path() . '/templates', $template, $args ); + + // Get default slug-name.php + if ( ! $template && $name && file_exists( $template_path . "/{$slug}-{$name}.php" ) ) { + $template = $template_path . "/{$slug}-{$name}.php"; + $default_template = $template; + } + + if ( ! $template && ! $name && file_exists( $template_path . "/{$slug}.php" ) ) { + $template = $template_path . "/{$slug}.php"; + $default_template = $template; + } + + // Allow 3rd party plugin filter template file from their plugin + $template = apply_filters( 'dokan_get_template_part', $template, $slug, $name ); + + return $template && $default_template !== $template ? $template : false; + } + + /** + * List overridden templates. + * + * @since DOKAN_SINCE + * + * @return array + */ + public function list_overridden_templates(): array { + $dependencies = $this->get_template_dependencies_resolutions(); + $overridden_templates = []; + foreach ( $dependencies as $dependency ) { + $overridden_templates = array_merge( $overridden_templates, $dependency ); + } + + return $overridden_templates; + } + + /** + * Display notice if templates are overridden. + * + * @since DOKAN_SINCE + * + * @param array $notices Notices. + * + * @return array + */ + public function display_notice( array $notices ): array { + $overridden_templates = $this->list_overridden_templates(); + + if ( empty( $overridden_templates ) ) { + return $notices; + } + + $notice = sprintf( + /* translators: %s: overridden templates */ + __( 'The following templates are overridden:
%s', 'dokan-lite' ), + implode( ',
', $overridden_templates ) + ); + + $notices[] = [ + 'type' => 'alert', + 'title' => esc_html__( 'Some of Dokan Templates are overridden which limit new features.', 'dokan-lite' ), + 'description' => $notice, + ]; + + return $notices; + } +} diff --git a/src/Dashboard/index.tsx b/src/Dashboard/index.tsx index 4bf0eadc89..3706191244 100644 --- a/src/Dashboard/index.tsx +++ b/src/Dashboard/index.tsx @@ -1,41 +1,42 @@ -import {createRoot} from "@wordpress/element"; -import domReady from "@wordpress/dom-ready"; -import Layout from "../Layout"; -import getRoutes, { withRouter } from "../Routing"; -import { - createHashRouter, - RouterProvider, -} from "react-router-dom"; +import { createRoot } from '@wordpress/element'; +import domReady from '@wordpress/dom-ready'; +import Layout from '../Layout'; +import getRoutes, { withRouter } from '../Routing'; +import { createHashRouter, RouterProvider } from 'react-router-dom'; import './tailwind.scss'; const App = () => { const routes = getRoutes(); - const mapedRoutes = routes.map((route) => { + const mapedRoutes = routes.map( ( route ) => { const WithRouterComponent = withRouter( route.element ); return { path: route.path, - element: - - , - } - }); + element: ( + + + + ), + }; + } ); - const router = createHashRouter(mapedRoutes); + const router = createHashRouter( mapedRoutes ); - return ; -} + return ( + <> + + + ); +}; domReady( function () { const rootElement = document.querySelector( '.dashboard-content-area' ); const root = createRoot( rootElement! ); - root.render( - - ); + root.render( ); } ); diff --git a/src/Dashboard/tailwind.scss b/src/Dashboard/tailwind.scss index 0a10673143..09237f265a 100644 --- a/src/Dashboard/tailwind.scss +++ b/src/Dashboard/tailwind.scss @@ -1,2 +1,8 @@ @config './../../base-tailwind.config.js'; @import '../base-tailwind'; + +#headlessui-portal-root { + [role-dialog] { + z-index: 9999; + } +} diff --git a/tests/php/src/VendorNavMenuCheckerTest.php b/tests/php/src/VendorNavMenuCheckerTest.php new file mode 100644 index 0000000000..f54aa17b14 --- /dev/null +++ b/tests/php/src/VendorNavMenuCheckerTest.php @@ -0,0 +1,347 @@ +get( VendorNavMenuChecker::class ); + $this->assertInstanceOf( VendorNavMenuChecker::class, $service ); + } + + /** + * Test that template dependencies are returned. + * + * @test + */ + public function test_that_template_dependencies_are_returned() { + $checker = new VendorNavMenuChecker(); + $dependencies = $checker->get_template_dependencies(); + $this->assertIsArray( $dependencies ); + } + + /** + * Test that menu items are converted to react menu items. + * + * @test + */ + public function test_that_menu_items_are_converted_to_react_menu_items() { + $checker = new VendorNavMenuChecker(); + $menu_items = [ + 'dashboard' => [ + 'route' => 'dashboard', + 'url' => 'http://example.com/dashboard', + 'name' => 'Dashboard', + ], + 'products' => [ + 'route' => 'products', + 'url' => 'http://example.com/products', + 'name' => 'Products', + ], + 'orders' => [ + 'route' => 'orders', + 'url' => 'http://example.com/orders', + 'name' => 'Orders', + ], + ]; + $react_menu_items = $checker->convert_to_react_menu( $menu_items ); + $this->assertIsArray( $react_menu_items ); + $this->assertNotEquals( $menu_items['dashboard']['url'], $react_menu_items['dashboard']['url'] ); + $this->assertNotEquals( $menu_items['products']['url'], $react_menu_items['products']['url'] ); + + add_filter( + 'dokan_get_dashboard_nav_template_dependency', + function ( $template_dependencies ) { + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/dashboard', + 'name' => '', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'big-counter', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'orders', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'products', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'sales-chart', + 'name' => 'widget', + 'args' => [], + ]; + return $template_dependencies; + } + ); + + $react_menu_items = $checker->convert_to_react_menu( $menu_items ); + $this->assertIsArray( $react_menu_items ); + $this->assertNotEquals( $menu_items['dashboard']['url'], $react_menu_items['dashboard']['url'] ); + $this->assertNotEquals( $menu_items['products']['url'], $react_menu_items['products']['url'] ); + } + + /** + * Test that menu items are not converted to react if template is overridden by file. + * + * @test + */ + public function test_that_menu_items_are_not_converted_to_react_if_template_is_overridden_by_file() { + $checker = new VendorNavMenuChecker(); + $menu_items = [ + 'dashboard' => [ + 'route' => 'dashboard', + 'url' => dokan_get_navigation_url(), + 'name' => 'Dashboard', + ], + 'products' => [ + 'route' => 'products', + 'url' => dokan_get_navigation_url( 'products' ), + 'name' => 'Products', + ], + 'orders' => [ + 'route' => 'orders', + 'url' => dokan_get_navigation_url( 'orders' ), + 'name' => 'Orders', + ], + ]; + + add_filter( + 'dokan_get_dashboard_nav_template_dependency', + function ( $template_dependencies ) { + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/dashboard', + 'name' => '', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'big-counter', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'orders', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'products', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'sales-chart', + 'name' => 'widget', + 'args' => [], + ]; + + return $template_dependencies; + } + ); + + $react_menu_items = $checker->convert_to_react_menu( $menu_items ); + $this->assertIsArray( $react_menu_items ); + $this->assertNotEquals( $menu_items['dashboard']['url'], $react_menu_items['dashboard']['url'] ); + $this->assertNotEquals( $menu_items['products']['url'], $react_menu_items['products']['url'] ); + + $theme_dir = wp_get_theme()->get_stylesheet_directory(); + $file = $theme_dir . '/dokan/dashboard/dashboard.php'; + + self::touch( $file ); + + $react_menu_items = $checker->convert_to_react_menu( $menu_items ); + $this->assertIsArray( $react_menu_items ); + $this->assertEquals( $menu_items['dashboard']['url'], $react_menu_items['dashboard']['url'] ); + $this->assertNotEquals( $menu_items['products']['url'], $react_menu_items['products']['url'] ); + + self::unlink( $file ); + } + + /** + * Test that menu items are not converted to react if template is overridden by third party plugin. + * + * @test + */ + public function test_that_menu_items_are_not_converted_to_react_if_template_is_overridden_by_third_party_plugin() { + $checker = new VendorNavMenuChecker(); + $menu_items = [ + 'dashboard' => [ + 'route' => 'dashboard', + 'url' => dokan_get_navigation_url(), + 'name' => 'Dashboard', + ], + 'products' => [ + 'route' => 'products', + 'url' => dokan_get_navigation_url( 'products' ), + 'name' => 'Products', + ], + 'orders' => [ + 'route' => 'orders', + 'url' => dokan_get_navigation_url( 'orders' ), + 'name' => 'Orders', + ], + ]; + + add_filter( + 'dokan_get_dashboard_nav_template_dependency', + function ( $template_dependencies ) { + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/dashboard', + 'name' => '', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'big-counter', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'orders', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'products', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'sales-chart', + 'name' => 'widget', + 'args' => [], + ]; + + return $template_dependencies; + } + ); + + $react_menu_items = $checker->convert_to_react_menu( $menu_items ); + $this->assertIsArray( $react_menu_items ); + $this->assertNotEquals( $menu_items['dashboard']['url'], $react_menu_items['dashboard']['url'] ); + $this->assertNotEquals( $menu_items['products']['url'], $react_menu_items['products']['url'] ); + + $custom_template_file = WP_PLUGIN_DIR . '/dokan-custom/dashboard/dashboard.php'; + + add_filter( + 'dokan_get_template_part', + function ( $template, $slug, $name ) use ( $custom_template_file ) { + if ( 'dashboard/dashboard' === $slug && '' === $name ) { + return $custom_template_file; + } + + return $template; + }, + 10, + 3 + ); + + $react_menu_items = $checker->convert_to_react_menu( $menu_items ); + $this->assertIsArray( $react_menu_items ); + $this->assertEquals( $menu_items['dashboard']['url'], $react_menu_items['dashboard']['url'] ); + $this->assertNotEquals( $menu_items['products']['url'], $react_menu_items['products']['url'] ); + } + + /** + * Test that menu items template dependency is being resolved on template override. + * + * @test + */ + public function test_that_menu_items_template_dependency_is_being_resolved_on_template_override() { + $checker = new VendorNavMenuChecker(); + $menu_items = [ + 'dashboard' => [ + 'route' => 'dashboard', + 'url' => dokan_get_navigation_url(), + 'name' => 'Dashboard', + ], + 'products' => [ + 'route' => 'products', + 'url' => dokan_get_navigation_url( 'products' ), + 'name' => 'Products', + ], + 'orders' => [ + 'route' => 'orders', + 'url' => dokan_get_navigation_url( 'orders' ), + 'name' => 'Orders', + ], + ]; + + add_filter( + 'dokan_get_dashboard_nav_template_dependency', + function ( $template_dependencies ) { + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/dashboard', + 'name' => '', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/big-counter', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/orders', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/products', + 'name' => 'widget', + 'args' => [], + ]; + $template_dependencies['dashboard'][] = [ + 'slug' => 'dashboard/sales-chart', + 'name' => 'widget', + 'args' => [], + ]; + + return $template_dependencies; + } + ); + + $theme_dir = wp_get_theme()->get_stylesheet_directory(); + $products_widget_file = $theme_dir . '/dokan/dashboard/products-widget.php'; + $orders_widget_file = $theme_dir . '/dokan/dashboard/orders-widget.php'; + + self::touch( $products_widget_file ); + self::touch( $orders_widget_file ); + + $custom_template_file = WP_PLUGIN_DIR . '/dokan-custom/dashboard/dashboard.php'; + + add_filter( + 'dokan_get_template_part', + function ( $template, $slug, $name ) use ( $custom_template_file ) { + if ( 'dashboard/dashboard' === $slug && '' === $name ) { + return $custom_template_file; + } + + return $template; + }, + 10, + 3 + ); + + $overridden = $checker->list_overridden_templates(); + + self::unlink( $products_widget_file ); + self::unlink( $orders_widget_file ); + + $this->assertIsArray( $overridden ); + $this->assertContains( $products_widget_file, $overridden ); + $this->assertContains( $orders_widget_file, $overridden ); + $this->assertContains( $custom_template_file, $overridden ); + } +} From 125f1b77db7fa37dc47a5385b186e8b0005a976a Mon Sep 17 00:00:00 2001 From: Shazahanul Islam Shohag Date: Tue, 10 Dec 2024 16:33:26 +0600 Subject: [PATCH 16/16] update: secondary tertiary button support added --- base-tailwind.config.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/base-tailwind.config.js b/base-tailwind.config.js index 2047d53595..4720824467 100644 --- a/base-tailwind.config.js +++ b/base-tailwind.config.js @@ -22,6 +22,16 @@ const baseConfig = { DEFAULT: 'var(--dokan-button-background-color, #F05025)', hover: 'var(--dokan-button-hover-background-color, #F05025)', + secondary: { + DEFAULT: + 'var(--dokan-button-secondary-background-color, var(--dokan-button-text-color))', + hover: 'var(--dokan-button-secondary-hover-background-color, var(--dokan-button-background-color))', + }, + tertiary: { + DEFAULT: + 'var(--dokan-button-tertiary-background-color, #00000000)', + hover: 'var(--dokan-button-tertiary-hover-background-color, var(--dokan-button-text-color))', + }, }, }, }, @@ -34,6 +44,16 @@ const baseConfig = { btn: { DEFAULT: 'var(--dokan-button-text-color, #ffffff)', hover: 'var(--dokan-button-hover-text-color, #ffffff)', + secondary: { + DEFAULT: + 'var(--dokan-button-secondary-text-color, var(--dokan-button-background-color))', + hover: 'var(--dokan-button-secondary-hover-text-color, var(--dokan-button-text-color))', + }, + tertiary: { + DEFAULT: + 'var(--dokan-button-tertiary-text-color, var(--dokan-button-background-color))', + hover: 'var(--dokan-button-tertiary-hover-text-color, var(--dokan-button-background-color))', + }, }, }, }, @@ -42,6 +62,16 @@ const baseConfig = { btn: { DEFAULT: 'var(--dokan-button-border-color, #F05025)', hover: 'var(--dokan-button-hover-border-color, #F05025)', + secondary: { + DEFAULT: + 'var(--dokan-button-secondary-border-color, var(--dokan-button-border-color))', + hover: 'var(--dokan-button-secondary-hover-border-color, var(--dokan-button-border-color))', + }, + tertiary: { + DEFAULT: + 'var(--dokan-button-tertiary-border-color, #00000000)', + hover: 'var(--dokan-button-tertiary-hover-border-color, var(--dokan-button-border-color))', + }, }, }, },