Oracle APEX Drag and Drop: Step-by-Step Guide with Code for Less Than 20 Minute Implementation

 In modern web applications, having interactive elements like drag-and-drop functionality greatly enhances user experience and boosts productivity. In Oracle APEX, implementing this functionality is especially useful for task management systems, where users need to move items between different statuses or categories in a visual interface.

This post will guide you step by step through the process of creating dynamic drag-and-drop elements in Oracle APEX using the powerful Sortable.js library. We'll build a swimlane-style interface that allows users to rearrange tasks across different statuses, with task movements automatically updating both the task status and order using AJAX. You'll also find detailed explanations of the Sortable.js classes used in the process and how they integrate with APEX.

Creating Dynamic Drag and Drop Elements in Oracle APEX (With Code and AJAX Updates)
Creating Dynamic Drag and Drop Elements in Oracle APEX (With Code and AJAX Updates)


By the end of this post, you'll have a fully functioning drag-and-drop interface in your APEX application, along with the knowledge to customize it further. If you feel that a video guideline would help you more, then feel free to check my YouTube video that uses this post to create the Drag and Drop feature in Oracle APEX in less than 20 minutes:) 

Let's get started!

Now, there are some tables that I'd be using throughout this post. I have attached the DDL and DML statements for the same below:



-- DDL for TMS_TASKS
CREATE TABLE TMS_TASKS (
    ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    PROJECT_SPRINT_ID NUMBER NOT NULL,
    PROJECT_UNIQUE_ID NUMBER NOT NULL,
    SHORT_TITLE VARCHAR2(255),
    DESCRIPTION CLOB,
    ESTIMATION NUMBER,
    ASSIGNED_TO VARCHAR2(100),
    REPORTER VARCHAR2(100),
    STATUS_ID NUMBER,
    PRIORITY VARCHAR2(50),
    SEQUENCE_ORDER NUMBER
)
/
-- DML for TMS_TASKS
INSERT INTO TMS_TASKS (PROJECT_SPRINT_ID, PROJECT_UNIQUE_ID, SHORT_TITLE, DESCRIPTION, ESTIMATION, ASSIGNED_TO, REPORTER, STATUS_ID, PRIORITY, SEQUENCE_ORDER) 
VALUES (1, 4, 'Fourth Task: Continuing task creation', 'Fourth Task: Continuing task creation', 1, 'SH', 'SH', 2, 'High', 30)
/
INSERT INTO TMS_TASKS (PROJECT_SPRINT_ID, PROJECT_UNIQUE_ID, SHORT_TITLE, DESCRIPTION, ESTIMATION, ASSIGNED_TO, REPORTER, STATUS_ID, PRIORITY, SEQUENCE_ORDER) 
VALUES (1, 1, 'Initial Task - Creating a TMS', 'This is the first task.', 4, 'JD', 'JD', 1, 'High', 10)
/
INSERT INTO TMS_TASKS (PROJECT_SPRINT_ID, PROJECT_UNIQUE_ID, SHORT_TITLE, DESCRIPTION, ESTIMATION, ASSIGNED_TO, REPORTER, STATUS_ID, PRIORITY, SEQUENCE_ORDER) 
VALUES (1, 2, 'The Second Task', NULL, 3, 'JD', 'SH', 2, 'Low', 20)
/
INSERT INTO TMS_TASKS (PROJECT_SPRINT_ID, PROJECT_UNIQUE_ID, SHORT_TITLE, DESCRIPTION, ESTIMATION, ASSIGNED_TO, REPORTER, STATUS_ID, PRIORITY, SEQUENCE_ORDER) 
VALUES (1, 3, 'Third Task: Continuing task creation', 'Third Task: Continuing task creation...', 3.5, 'JD', 'JD', 2, 'Medium', 10)
/
-- DDL for TMS_PROJECT_STATUS
CREATE TABLE TMS_PROJECT_STATUS (
    ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    NAME VARCHAR2(100) NOT NULL,
    PROJECT_ID NUMBER NOT NULL,
    SEQUENCE_ORDER NUMBER NOT NULL
)
/
-- DML for TMS_PROJECT_STATUS
INSERT INTO TMS_PROJECT_STATUS (NAME, PROJECT_ID, SEQUENCE_ORDER) VALUES ('New', 1, 1)
/
INSERT INTO TMS_PROJECT_STATUS (NAME, PROJECT_ID, SEQUENCE_ORDER) VALUES ('Active', 1, 2)
/
INSERT INTO TMS_PROJECT_STATUS (NAME, PROJECT_ID, SEQUENCE_ORDER) VALUES ('On Hold', 1, 3)
/

