Editable tasks and project labels
This commit is contained in:
parent
034bc0e90b
commit
6e0ccd5575
|
@ -0,0 +1,56 @@
|
|||
import { FunctionalComponent, h, Component, ComponentChild, Fragment } from "preact";
|
||||
import { Link } from "preact-router/match";
|
||||
import * as style from "./style.css";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
|
||||
export interface EditableTextProps {
|
||||
value: string
|
||||
class?: string
|
||||
editIconClass?: string
|
||||
onChange?: (value: string) => void
|
||||
render: (value: string) => ComponentChild
|
||||
}
|
||||
|
||||
const EditableText: FunctionalComponent<EditableTextProps> = ({ onChange, value, render, ...props }) => {
|
||||
const [ internalValue, setInternalValue ] = useState(value);
|
||||
const [ editMode, setEditMode ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
|
||||
const onEditIconClick = () => {
|
||||
setEditMode(true);
|
||||
};
|
||||
|
||||
const onValidateButtonClick = () => {
|
||||
setEditMode(false);
|
||||
}
|
||||
|
||||
const onValueChange = (evt: Event) => {
|
||||
const currentTarget = evt.currentTarget as HTMLInputElement;
|
||||
setInternalValue(currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={`${style.editableText} ${props.class ? props.class : ''}`}>
|
||||
{
|
||||
editMode ?
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input class="input is-expanded" type="text" value={internalValue} onChange={onValueChange} />
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button" onClick={onValidateButtonClick}>✔️</a>
|
||||
</div>
|
||||
</div> :
|
||||
<Fragment>
|
||||
{ render(internalValue) }
|
||||
<i class={`${style.editIcon} icon ${props.editIconClass ? props.editIconClass : ''}`} onClick={onEditIconClick}>🖋️</i>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableText;
|
|
@ -0,0 +1,17 @@
|
|||
.editableText {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.editableText > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.editIcon {
|
||||
visibility: hidden;
|
||||
margin-left: 0.25em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editableText:hover > .editIcon {
|
||||
visibility: visible;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// This file is automatically generated from your CSS. Any edits will be overwritten.
|
||||
export const editableText: string;
|
||||
export const editIcon: string;
|
|
@ -7,9 +7,11 @@ export interface Action {
|
|||
}
|
||||
|
||||
export type ProjectReducerActions =
|
||||
AddTaskAction |
|
||||
RemoveTaskAction |
|
||||
UpdateTaskEstimation
|
||||
AddTask |
|
||||
RemoveTask |
|
||||
UpdateTaskEstimation |
|
||||
UpdateProjectLabel |
|
||||
UpdateTaskLabel
|
||||
|
||||
export function useProjectReducer(project: Project) {
|
||||
return useReducer(projectReducer, project);
|
||||
|
@ -18,7 +20,36 @@ export function useProjectReducer(project: Project) {
|
|||
export function projectReducer(project: Project, action: ProjectReducerActions): Project {
|
||||
switch(action.type) {
|
||||
case ADD_TASK:
|
||||
const task = { ...(action as AddTaskAction).task };
|
||||
return handleAddTask(project, action as AddTask);
|
||||
|
||||
case REMOVE_TASK:
|
||||
return handleRemoveTask(project, action as RemoveTask);
|
||||
|
||||
case UPDATE_TASK_ESTIMATION:
|
||||
return handleUpdateTaskEstimation(project, action as UpdateTaskEstimation);
|
||||
|
||||
case UPDATE_PROJECT_LABEL:
|
||||
return handleUpdateProjectLabel(project, action as UpdateProjectLabel);
|
||||
|
||||
case UPDATE_TASK_LABEL:
|
||||
return handleUpdateTaskLabel(project, action as UpdateTaskLabel);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
export interface AddTask extends Action {
|
||||
task: Task
|
||||
}
|
||||
|
||||
export const ADD_TASK = "ADD_TASK";
|
||||
|
||||
export function addTask(task: Task): AddTask {
|
||||
return { type: ADD_TASK, task };
|
||||
}
|
||||
|
||||
export function handleAddTask(project: Project, action: AddTask): Project {
|
||||
const task = { ...action.task };
|
||||
return {
|
||||
...project,
|
||||
tasks: {
|
||||
|
@ -26,59 +57,25 @@ export function projectReducer(project: Project, action: ProjectReducerActions):
|
|||
[task.id]: task,
|
||||
}
|
||||
};
|
||||
case REMOVE_TASK:
|
||||
action = action as RemoveTaskAction;
|
||||
}
|
||||
|
||||
export interface RemoveTask extends Action {
|
||||
id: TaskID
|
||||
}
|
||||
|
||||
export const REMOVE_TASK = "REMOVE_TASK";
|
||||
|
||||
export function removeTask(id: TaskID): RemoveTask {
|
||||
return { type: REMOVE_TASK, id };
|
||||
}
|
||||
|
||||
export function handleRemoveTask(project: Project, action: RemoveTask): Project {
|
||||
const tasks = { ...project.tasks };
|
||||
delete tasks[action.id];
|
||||
return {
|
||||
...project,
|
||||
tasks
|
||||
};
|
||||
case UPDATE_TASK_ESTIMATION:
|
||||
action = action as UpdateTaskEstimation;
|
||||
const estimations = {
|
||||
...project.tasks[action.id].estimations,
|
||||
[(action as UpdateTaskEstimation).confidence]: (action as UpdateTaskEstimation).value
|
||||
};
|
||||
if (estimations.likely <= estimations.optimistic) {
|
||||
estimations.likely = estimations.optimistic + 1;
|
||||
}
|
||||
if (estimations.pessimistic <= estimations.likely) {
|
||||
estimations.pessimistic = estimations.likely + 1;
|
||||
}
|
||||
return {
|
||||
...project,
|
||||
tasks: {
|
||||
...project.tasks,
|
||||
[action.id]: {
|
||||
...project.tasks[action.id],
|
||||
estimations: estimations,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
export interface AddTaskAction extends Action {
|
||||
task: Task
|
||||
}
|
||||
|
||||
export const ADD_TASK = "ADD_TASK";
|
||||
|
||||
export function addTask(task: Task): AddTaskAction {
|
||||
return { type: ADD_TASK, task };
|
||||
}
|
||||
|
||||
export interface RemoveTaskAction extends Action {
|
||||
id: TaskID
|
||||
}
|
||||
|
||||
export const REMOVE_TASK = "REMOVE_TASK";
|
||||
|
||||
export function removeTask(id: TaskID): RemoveTaskAction {
|
||||
return { type: REMOVE_TASK, id };
|
||||
}
|
||||
|
||||
export interface UpdateTaskEstimation extends Action {
|
||||
|
@ -92,3 +89,71 @@ export const UPDATE_TASK_ESTIMATION = "UPDATE_TASK_ESTIMATION";
|
|||
export function updateTaskEstimation(id: TaskID, confidence: EstimationConfidence, value: number): UpdateTaskEstimation {
|
||||
return { type: UPDATE_TASK_ESTIMATION, id, confidence, value };
|
||||
}
|
||||
|
||||
export function handleUpdateTaskEstimation(project: Project, action: UpdateTaskEstimation): Project {
|
||||
const estimations = {
|
||||
...project.tasks[action.id].estimations,
|
||||
[action.confidence]: action.value
|
||||
};
|
||||
|
||||
if (estimations.likely <= estimations.optimistic) {
|
||||
estimations.likely = estimations.optimistic + 1;
|
||||
}
|
||||
|
||||
if (estimations.pessimistic <= estimations.likely) {
|
||||
estimations.pessimistic = estimations.likely + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
...project,
|
||||
tasks: {
|
||||
...project.tasks,
|
||||
[action.id]: {
|
||||
...project.tasks[action.id],
|
||||
estimations: estimations,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface UpdateProjectLabel extends Action {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const UPDATE_PROJECT_LABEL = "UPDATE_PROJECT_LABEL";
|
||||
|
||||
export function updateProjectLabel(label: string): UpdateProjectLabel {
|
||||
return { type: UPDATE_PROJECT_LABEL, label };
|
||||
}
|
||||
|
||||
export function handleUpdateProjectLabel(project: Project, action: UpdateProjectLabel): Project {
|
||||
return {
|
||||
...project,
|
||||
label: action.label
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateTaskLabel extends Action {
|
||||
id: TaskID
|
||||
label: string
|
||||
}
|
||||
|
||||
export const UPDATE_TASK_LABEL = "UPDATE_TASK_LABEL";
|
||||
|
||||
export function updateTaskLabel(id: TaskID, label: string): UpdateTaskLabel {
|
||||
return { type: UPDATE_TASK_LABEL, id, label };
|
||||
}
|
||||
|
||||
export function handleUpdateTaskLabel(project: Project, action: UpdateTaskLabel): Project {
|
||||
return {
|
||||
...project,
|
||||
tasks: {
|
||||
...project.tasks,
|
||||
[action.id]: {
|
||||
...project.tasks[action.id],
|
||||
label: action.label,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -5,10 +5,11 @@ import { newProject } from "../../models/project";
|
|||
import TaskTable from "./tasks-table";
|
||||
import TimePreview from "./time-preview";
|
||||
import FinancialPreview from "./financial-preview";
|
||||
import { useProjectReducer, addTask, updateTaskEstimation, removeTask } from "../../hooks/use-project-reducer";
|
||||
import { useProjectReducer, addTask, updateTaskEstimation, removeTask, updateProjectLabel, updateTaskLabel } from "../../hooks/use-project-reducer";
|
||||
import { Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||
import { getProjectStorageKey } from "../../util/storage";
|
||||
import { useLocalStorage } from "../../hooks/use-local-storage";
|
||||
import EditableText from "../../components/editable-text";
|
||||
|
||||
export interface ProjectProps {
|
||||
projectId: string
|
||||
|
@ -27,10 +28,18 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
|||
dispatch(removeTask(taskId));
|
||||
}
|
||||
|
||||
const onTaskLabelUpdate = (taskId: TaskID, label: string) => {
|
||||
dispatch(updateTaskLabel(taskId, label));
|
||||
}
|
||||
|
||||
const onEstimationChange = (taskId: TaskID, confidence: EstimationConfidence, value: number) => {
|
||||
dispatch(updateTaskEstimation(taskId, confidence, value));
|
||||
};
|
||||
|
||||
const onProjectLabelChange = (projectLabel: string) => {
|
||||
dispatch(updateProjectLabel(projectLabel));
|
||||
};
|
||||
|
||||
// Save project in local storage on change
|
||||
useEffect(()=> {
|
||||
storeProject(project);
|
||||
|
@ -38,11 +47,11 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
|||
|
||||
return (
|
||||
<div class={`container ${style.estimation}`}>
|
||||
<h3 class="is-size-3">
|
||||
{project.label ? project.label : "Projet sans nom"}
|
||||
|
||||
<i class="icon is-size-4">🖋️</i>
|
||||
</h3>
|
||||
<EditableText
|
||||
editIconClass="is-size-4"
|
||||
render={(value) => (<h3 class="is-size-3">{value}</h3>)}
|
||||
onChange={onProjectLabelChange}
|
||||
value={project.label ? project.label : "Projet sans nom"} />
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li class="is-active">
|
||||
|
@ -65,6 +74,7 @@ const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
|
|||
project={project}
|
||||
onTaskAdd={onTaskAdd}
|
||||
onTaskRemove={onTaskRemove}
|
||||
onTaskLabelUpdate={onTaskLabelUpdate}
|
||||
onEstimationChange={onEstimationChange} />
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useState, useEffect } from "preact/hooks";
|
|||
import * as style from "./style.css";
|
||||
import { Project } from "../../models/project";
|
||||
import { newTask, Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||
import EditableText from "../../components/editable-text";
|
||||
|
||||
|
||||
|
||||
|
@ -11,11 +12,12 @@ export interface TaskTableProps {
|
|||
onTaskAdd: (task: Task) => void
|
||||
onTaskRemove: (taskId: TaskID) => void
|
||||
onEstimationChange: (taskId: TaskID, confidence: EstimationConfidence, value: number) => void
|
||||
onTaskLabelUpdate: (taskId: TaskID, label: string) => void
|
||||
}
|
||||
|
||||
export type EstimationTotals = { [confidence in EstimationConfidence]: number }
|
||||
|
||||
const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, onEstimationChange, onTaskRemove }) => {
|
||||
const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, onEstimationChange, onTaskRemove, onTaskLabelUpdate }) => {
|
||||
|
||||
const defaultTaskCategory = Object.keys(project.params.taskCategories)[0];
|
||||
const [ task, setTask ] = useState(newTask("", defaultTaskCategory));
|
||||
|
@ -39,16 +41,20 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
setTotals({ optimistic, likely, pessimistic });
|
||||
}, [project.tasks]);
|
||||
|
||||
const onTaskLabelChange = (evt: Event) => {
|
||||
const onNewTaskLabelChange = (evt: Event) => {
|
||||
const value = (evt.currentTarget as HTMLInputElement).value;
|
||||
setTask({...task, label: value});
|
||||
};
|
||||
|
||||
const onTaskCategoryChange = (evt: Event) => {
|
||||
const onNewTaskCategoryChange = (evt: Event) => {
|
||||
const value = (evt.currentTarget as HTMLInputElement).value;
|
||||
setTask({...task, category: value});
|
||||
};
|
||||
|
||||
const onTaskLabelChange = (taskId: TaskID, value: string) => {
|
||||
onTaskLabelUpdate(taskId, value);
|
||||
};
|
||||
|
||||
const onAddTaskClick = (evt: Event) => {
|
||||
onTaskAdd(task);
|
||||
setTask(newTask("", defaultTaskCategory));
|
||||
|
@ -98,7 +104,12 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
<td class={style.mainColumn}>{t.label}</td>
|
||||
<td class={style.mainColumn}>
|
||||
<EditableText
|
||||
render={(value) => (<span>{value}</span>)}
|
||||
onChange={onTaskLabelChange.bind(null, t.id)}
|
||||
value={t.label} />
|
||||
</td>
|
||||
<td>{ categoryLabel }</td>
|
||||
<td>
|
||||
<input class="input" type="number" value={t.estimations.optimistic}
|
||||
|
@ -135,11 +146,11 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="text" placeholder="Nouvelle tâche"
|
||||
value={task.label} onChange={onTaskLabelChange} />
|
||||
value={task.label} onChange={onNewTaskLabelChange} />
|
||||
</p>
|
||||
<p class="control">
|
||||
<span class="select">
|
||||
<select onChange={onTaskCategoryChange} value={task.category}>
|
||||
<select onChange={onNewTaskCategoryChange} value={task.category}>
|
||||
{
|
||||
Object.values(project.params.taskCategories).map(tc => {
|
||||
return (
|
||||
|
|
Loading…
Reference in New Issue