Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions README ENG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Check List Plugin for Redmine

**Check List Plugin** is a plugin that extends Redmine issue management capabilities and allows you to add checklist items to each ticket.

## Table of Contents

- [Features](#Features)
- [Installation](#Installation)
- [Steps](#Steps)
- [Supported-versions](#Supported-versions)
- [Screenshots](#Screenshots)
- [License](#License)

## Features

- **Add checklists**: Add multiple checklist items to each issue.
- **Hierarchical structure**: Create subtasks within checklist items for hierarchical management.
- **Reordering**: Organize checklist items using drag and drop.

## Installation

### Steps

1. **Download the plugin**

Find the Redmine plugin directory and clone it from GitHub.
```bash
cd /path/to/redmine/plugins
git clone https://github.com/shnri/redmine_plugin_checklist.git

2. **Build**

Execute the build in the plugin directory.
```bash
cd /path/to/redmine/plugins/check_list
npm run build

3. **Install dependencies**(à vérifier si nécessaire)

Install required dependencies using Bundler.
```bash
bundle install

4. **Precompiling assets**

Go to the Redmine root directory and precompile the assets.
```bash
cd /path/to/redmine/
bundle exec rake redmine:plugins:assets RAILS_ENV=production

5. **Database migration**

Run the database migration.
```bash
bundle exec rake redmine:plugins:migrate RAILS_ENV=production

6. **Set permissions**(à vérifier si nécessaire)

Ensure proper permissions are set for the plugin directories.
```bash
chown -R redmine:redmine /path/to/redmine/plugins/check_list

7. **Restart Redmine**

Restart the server to apply the changes.
```bash
systemctl restart redmine

### Supported-versions

- **Redmine version**: `5.1.3`
- **Ruby version**: `3.1.6`

## Screenshots

![Check List](https://github.com/shnri/redmine_plugin_checklist/blob/master/img/checklist.png)

## License
This plugin is released under the MIT license. For more details, refer to the LICENSE file.
141 changes: 141 additions & 0 deletions app/controllers/checklist_items_controller ENG.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
class ChecklistItemsController < ApplicationController
before_action :find_issue
before_action :find_checklist_item, only: [:update, :destroy], except: [:bulk_update, :create]

# Retrieve checklist items (GET)
def index
@checklist_items = ChecklistItem.where(issue_id: @issue.id)
render json: build_task_tree(@checklist_items)
end

# Create a checklist item (POST)
def create
# Check if the root element exists
root_item = ChecklistItem.find_by(task_id: 'root', issue_id: @issue.id)

# Create root if it does not exist
unless root_item
root_item = ChecklistItem.create!(
task_id: 'root',
issue_id: params[:issue_id],
label: 'root',
checked: false,
is_layer_open: true
)
end

checklist_item = ChecklistItem.new(checklist_item_params)
checklist_item.issue_id = @issue.id
if checklist_item.save
render json: { message: "Checklist item created successfully", task_id: checklist_item.task_id }, status: :created
else
render json: { errors: checklist_item.errors.full_messages }, status: :unprocessable_entity
end
end

# Update a checklist item (PUT/PATCH)
def update
if @checklist_item.update(checklist_item_params)
render json: { message: "Checklist item updated successfully" }, status: :ok
else
render json: { errors: @checklist_item.errors.full_messages }, status: :unprocessable_entity
end
end

# Update multiple checklist items at once (PUT)
def bulk_update
Rails.logger.debug "Starting bulk_update with params: #{params.inspect}"

errors = []

checklist_items_params.each do |item_params|
Rails.logger.debug "Processing item_params: #{item_params.inspect}"
begin
checklist_item = ChecklistItem.find_by!(task_id: item_params[:task_id], issue_id: @issue.id)

unless checklist_item.update(
label: item_params[:label],
checked: item_params[:checked],
is_layer_open: item_params[:is_layer_open],
position: item_params[:position],
parent_id: item_params[:parent_id]
)
errors << { task_id: checklist_item.task_id, errors: checklist_item.errors.full_messages }
end
rescue ActiveRecord::RecordNotFound => e
errors << { task_id: item_params[:task_id], errors: ["Item not found"] }
end
end

if errors.empty?
render json: { message: "Checklist items updated successfully" }, status: :ok
else
render json: { message: "Some checklist items failed to update", errors: errors }, status: :unprocessable_entity
end
end

# Delete a checklist item (DELETE)
def destroy
if @checklist_item.destroy
render json: { message: "Checklist item deleted successfully" }, status: :ok
else
render json: { errors: "Failed to delete checklist item" }, status: :unprocessable_entity
end
end

private

# Parameter permission method for a single item
def checklist_item_params
permitted = params.require(:checklist_item).permit(:taskId, :label, :checked, :isLayerOpen, :position, :parentId)
{
task_id: permitted['taskId'],
label: permitted['label'],
checked: permitted['checked'],
is_layer_open: permitted['isLayerOpen'],
position: permitted['position'],
parent_id: permitted['parentId'],
issue_id: params['issue_id']
}
end

# Parameter permission method for multiple items
def checklist_items_params
params.require(:checklist_items).require(:data).map do |item|
permitted = item.permit(:taskId, :label, :checked, :isLayerOpen, :position, :parentId)
{
task_id: permitted['taskId'],
label: permitted['label'],
checked: permitted['checked'],
is_layer_open: permitted['isLayerOpen'],
position: permitted['position'],
parent_id: permitted['parentId'],
issue_id: params['issue_id']
}
end
end

# Find the related issue
def find_issue
@issue = Issue.find(params[:issue_id])
end

# Find the checklist item
def find_checklist_item
@checklist_item = ChecklistItem.find_by!(task_id: params[:id], issue_id: @issue.id)
end

# Build hierarchical structure
def build_task_tree(items, parent_id = nil)
items.select { |item| item.parent_id == parent_id }.map do |item|
{
taskId: item.task_id,
label: item.label,
checked: item.checked,
isLayerOpen: item.is_layer_open,
position: item.position,
children: build_task_tree(items, item.task_id)
}
end
end
end
80 changes: 80 additions & 0 deletions app/javascript/components/AddTask ENG.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useState } from "react";
import { TaskTree } from "../types";
import { updateTaskTree } from "../utils/taskTreeActions";
import { rootId } from "./TaskList";
import { useTaskTree } from "./TaskTreeProvider";
import { createTask } from "../utils/apiHelper";
import { v4 as uuidv4 } from "uuid";

const initTask = (): TaskTree => ({
taskId: uuidv4(),
label: "",
checked: false,
isLayerOpen: false,
position: 0,
children: [],
});

const AddTask: React.FC<{
taskTree: TaskTree;
}> = ({ taskTree: myTaskTree }) => {
const { setTaskTree } = useTaskTree();
const [newTaskLabel, setNewTaskLabel] = useState<string>("");

return (
<div
className={
"my-2 mr-[40px] " +
(myTaskTree.taskId === rootId ? "ml-[4.5rem]" : "ml-[6.5rem]")
}
>
<input
type="text"
value={newTaskLabel}
className="
content_placeholder flex-1
break-all
px-[8px]
h-8
rounded-[6px]
border-0
outline-none
text-[14px]
size-full
bg-gray-200"
onChange={(e) => setNewTaskLabel(e.target.value)}
placeholder={
myTaskTree.taskId === rootId
? "Add a checklist item..."
: "Add a sub-checklist item..."
}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
// Add a task when pressing Enter
const newTask = {
...initTask(),
position:
myTaskTree.children.length > 0
? Math.max(
...myTaskTree.children.map((item) => item.position)
) + 1
: 0,
label: newTaskLabel.trim(),
};
if (newTaskLabel.trim()) {
updateTaskTree(setTaskTree, myTaskTree.taskId, "children", [
...myTaskTree.children,
newTask,
]);
setNewTaskLabel("");
createTask(setTaskTree, newTask, myTaskTree.taskId);
}
}
}}
/>
</div>
);
};

export default AddTask;
Loading