We'll start with a blank page like the one below.

Oracle Apex - Blank Page
Fig 1 Blank Page in an Application

Understanding Drag and Drop Functionality in Web Applications

Drag and drop functionality is a key feature in modern web applications, allowing users to interact intuitively with elements on a page.

Making Elements Draggable

To create draggable elements, you need to set the draggable attribute to true. This enables the user to move the element by dragging it with the mouse. For example:

<div class="item" draggable="true">Draggable Item</div>

Making Containers Droppable

Containers can accept draggable elements by utilizing the ondrop and ondragover event handlers. Here are some key points to remember:

  • ondragover: This event handler prevents the default behavior to allow dropping.
  • ondrop: This handles the drop event and places the dragged element into the designated container.

An example of a droppable container is:

<div class="container" ondrop="drop(event)" ondragover="allowDrop(event)"><!-- Drop Target --></div>

Complete Example

Here’s a complete example that combines draggable elements and droppable containers:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Drag and Drop Example</title>
    <style>
        .container { border: 1px solid #ccc; padding: 10px; width: 200px; min-height: 100px; float: left; margin: 10px; }
        .item { margin: 5px; padding: 5px; border: 1px solid #000; background: #f0f0f0; cursor: move; }
    </style>
</head>
<body>
    <div id="container1" class="container" ondrop="drop(event)" ondragover="allowDrop(event)">
        <div id="item1" class="item" draggable="true" ondragstart="drag(event)">Item 1</div>
    </div>
    <div id="container2" class="container" ondrop="drop(event)" ondragover="allowDrop(event)">
        <div id="item2" class="item" draggable="true" ondragstart="drag(event)">Item 2</div>
    </div>

    <script>
        function allowDrop(event) {
            event.preventDefault(); // Necessary to allow dropping
        }

        function drag(event) {
            event.dataTransfer.setData("text", event.target.id); // Stores the ID of the element being dragged
        }

        function drop(event) {
            event.preventDefault();
            const data = event.dataTransfer.getData("text"); // Retrieves the ID of the dragged element
            event.target.appendChild(document.getElementById(data)); // Appends the dragged element to the drop target
        }
    </script>
</body>
</html>

With this setup, elements with the draggable="true" attribute can be moved around, and containers equipped with ondrop and ondragover event handlers can accept these elements.

Utilizing Sortable.js

While the traditional method of implementing drag and drop can be quite verbose, using the Sortable.js library simplifies the process significantly. Sortable.js is a lightweight and flexible library that allows for easy implementation of drag-and-drop features without the need for extensive custom code. It provides a clean and efficient way to manage sortable lists and grids.

Here’s how you can achieve the same functionality using Sortable.js:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sortable Example</title>
    <script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
    <style>
        .container { border: 1px solid #ccc; padding: 10px; width: 200px; min-height: 100px; float: left; margin: 10px; }
        .item { margin: 5px; padding: 5px; border: 1px solid #000; background: #f0f0f0; cursor: move; }
    </style>
</head>
<body>
    <div id="container1" class="container">
        <div id="item1" class="item">Item 1</div>
    </div>
    <div id="container2" class="container">
        <div id="item2" class="item">Item 2</div>
    </div>

    <script>
        new Sortable(document.getElementById('container1'), { group: 'shared', animation: 150 });
        new Sortable(document.getElementById('container2'), { group: 'shared', animation: 150 });
    </script>
</body>
</html>

With this implementation, you can easily handle drag-and-drop functionalities in a much more streamlined manner. The Sortable.js library takes care of the heavy lifting, allowing for more focus on building your application's features.

Integrating Drag and Drop Functionality in Oracle APEX

In this guide, we will explore the steps to integrate a drag-and-drop feature in Oracle APEX and create dynamic swimlanes with associated tasks.

Step 1: Add Sortable.js Library

Add the following Sortable.js library link to the JavaScript – File URL section:

Oracle APEX - Add Sortable JS Library to Page JS File URL
Fig 2. Adding Sortable JS Library to Page JS File URL

https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js

Step 2: Create a Dashboard Region

Create a “Static Content” region titled Dashboard and paste the following HTML code into it:

<div id="requestdashboard" class="swimlanes"></div>

This <div> will hold all the swimlanes together. The user interface (UI) should resemble the example shown below:

Oracle APEX - Static Content rendering
Fig 3. Static Content rendering


Step 3: Populate the Swimlanes

Next, we need to create the swimlanes dynamically. The number of swimlanes and their associated metadata (such as name) may vary based on different projects or inputs. To achieve this, we will utilize RESTful Data Services to fetch the swimlanes for a given product. This requires creating a REST endpoint to provide the necessary content.

Follow these steps:

  1. Ensure that your schema is registered with ORDS. For guidance, refer to the Oracle documentation on accessing RESTful services.
  2. Create a new Module in ORDS. You can modify the parameters as needed, but be mindful that changes will affect the URI structure.
  3. Create Resource Templates under the newly created module. Select your module in the left pane and click the “Create Template” button.
  4. Provide a URI Template. I recommend using getStatus as the template. Set the Priority to 0 and the HTTP Entity Tag Type to Secure Hash.
  5. Create a Handler or Method. Since we will fetch data without sending any inputs in the body (the Project/Sprint ID filter will be sent via the header), select GET.
  6. Click on “Create Handler” and input the following values:
    • Source Type: Collection Query
    • Pagination Size: 10 (Provide the maximum swimlanes that might be present for a given Project/Sprint)
    • SQL Query:
      SELECT 
            name,
            '<div id="'||lower(name)||'swimlane" class="swimlanes__column">
            <div class="column_name">'||name||'</div>
            <ul id="'||lower(name)||'list" class="swimlanes__list jenira_container"></ul>
            </div>' AS code
      FROM tms_project_status
      WHERE project_id = :projectId
      ORDER BY id
    • In the Parameters section, provide the following parameters:
      • Name: projectId
      • Bind Variable: projectId
      • Access Method: IN
      • Source Type: HTTP HEADER
      • Data Type: INTEGER
      • Comments: Gets Project ID to filter Status related to a given project

    Now that we have the endpoint ready, you can test it by copying the full URL and applying the projectId filter in the header.

    Note that the SQL Query includes a column called “Code,” which is structured to incorporate classes for creating droppable containers using Sortable.js.

Rendering Swimlanes in the Dashboard

We will now take the “Code” value and insert it into the static element created earlier. Use the following code in the JavaScript Function and Global Declaration:

const printSwimlane = (swimlanes) => {
        let swimlanesCount = swimlanes.length;
        const dashboardContainer = document.getElementById("requestdashboard");
        for(let i = 0; i < swimlanesCount; i++) {
            dashboardContainer.innerHTML += swimlanes[i]["code"];
        }
    };

    const createSwimlane = async () => {
        const statusUrl = 'replace_this_with_your_url/getStatus?projectId=1';
        try {
            const response = await fetch(statusUrl);
            const data = await response.json();
            printSwimlane(data.items);
        } catch (error) {
            console.error(error);
        }
    };

    createSwimlane();

With this code in place, the UI will now dynamically display the swimlanes as intended.

Oracle APEX - Swimlane Population without UI enhancements
Fig 4. Swimlane Population without UI enhancements

Enhancing the UI with CSS

To improve the UI further, paste the following inline CSS:

/* Mobile-first layout: Swimlanes are vertically stacked by default */
.swimlanes {
    display: flex;
    flex-direction: column;
}

/* The swimlanes__column element groups the heading and the list */
.swimlanes__column {
    min-width: 15rem;
    max-width: fit-content;
    flex: 1;
    background: #F2F2F3; 
    padding: 0.5rem;
    border-radius: 0.5rem;
    margin: 0.5rem;
}

/* Remove ULs inherited margin and padding */
.swimlanes__list {
    padding: 0;
    margin: 0;
    min-height: 90%;
}

/* Style the LIs */
.swimlanes__listItem {
    list-style-type: none;
    margin: 0.5rem 0;
    padding: 1rem;
    cursor: move;
    background: #FFF;
    border: solid blue 0.1px;
    border-radius: 0.25rem;
}

/* Change the flex-direction to row for horizontally stacked swimlanes on larger screens */
@media (min-width: 48rem) {
    .swimlanes {
        flex-direction: row;
        overflow-x: scroll;
    }
}

/* Style the Swimlane header */
.column_name {
    font-weight: bolder;
    font-size: large;
}

With these enhancements, your user interface will look more organized and visually appealing.

Oracle APEX - Swimlane Population with UI enhancements
Fig 5. Swimlane Population with UI enhancements

Step 4: Populate the Tickets

Now, before making the container droppable, let’s populate the tickets as well so that we can test them better. To populate the tickets, let’s follow the same methodology that we used for Swimlane population. We’ll create a new “Resource Template” under the URI Template name – getTickets, and we’ll create a GET handler for the same with the below values.

SQL Query:


SELECT 
      jd.project_unique_id,
      jd.id,
      jd.short_title,
      jd.priority,
      jd.assigned_to,
      js.name,
      lower(js.name) || 'list' AS target,
      '<li id="ticket'||jd.id||'" class="swimlanes__listItem draggable">
          <div class="ticket_number" onclick="">'||jd.project_unique_id||'</div>
          <div class="ticket_name">'||jd.short_title||'</div>
          <div class="item_footer">
              <span class="ticket_priority">'||jd.priority||'</span>
              <span class="ticket_points">'||jd.estimation||'</span>
              <span class="ticket_assignee">'||jd.assigned_to||'</span>
         </div>
      </li>' AS code
FROM 
    TMS_TASKS jd 
LEFT JOIN
    TMS_PROJECT_STATUS js ON jd.status_id = js.id
WHERE 
    project_sprint_id = :sprintId
ORDER BY 
    jd.sequence_order
    

Parameters:

  • Name: sprintId
  • Bind Variable: sprintId
  • Access Method: IN
  • Source Type: HTTP HEADER
  • Data Type: INTEGER
  • Comments: Gets Project ID to filter Status related to a given project

Now, we have the endpoint ready. So, let’s populate ticket data within the Swimlanes. Let’s also replace the createSwimlane() function with the below code to ensure that we start populating tasks after the swimlanes are created:

Updated JavaScript Functions:


const printSwimlane = (swimlanes) => {
    let swimlanesCount = swimlanes.length;
    const dashboardContainer = document.getElementById("requestdashboard");
    for (let i = 0; i < swimlanesCount; i++) {
        dashboardContainer.innerHTML += swimlanes[i]["code"];
    }
};

const createSwimlane = async () => {
    const statusUrl = 'replace_this_with_your_url/getStatus?projectId=1';   
    // Get the Swimlane templates in Ticket Details container
    try {
        const response = await fetch(statusUrl);
        const data = await response.json();
        printSwimlane(data.items);
    } catch (error) {
        console.error(error);
    }
};

const populateSwimlane = (ticketData) => {
    let ticketCount = ticketData.length;
    for (let i = 0; i < ticketCount; i++) {
        document.getElementById(ticketData[i]["target"]).innerHTML += ticketData[i]["code"];
    }
};

const populateTickets = async () => {
    const ticketUrl = 'replace_this_with_your_url/getTickets?sprintId=1';
    // Get Ticket data to populate in the Swimlanes
    try {
        const response = await fetch(ticketUrl);
        const data = await response.json();
        populateSwimlane(data.items);
    } catch (error) {
        console.error(error);
    }
};

Promise.all([createSwimlane()])
    .then(() => {
        // This will be executed only after createSwimlane has resolved
        return populateTickets();
    })
    .catch((error) => {
        console.error(error);
    });
    

UI Preview:

Now, the UI would look like below:

Oracle APEX - Static Swimlane with Tasks
Fig 6. Static Swimlane with Tasks

Now that we have the setup ready, there are only two steps remaining to complete our implementation.

Step 5: Make Tasks Draggable and Containers Droppable

This can be achieved with a small JavaScript function that initializes Sortable.js to make the Swimlane containers "Droppable" and create templates to update tickets dropped into them.

const createDropcontainers = (element) => {
  new Sortable(element, {
    group: 'shared',
    animation: 150,
    onEnd: function (evt) {
      console.log('Ticket Moved');
    },
  });
};

When the container becomes “droppable” through the above Sortable.js initialization, all children within it become “draggable” by default. Let’s ensure all containers created are passed into this function. Below is the updated JavaScript function and global variable declaration:

const createDropcontainers = (element) => {
  new Sortable(element, {
    group: 'shared',
    animation: 150,
    onEnd: function (evt) {
      console.log('Ticket Moved');
    },
  });
};

//This prints the Swimlanes in UI
const printSwimlane = (swimlanes) => {
  let swimlanesCount = swimlanes.length;
  const dashboardContainer = document.getElementById("requestdashboard");
  for (let i = 0; i < swimlanesCount; i++) {
    dashboardContainer.innerHTML += swimlanes[i]["code"];
  }
  let containers = document.getElementsByClassName("jenira_container");
  Array.from(containers).forEach(createDropcontainers);
};

//This Fetches Swimlanes for a given project/sprint
const createSwimlane = async () => {
    const statusUrl = 'replace_this_with_your_url/getStatus?projectId=1';   
    // Get the Swimlane templates in Ticket Details container
    try {
      const response = await fetch(statusUrl);
      const data = await response.json();
      printSwimlane(data.items);
    } catch (error) {
      console.error(error);
    }
};
//This populates Swimlanes with Tasks/Tickets
const populateSwimlane = (ticketData) => {
    let ticketCount = ticketData.length;
    for (let i = 0; i < ticketCount; i++) {
      document.getElementById(ticketData[i]["target"]).innerHTML += ticketData[i]["code"];
    }
};
//This gets the Ticket details for a given project
const populateTickets = async () => {
    const ticketUrl = 'replace_this_with_your_url/getTickets?sprintId=1';
    // Get Ticket data to populate in the Swimlanes
    try {
      const response = await fetch(ticketUrl);
      const data = await response.json();
      populateSwimlane(data.items);
    } catch (error) {
      console.error(error);
    }
};
//We will create swimlanes first and then populate it with tasks next
Promise.all([createSwimlane()])
  .then(() => {
    return populateTickets();
  })
  .catch((error) => {
    console.error(error);
  });

Now, we have the working drag and drop feature ready! 😊

Step 6: Update Database on Task Movement

Currently, changes made when moving a task are not saved after a page refresh. To enhance user experience, we need to implement AJAX functions to update the database whenever a task is moved across lanes. We will create an Application Process for updating ticket status and a page-level AJAX process for updating ticket sequences.

Note: It's recommended to create AJAX processes at the page level instead of in the Application process. This example follows the Application process for exploration purposes.

Create the Application Process

Navigate to Shared Components > Application Process and click “Create.” Provide the following values:

  • Sequence: 1
  • Process Point: Ajax Callback: Run this application process when requested by a page process.
  • Name: UPDATE TICKET STATUS
  • Type: PL/SQL
  • Execute Code:
BEGIN
    UPDATE tms_tasks
    SET status_id = (SELECT id FROM tms_project_status WHERE lower(name) = APEX_APPLICATION.g_x01)
    WHERE id = APEX_APPLICATION.g_x02;
END;

Create a Page-Level AJAX Process

Navigate to the page designer of the dashboard page and go to “Process” to create an “AJAX Process” with the following values:

Name: UPDATE TICKET SEQUENCE 

PL/SQL Code:
 
DECLARE
  v_status_id tms_project_status.id%TYPE;
  TYPE t_ticket IS RECORD (
    id tms_tasks.id%TYPE,
    sequence tms_tasks.sequence_order%TYPE
  );
  l_ticket t_ticket;
  l_ticket_ids APEX_APPLICATION_GLOBAL.VC_ARR2;
  l_sequences APEX_APPLICATION_GLOBAL.VC_ARR2;
BEGIN
  SELECT id INTO v_status_id
  FROM tms_project_status
  WHERE lower(name) = APEX_APPLICATION.g_x01;

  l_ticket_ids := APEX_APPLICATION.G_F01;
  l_sequences := APEX_APPLICATION.G_F02;

  FOR i IN 1..l_ticket_ids.COUNT LOOP
    l_ticket.id := TO_NUMBER(l_ticket_ids(i));
    l_ticket.sequence := TO_NUMBER(l_sequences(i));

    UPDATE tms_tasks
    SET sequence_order = l_ticket.sequence
    WHERE id = l_ticket.id;
  END LOOP;
END;

Now, let’s implement the JavaScript functions to update the status and sequence when tasks are moved:

// This function updates the ticket status once it is moved across lanes
const updateStatus = (element, currStatus) => {
    let id = element.replace('ticket', '');
    let status = currStatus.replace('list', '');
    apex.server.process("UPDATE TICKET STATUS", {
      x01: status,
      x02: id
    }, {
      dataType: 'text',
      success: function() {
        // Status updated successfully
      }
    });
}

// This function updates the sequence of all tickets within a lane
const updateSequence = (currentSeq, currStatus) => {
    const status = currStatus.replace('list', '');
    const sequenceData = currentSeq.map(item => ({
      id: item.id.replace('ticket', ''),
      sequence: (item.index + 1) * 10
    }));
    
    apex.server.process("UPDATE TICKET SEQUENCE", {
      x01: status,
      f01: sequenceData.map(item => item.id),
      f02: sequenceData.map(item => item.sequence)
    }, {
      dataType: 'text',
      success: function() {
        // Sequence updated successfully
      }
    });
};

Finally, let’s combine everything in the drop container function:

const createDropcontainers = (element) => {
  new Sortable(element, {
    group: 'shared',
    animation: 150,
    onEnd: function (evt) {
      const currentSeq = Array.from(evt.to.children).map((item, index) => ({ index, id: item.id }));
      updateStatus(evt.item.id, evt.item.parentNode.id);
      updateSequence(currentSeq, evt.item.parentNode.id);
    },
  });
};

The final code in the Function and Global Value declaration would look as follows:

//This function is used to update Status of the tasks when moved between lanes
const updateStatus = (element, currStatus) => {
    let id = element.replace('ticket','');
    let status = currStatus.replace('list','');
    apex.server.process("UPDATE TICKET STATUS", {
    x01: status,
    x02: id
  }, {
    dataType: 'text',
    success: function() {
      //console.log('Status Updated!');
    }
  });
}

// This function is used to update the Sequence of all tickets within a lane 
const updateSequence = (currentSeq, currStatus) => {
  const status = currStatus.replace('list', '');
  const sequenceData = currentSeq.map(item => ({
    id: item.id.replace('ticket', ''),
    sequence: (item.index + 1) * 10
  }));
  
  apex.server.process("UPDATE TICKET SEQUENCE", {
    x01: status,
    f01: sequenceData.map(item => item.id),
    f02: sequenceData.map(item => item.sequence)
  }, {
    dataType: 'text',
    success: function() {
      //console.log('Sequence Updated!');
    }
  });
};
// This function is used to make the Swimlane containers "Droppable" 
const createDropcontainers = (element) => {
  new Sortable(element, {
    group: 'shared',
    animation: 150,
    onEnd: function (evt) {
      const currentSeq = Array.from(evt.to.children).map((item, index) => ({ index, id: item.id }));
      updateStatus(evt.item.id, evt.item.parentNode.id);
      updateSequence(currentSeq, evt.item.parentNode.id);
    },
  });
};

const printSwimlane = (swimlanes) => {
        let swimlanesCount = swimlanes.length;
        const dashboardContainer = document.getElementById("requestdashboard");
        for(let i=0;i<swimlanesCount;i++){
            dashboardContainer.innerHTML += swimlanes[i]["code"];
        }
        let containers = document.getElementsByClassName("jenira_container");
        Array.from(containers).forEach(createDropcontainers);
    };

const createSwimlane = async () => {
    const statusUrl = 'replace_this_with_your_url/getStatus?projectId=1';   
    try {
        const response=await fetch(statusUrl);
        const data=await response.json();
        printSwimlane(data.items);
    } 
    catch(error) {
        console.error(error);
    }
};

const populateSwimlane = (ticketData) => {
    let ticketCount = ticketData.length;
    for(let i=0;i<ticketCount;i++){
        document.getElementById(ticketData[i]["target"]).innerHTML += ticketData[i]["code"];
    }
};

const populateTickets = async () => {
    const ticketUrl = 'replace_this_with_your_url/getTickets?sprintId=1';
    try {
        const response=await fetch(ticketUrl);
        const data=await response.json();
        populateSwimlane(data.items);
    } 
    catch(error) {
        console.error(error);
    }
};

Promise.all([createSwimlane()])
  .then(() => {
    return populateTickets();
  })
  .catch((error) => {
    console.error(error);
  });
  

Now, the drag-and-drop feature is fully operational!

Additional CSS for Enhanced UI

/* mobile-first layout: Swimlanes are vertically stacked to begin with. */
.swimlanes {
  display: flex;
  flex-direction: column;
}

/* The swimlanes__column element groups the heading and the list */
.swimlanes__column {
  min-width: 15rem; /* this is what makes the concept legible when there are many swimlanes */
  max-width: fit-content;
  flex: 1;  /* to keep everything equally sized */
  background: #F2F2F3;  /* replace with casper color variable */
  padding: 0.5rem;
  border-radius: 0.5rem;
  margin: 0.5rem;
}

/* nuke the ULs inherited margin and padding */
.swimlanes__list {
  padding: 0;
  margin: 0;
  min-height: 90%;
}

/* overwrite the LIs inherited list-style-type, margin and padding and add background and border-radius */
.swimlanes__listItem {
  list-style-type: none;
  margin: 0.5rem 0;
  padding: 1rem;
  cursor: move;
  background: #FFF;
  border: solid blue 0.1px;
  border-radius: 0.25rem;
}

/* From 48rem and up we switch the flex-direction to row for horizontally stacked swimlanes */
@media (min-width: 48rem) {
  .swimlanes {
    flex-direction: row;
    overflow-x: scroll;
  }
}

/*This is to style the Swimlane's header*/
.column_name{
    font-weight: bolder;
    font-size: large;
}

/* Style for the container div */
.ticket-details-container {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

/* Style for the ticket number */
.ticket_number {
  font-size: 18px;
  font-weight: bold;
  color: #1422dd;
  cursor: pointer;
  text-decoration: underline;
}

/* Style for the ticket name */
.ticket_name {
  font-size: 16px;
  margin-top: 5px;
  color: #555;
}

/* Style for the item footer */
.item_footer {
  margin-top: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* Style for ticket priority, points, and assignee */
.ticket_priority,
.ticket_points,
.ticket_assignee {
  padding: 5px 10px;
  border-radius: 5px;
}

.ticket_priority {
  background-color: #e74c3c; /* Red for priority */
  color: #fff;
}

.ticket_points {
  background-color: #3498db; /* Blue for points */
  color: #fff;
}

.ticket_assignee {
  background-color: #2ecc71; /* Green for assignee */
  color: #fff;
}

Now, your UI should look like this:

Oracle APEX - Drag and Drop Tasks in Swimlane
Fig 7. Oracle APEX - Drag and Drop Tasks in Swimlane


And that’s it! You have successfully created dynamic drag-and-drop functionality in your Oracle APEX application using Sortable.js. Feel free to customize the design further as needed. 

If you've made it this far, I truly hope you found this content helpful and informative. It takes a lot of time and effort to create these guides, and your support would mean the world to me. Please consider subscribing to my YouTube channel – Into the Oracle Verse – where I share even more in-depth tutorials and insights on Oracle APEX, SQL, and PL/SQL. Your suggestions and feedback are always welcome to help improve future content. Thank you, and I look forward to connecting with you again on our next topic!

Good luck!

data:post.title

Oracle APEX Drag and Drop: Step-by-Step Guide with Code for Less Than 20 Minute Implementation

Written by JENISH JOYAL J

Published on October 19, 2024

No comments:

Powered by Blogger.