rename use-case to modules
This commit is contained in:
12
src/modules/task/components/NewTaskForm.test.ts
Normal file
12
src/modules/task/components/NewTaskForm.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { withPlugins } from '@/tests/utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import NewTaskVue from './NewTaskForm.vue'
|
||||
|
||||
describe('New Task Form', () => {
|
||||
it('displays New Task Form title', () => {
|
||||
const wrapper = mount(NewTaskVue, withPlugins())
|
||||
|
||||
expect(wrapper.text()).toContain('Create a task')
|
||||
})
|
||||
})
|
||||
56
src/modules/task/components/NewTaskForm.vue
Normal file
56
src/modules/task/components/NewTaskForm.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { createUuid } from '@/shared/create-uuid'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { createStepFixture } from '../models/step.fixture'
|
||||
import { Task } from '../models/task'
|
||||
import { useTaskStore } from '../stores/useTask.store'
|
||||
import StepInput from './StepInput.vue'
|
||||
|
||||
const store = useTaskStore()
|
||||
const router = useRouter()
|
||||
|
||||
const id = createUuid()
|
||||
|
||||
const title = ref(faker.animal.bird())
|
||||
const steps = ref([createStepFixture(), createStepFixture()])
|
||||
const totalEstimation = computed(() =>
|
||||
steps.value.map((step) => step.estimation).reduce((a, b) => a + b, 0)
|
||||
)
|
||||
|
||||
const saveTask = () => {
|
||||
const task = new Task(id, title.value)
|
||||
task.addSteps(...steps.value)
|
||||
|
||||
if (Task.validate(task)) {
|
||||
store.saveTask(task)
|
||||
router.push({
|
||||
name: 'home'
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Create a task</h1>
|
||||
<h2>Estimation: {{ totalEstimation }} minutes</h2>
|
||||
<form @submit.prevent="saveTask">
|
||||
<button type="submit">save task</button>
|
||||
<div>
|
||||
<label for="title">Title</label>
|
||||
<input type="text" id="title" v-model="title" />
|
||||
</div>
|
||||
<StepInput v-model="steps" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
form {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
38
src/modules/task/components/StepInput.test.ts
Normal file
38
src/modules/task/components/StepInput.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createStepFixture } from '../models/step.fixture'
|
||||
import StepInput from './StepInput.vue'
|
||||
|
||||
describe('Step input textarea', () => {
|
||||
it('displays a text area with steps inside', () => {
|
||||
const wrapper = mount(StepInput, {
|
||||
props: {
|
||||
modelValue: []
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.get('textarea')).toBeDefined()
|
||||
})
|
||||
|
||||
it('displays the steps in the textarea', () => {
|
||||
const steps = [
|
||||
createStepFixture(),
|
||||
createStepFixture(),
|
||||
createStepFixture()
|
||||
]
|
||||
|
||||
const stepsInTextarea = steps
|
||||
.map((s) => `- ${s.title} | ${s.estimation}`)
|
||||
.join('\n')
|
||||
|
||||
const wrapper = mount(StepInput, {
|
||||
props: {
|
||||
modelValue: steps
|
||||
}
|
||||
})
|
||||
|
||||
const textarea = wrapper.get('textarea')
|
||||
|
||||
expect(textarea.element.value).toEqual(stepsInTextarea)
|
||||
})
|
||||
})
|
||||
47
src/modules/task/components/StepInput.vue
Normal file
47
src/modules/task/components/StepInput.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
adaptStepsToTextarea,
|
||||
adaptTextareaToSteps
|
||||
} from '../infra/adaptStepsToTextarea'
|
||||
import type { Step } from '../models/step'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Step[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', payload: Step[]): void
|
||||
}>()
|
||||
|
||||
const rawSteps = ref(adaptStepsToTextarea(props.modelValue))
|
||||
|
||||
const stepsTextarea = computed({
|
||||
get() {
|
||||
return rawSteps.value
|
||||
},
|
||||
set(value) {
|
||||
rawSteps.value = value
|
||||
|
||||
emit('update:modelValue', adaptTextareaToSteps(value))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="step-input">
|
||||
<label for="steps">steps</label>
|
||||
<textarea
|
||||
id="steps"
|
||||
name="steps"
|
||||
v-model="stepsTextarea"
|
||||
cols="40"
|
||||
rows="20"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.step-input {
|
||||
}
|
||||
</style>
|
||||
23
src/modules/task/components/TaskList.vue
Normal file
23
src/modules/task/components/TaskList.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useTaskStore } from '@/modules/task/stores/useTask.store'
|
||||
import { formatDate } from '@/shared/format-date'
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="task-list">
|
||||
<li v-for="task in taskStore.recentTasks" :key="task.id">
|
||||
<router-link :to="{ name: 'task-view', params: { id: task.id } }">{{
|
||||
task.title
|
||||
}}</router-link>
|
||||
| {{ task.totalEstimation }} minutes |
|
||||
{{ formatDate(task.date) }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-list {
|
||||
}
|
||||
</style>
|
||||
79
src/modules/task/infra/adaptStepsToTextarea.test.ts
Normal file
79
src/modules/task/infra/adaptStepsToTextarea.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createStepFixture } from '../models/step.fixture'
|
||||
import {
|
||||
adaptStepsToTextarea,
|
||||
adaptTextareaToSteps
|
||||
} from './adaptStepsToTextarea'
|
||||
|
||||
describe('adapt steps to textarea value', () => {
|
||||
it('returns a string with the listed steps', () => {
|
||||
const steps = [
|
||||
createStepFixture(),
|
||||
createStepFixture(),
|
||||
createStepFixture(),
|
||||
createStepFixture()
|
||||
]
|
||||
|
||||
const stepsInTextarea = steps
|
||||
.map((step) => `- ${step.title} | ${step.estimation}`)
|
||||
.join('\n')
|
||||
|
||||
expect(adaptStepsToTextarea(steps)).toEqual(stepsInTextarea)
|
||||
})
|
||||
|
||||
it('returns a list of steps from a textarea value', () => {
|
||||
const stepsInTextarea = `- step 1 | 3
|
||||
- step 2 | 4
|
||||
- step 3 | 5`
|
||||
|
||||
const expectedSteps = [
|
||||
createStepFixture({
|
||||
id: expect.any(String),
|
||||
title: 'step 1',
|
||||
estimation: 3
|
||||
}),
|
||||
createStepFixture({
|
||||
id: expect.any(String),
|
||||
title: 'step 2',
|
||||
estimation: 4
|
||||
}),
|
||||
createStepFixture({
|
||||
id: expect.any(String),
|
||||
title: 'step 3',
|
||||
estimation: 5
|
||||
})
|
||||
]
|
||||
|
||||
expect(adaptTextareaToSteps(stepsInTextarea)).toEqual(expectedSteps)
|
||||
})
|
||||
|
||||
it('fallbacks to 0 for the estimation if there is no estimation', () => {
|
||||
const stepInTextarea = '- step 1'
|
||||
|
||||
const expectedStep = createStepFixture({
|
||||
id: expect.any(String),
|
||||
title: 'step 1',
|
||||
estimation: 0
|
||||
})
|
||||
|
||||
expect(adaptTextareaToSteps(stepInTextarea)).toEqual([expectedStep])
|
||||
})
|
||||
|
||||
it('fallbacks to 0 for the estimation if it can not read the number', () => {
|
||||
const stepInTextarea = '- step 1 | not an estimation'
|
||||
|
||||
const expectedStep = createStepFixture({
|
||||
id: expect.any(String),
|
||||
title: 'step 1',
|
||||
estimation: 0
|
||||
})
|
||||
|
||||
expect(adaptTextareaToSteps(stepInTextarea)).toEqual([expectedStep])
|
||||
})
|
||||
|
||||
it('does not create a step with an empty title', () => {
|
||||
const stepInTextArea = '\n-'
|
||||
|
||||
expect(adaptTextareaToSteps(stepInTextArea)).toEqual([])
|
||||
})
|
||||
})
|
||||
38
src/modules/task/infra/adaptStepsToTextarea.ts
Normal file
38
src/modules/task/infra/adaptStepsToTextarea.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createUuid } from '@/shared/create-uuid'
|
||||
import { Step } from '../models/step'
|
||||
|
||||
export const adaptStepsToTextarea = (steps: Step[]) =>
|
||||
steps.map((step) => `- ${step.title} | ${step.estimation}`).join('\n')
|
||||
|
||||
const extractTitleAndEstimationFromStep = (
|
||||
rawStep: string
|
||||
): [string, number] => {
|
||||
const [rawTitle, rawEstimation] = rawStep
|
||||
.trim()
|
||||
.replace(/^-\s*/, '')
|
||||
.split('|')
|
||||
const title = rawTitle.trim()
|
||||
|
||||
const estimationString = (rawEstimation || '').trim()
|
||||
const estimation = Number(estimationString)
|
||||
|
||||
if (isNaN(estimation)) {
|
||||
return [title, 0]
|
||||
}
|
||||
|
||||
return [title, estimation]
|
||||
}
|
||||
|
||||
export const adaptTextareaToSteps = (textareaValue: string): Step[] =>
|
||||
textareaValue
|
||||
.split('\n')
|
||||
.map((rawStep) => {
|
||||
const [title, estimation] = extractTitleAndEstimationFromStep(rawStep)
|
||||
|
||||
if (!title) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new Step(createUuid(), title, estimation)
|
||||
})
|
||||
.filter((step) => step !== null) as Step[]
|
||||
12
src/modules/task/interfaces/result.ts
Normal file
12
src/modules/task/interfaces/result.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export enum ResultStatus {
|
||||
RUN,
|
||||
DEBRIEF
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
taskId: string
|
||||
time: number
|
||||
steps: Record<string, number>
|
||||
currentStepId: string | null
|
||||
status: ResultStatus
|
||||
}
|
||||
8
src/modules/task/interfaces/stepable.ts
Normal file
8
src/modules/task/interfaces/stepable.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface Stepable {
|
||||
id: string
|
||||
title: string
|
||||
/**
|
||||
* estimation in minutes
|
||||
*/
|
||||
estimation: number
|
||||
}
|
||||
9
src/modules/task/interfaces/taskable.ts
Normal file
9
src/modules/task/interfaces/taskable.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
|
||||
export interface Taskable {
|
||||
id: string
|
||||
title: string
|
||||
date: Date
|
||||
link: string | null
|
||||
steps: Stepable[]
|
||||
}
|
||||
14
src/modules/task/models/step.fixture.ts
Normal file
14
src/modules/task/models/step.fixture.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
import { Step } from '@/modules/task/models/step'
|
||||
import { faker } from '@faker-js/faker'
|
||||
|
||||
export const createStepFixture = (partialStep?: Partial<Stepable>) =>
|
||||
new Step(
|
||||
partialStep?.id ?? faker.datatype.uuid(),
|
||||
partialStep?.title ?? faker.animal.bird(),
|
||||
partialStep?.estimation ??
|
||||
faker.datatype.number({
|
||||
min: 0,
|
||||
max: 40
|
||||
})
|
||||
)
|
||||
17
src/modules/task/models/step.ts
Normal file
17
src/modules/task/models/step.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
|
||||
export class Step implements Stepable {
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly title: string,
|
||||
readonly estimation: number
|
||||
) {
|
||||
return this
|
||||
}
|
||||
|
||||
public static fromStepable(...stepables: Stepable[]): Step[] {
|
||||
return stepables.map(
|
||||
(stepable) => new Step(stepable.id, stepable.title, stepable.estimation)
|
||||
)
|
||||
}
|
||||
}
|
||||
14
src/modules/task/models/task.fixture.ts
Normal file
14
src/modules/task/models/task.fixture.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import type { Stepable } from '../interfaces/stepable'
|
||||
import type { Taskable } from '../interfaces/taskable'
|
||||
import { createStepFixture } from './step.fixture'
|
||||
import { Task } from './task'
|
||||
|
||||
export const createTaskFixture = (
|
||||
partialTask?: Partial<Taskable>,
|
||||
...steps: Stepable[]
|
||||
) =>
|
||||
new Task(
|
||||
partialTask?.id ?? faker.datatype.uuid(),
|
||||
partialTask?.title ?? faker.animal.bird()
|
||||
).addSteps(...(steps ?? createStepFixture()))
|
||||
64
src/modules/task/models/task.test.ts
Normal file
64
src/modules/task/models/task.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Taskable } from '@/modules/task/interfaces/taskable'
|
||||
import { createStepFixture } from '@/modules/task/models/step.fixture'
|
||||
import { Task } from '@/modules/task/models/task'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('Task', () => {
|
||||
it('has a simple id', () => {
|
||||
const uuid = faker.datatype.uuid()
|
||||
|
||||
const task = new Task(uuid, faker.animal.bear())
|
||||
|
||||
expect(task.id).equals(uuid)
|
||||
})
|
||||
|
||||
it('allows a new task from a taskable object', () => {
|
||||
const taskable: Taskable = {
|
||||
id: faker.datatype.uuid(),
|
||||
date: faker.date.recent(),
|
||||
title: faker.animal.lion(),
|
||||
link: faker.internet.url(),
|
||||
steps: [createStepFixture()]
|
||||
}
|
||||
const task = Task.fromTaskable(taskable)
|
||||
|
||||
expect(task).toEqual(taskable)
|
||||
})
|
||||
|
||||
it('adds steps and removes them', () => {
|
||||
const task = new Task(faker.datatype.uuid(), faker.color.human())
|
||||
|
||||
const [firstStep, secondStep] = [createStepFixture(), createStepFixture()]
|
||||
|
||||
task.addSteps(firstStep, secondStep)
|
||||
|
||||
expect(task.steps).toEqual([firstStep, secondStep])
|
||||
|
||||
task.removeStep(0)
|
||||
expect(task.steps).toEqual([secondStep])
|
||||
|
||||
task.removeStep(0)
|
||||
expect(task.steps).toEqual([])
|
||||
})
|
||||
|
||||
it('must have an id, a title and steps to be valid', () => {
|
||||
const task = new Task(faker.datatype.uuid(), faker.color.human())
|
||||
expect(Task.validate(task)).toEqual(false)
|
||||
|
||||
task.addSteps(createStepFixture())
|
||||
expect(Task.validate(task)).toEqual(true)
|
||||
})
|
||||
|
||||
it('calculates the total estimation of steps', () => {
|
||||
const task = new Task(faker.datatype.uuid(), faker.color.human())
|
||||
|
||||
task.addSteps(
|
||||
createStepFixture({ estimation: 1 }),
|
||||
createStepFixture({ estimation: 2 }),
|
||||
createStepFixture({ estimation: 3 })
|
||||
)
|
||||
|
||||
expect(task.totalEstimation).toEqual(6)
|
||||
})
|
||||
})
|
||||
46
src/modules/task/models/task.ts
Normal file
46
src/modules/task/models/task.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
import type { Taskable } from '@/modules/task/interfaces/taskable'
|
||||
import { Step } from '@/modules/task/models/step'
|
||||
|
||||
export class Task implements Taskable {
|
||||
public date = new Date()
|
||||
public steps: Step[] = []
|
||||
public link: string | null = null
|
||||
|
||||
constructor(public readonly id: string, public readonly title: string) {}
|
||||
|
||||
public addSteps(...steps: Stepable[]) {
|
||||
this.steps.push(...Step.fromStepable(...steps))
|
||||
return this
|
||||
}
|
||||
|
||||
public removeStep(index: number) {
|
||||
if (index < 0) {
|
||||
return this
|
||||
}
|
||||
|
||||
if (index >= this.steps.length) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.steps.splice(index, 1)
|
||||
return this
|
||||
}
|
||||
|
||||
public get totalEstimation() {
|
||||
return this.steps.map((step) => step.estimation).reduce((a, b) => a + b, 0)
|
||||
}
|
||||
|
||||
public static fromTaskable(taskable: Taskable) {
|
||||
const task = new Task(taskable.id, taskable.title)
|
||||
task.link = taskable.link
|
||||
task.date = taskable.date
|
||||
task.addSteps(...taskable.steps)
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
public static validate(task: Taskable) {
|
||||
return !!task.id && !!task.title && task.steps.length > 0
|
||||
}
|
||||
}
|
||||
42
src/modules/task/stores/useTask.store.ts
Normal file
42
src/modules/task/stores/useTask.store.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { Taskable } from '../interfaces/taskable'
|
||||
import { Task } from '../models/task'
|
||||
|
||||
interface StoredTaskable extends Omit<Taskable, 'date'> {
|
||||
date: string
|
||||
}
|
||||
|
||||
export interface TaskStoreState {
|
||||
tasks: StoredTaskable[]
|
||||
}
|
||||
|
||||
export const useTaskStore = defineStore('task-store', {
|
||||
persist: true,
|
||||
state: (): TaskStoreState => ({
|
||||
tasks: []
|
||||
}),
|
||||
actions: {
|
||||
saveTask(task: Taskable) {
|
||||
this.tasks.push({
|
||||
...task,
|
||||
date: task.date.toISOString()
|
||||
})
|
||||
},
|
||||
reset() {
|
||||
this.tasks = []
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
recentTasks(state) {
|
||||
return state.tasks
|
||||
.map((task) =>
|
||||
Task.fromTaskable({ ...task, date: new Date(task.date) })
|
||||
)
|
||||
.sort((a, b) => (a.date > b.date ? -1 : 1))
|
||||
},
|
||||
getTask() {
|
||||
return (taskId: string): Task | undefined =>
|
||||
this.recentTasks.find((task) => task.id === taskId)
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user