feat(tasking): add projects feature and restructure backend
- Restructure backend code into Tasking module with organized endpoints - Add Project and Sprint entities with database migrations - Implement CRUD endpoints for projects (create, get, rename, delete) - Refactor task endpoints into Tasking namespace - Add integration test suite with Testcontainers and Respawn - Refactor frontend to use Pinia stores with dedicated API clients - Add DueDatePicker and DueTimePicker components for task scheduling - Add environment configuration for API base URL - Add infrastructure setup scripts for Docker/Postgres
This commit is contained in:
6
Tasker.Ui/.env
Normal file
6
Tasker.Ui/.env
Normal file
@@ -0,0 +1,6 @@
|
||||
# Development environment variables for Vite
|
||||
# You can override these locally without committing secrets.
|
||||
# Vite only exposes variables prefixed with VITE_.
|
||||
|
||||
# Backend API base URL for development
|
||||
VITE_API_BASE_URL=http://localhost:5239/
|
||||
5
Tasker.Ui/.env.production
Normal file
5
Tasker.Ui/.env.production
Normal file
@@ -0,0 +1,5 @@
|
||||
# Production environment variables for Vite
|
||||
# Replace the URL below with your production API endpoint.
|
||||
# Vite only exposes variables prefixed with VITE_.
|
||||
|
||||
VITE_API_BASE_URL=https://api.example.com/
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to Vuetify 3</title>
|
||||
<title>Done By Daylight</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
2998
Tasker.Ui/package-lock.json
generated
2998
Tasker.Ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,8 @@
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"axios": "^1.6.7",
|
||||
"axios": "^1.7.7",
|
||||
"date-fns": "^3.6.0",
|
||||
"pinia": "^2.1.7",
|
||||
"uuidv7": "^1.0.1",
|
||||
"vue": "^3.4.29",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
value="users"
|
||||
></v-list-item>
|
||||
</v-list>
|
||||
|
||||
<project-section></project-section>
|
||||
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar app
|
||||
@@ -67,31 +70,14 @@
|
||||
</v-app>
|
||||
|
||||
</template>
|
||||
|
||||
m
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import ProjectSection from '@/components/ProjectSection.vue'
|
||||
|
||||
const drawer = ref(true)
|
||||
const rail = ref(false)
|
||||
const group = ref(null)
|
||||
const items = ref([
|
||||
{
|
||||
title: 'Foo',
|
||||
value: 'foo'
|
||||
},
|
||||
{
|
||||
title: 'Bar',
|
||||
value: 'bar'
|
||||
},
|
||||
{
|
||||
title: 'Fizz',
|
||||
value: 'fizz'
|
||||
},
|
||||
{
|
||||
title: 'Buzz',
|
||||
value: 'buzz'
|
||||
}
|
||||
])
|
||||
|
||||
watch(group, () => {
|
||||
drawer.value = false
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
name: string; // [MaxLength(255)]
|
||||
description?: string | null; // [MaxLength(2048)]
|
||||
dueDate?: string | null;
|
||||
}
|
||||
|
||||
export interface TaskApiClient {
|
||||
getTasks(): Promise<Task[]>;
|
||||
|
||||
addTask(task: Task): Promise<Task>;
|
||||
}
|
||||
|
||||
export class TaskApiClientImpl implements TaskApiClient {
|
||||
private readonly baseUrl: string
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
async getTasks(): Promise<Task[]> {
|
||||
const response = await fetch(`${this.baseUrl}/tasks`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch tasks')
|
||||
}
|
||||
const data = await response.json()
|
||||
return data as Task[]
|
||||
}
|
||||
|
||||
async addTask(task: Task): Promise<Task> {
|
||||
const response = await fetch(`${this.baseUrl}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(task)
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add task')
|
||||
}
|
||||
const data = await response.json()
|
||||
return data as Task
|
||||
}
|
||||
|
||||
async completeTask(id: string): Promise<Task> {
|
||||
const response = await fetch(`${this.baseUrl}/tasks/${id}/complete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'CompleteOn': new Date().toISOString()
|
||||
})
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add task')
|
||||
}
|
||||
const data = await response.json()
|
||||
return data as Task
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function useTasks() {
|
||||
const tasks = ref<Task[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const taskApiClient = new TaskApiClientImpl('https://localhost:7055')
|
||||
|
||||
const fetchTasks = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
tasks.value = await taskApiClient.getTasks()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addTask = async (task: Task) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newTask = await taskApiClient.addTask(task)
|
||||
tasks.value.push(newTask)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const completeTask = async (id: string) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await taskApiClient.completeTask(id)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchTasks)
|
||||
|
||||
return {
|
||||
tasks,
|
||||
isLoading,
|
||||
error,
|
||||
fetchTasks,
|
||||
addTask,
|
||||
completeTask
|
||||
}
|
||||
}
|
||||
160
Tasker.Ui/src/components/DueDatePicker.vue
Normal file
160
Tasker.Ui/src/components/DueDatePicker.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="due-date-picker">
|
||||
<v-card>
|
||||
<!-- Date Search Input -->
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="Type a due date"
|
||||
variant="underlined"
|
||||
></v-text-field>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<!-- Quick Date Selection -->
|
||||
<v-list>
|
||||
<v-list-item @click="selectQuickDate('today')">
|
||||
<v-icon left>mdi-calendar-today</v-icon>
|
||||
<v-list-item-title>Today</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedToday }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="selectQuickDate('tomorrow')">
|
||||
<v-icon left>mdi-calendar-arrow-right</v-icon>
|
||||
<v-list-item-title>Tomorrow</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedTomorrow }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="selectQuickDate('weekend')">
|
||||
<v-icon left>mdi-calendar-weekend</v-icon>
|
||||
<v-list-item-title>This weekend</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedWeekend }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="selectQuickDate('nextWeek')">
|
||||
<v-icon left>mdi-calendar-arrow-right-outline</v-icon>
|
||||
<v-list-item-title>Next week</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formattedNextWeek }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<!-- Date Picker -->
|
||||
<v-date-picker v-model="selectedDate"></v-date-picker>
|
||||
|
||||
<v-btn @click="openTimePicker">Time</v-btn>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
text="Cancel"
|
||||
color="secondary"
|
||||
@click="cancel"
|
||||
></v-btn>
|
||||
|
||||
<v-btn
|
||||
text="Set Time"
|
||||
color="primary"
|
||||
@click="save"
|
||||
></v-btn>
|
||||
|
||||
</v-card-actions>
|
||||
|
||||
</v-card>
|
||||
|
||||
<!-- DueTimePicker Component -->
|
||||
<due-time-picker
|
||||
v-model="isTimeDialogOpen"
|
||||
:time="selectedTime"
|
||||
@cancel=""
|
||||
@save="handleTimeSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, defineEmits} from 'vue'
|
||||
import {format, addDays, nextMonday} from 'date-fns'
|
||||
import DueTimePicker from './DueTimePicker.vue'
|
||||
import type { Time } from './Time';
|
||||
|
||||
// Reactive state
|
||||
const selectedDate = ref<Date | null>(null)
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
const selectedTime = ref<Time | null>(null)
|
||||
|
||||
interface DueDateChanged {
|
||||
selectedDate: Date,
|
||||
selectedTime: Time,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: DueDateChanged],
|
||||
update: [value: number]
|
||||
}>()
|
||||
|
||||
// Dialog state for time picker
|
||||
const isTimeDialogOpen = ref<boolean>(false)
|
||||
|
||||
// Methods to get quick dates
|
||||
const today = new Date()
|
||||
const tomorrow = addDays(today, 1)
|
||||
const weekend = addDays(today, 6 - today.getDay())
|
||||
const nextWeek = nextMonday(today)
|
||||
|
||||
// Computed formatted dates
|
||||
const formattedToday = computed(() => format(today, 'PPPP'))
|
||||
const formattedTomorrow = computed(() => format(tomorrow, 'PPPP'))
|
||||
const formattedWeekend = computed(() => format(weekend, 'PPPP'))
|
||||
const formattedNextWeek = computed(() => format(nextWeek, 'PPPP'))
|
||||
|
||||
// Select quick date options
|
||||
function selectQuickDate(option: 'today' | 'tomorrow' | 'weekend' | 'nextWeek') {
|
||||
switch (option) {
|
||||
case 'today':
|
||||
selectedDate.value = today
|
||||
break
|
||||
case 'tomorrow':
|
||||
selectedDate.value = tomorrow
|
||||
break
|
||||
case 'weekend':
|
||||
selectedDate.value = weekend
|
||||
break
|
||||
case 'nextWeek':
|
||||
selectedDate.value = nextWeek
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('update', 1)
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
}
|
||||
|
||||
// Open Time Picker Dialog
|
||||
function openTimePicker() {
|
||||
isTimeDialogOpen.value = true
|
||||
}
|
||||
|
||||
// Handle Save Event from DueTimePicker
|
||||
function handleTimeSave(time: Time) {
|
||||
console.log('Time:', time.time)
|
||||
console.log('Duration:', time.duration)
|
||||
console.log('Time Zone:', time.timeZone)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.due-date-picker {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
100
Tasker.Ui/src/components/DueTimePicker.vue
Normal file
100
Tasker.Ui/src/components/DueTimePicker.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<v-dialog v-model="isTimeDialogOpen" width="400px">
|
||||
<v-card>
|
||||
<v-card-title>Time Picker</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- Time Picker or Manual Input -->
|
||||
<v-autocomplete
|
||||
v-model="selectedTime"
|
||||
:items="timeOptions"
|
||||
label="Select or Enter Time"
|
||||
clearable
|
||||
@blur="validateTime"
|
||||
></v-autocomplete>
|
||||
|
||||
<!-- Duration Picker -->
|
||||
<v-select
|
||||
v-model="selectedDuration"
|
||||
:items="durations"
|
||||
label="Duration"
|
||||
></v-select>
|
||||
|
||||
<!-- Time Zone Picker -->
|
||||
<v-select
|
||||
v-model="selectedTimeZone"
|
||||
:items="timeZones"
|
||||
label="Time zone"
|
||||
></v-select>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="secondary" @click="close">Cancel</v-btn>
|
||||
<v-btn color="primary" @click="saveTimeSettings">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue';
|
||||
import {type Time} from './Time';
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: [],
|
||||
ok: [value: Time]
|
||||
}>()
|
||||
|
||||
// Reactive state
|
||||
const selectedTime = ref<string | null>(null);
|
||||
const selectedDuration = ref<string | null>(null);
|
||||
const selectedTimeZone = ref<string | null>('Floating time');
|
||||
|
||||
// Duration and Time Zones options
|
||||
const durations = ref(['No duration', '15 mins', '30 mins', '1 hour', '2 hours']);
|
||||
const timeZones = ref(['Floating time', 'Pacific Time', 'Mountain Time', 'Central Time', 'Eastern Time']);
|
||||
|
||||
// Time options every 15 minutes
|
||||
const timeOptions = ref(generateTimeOptions());
|
||||
|
||||
// Generate time options in 15-minute intervals
|
||||
function generateTimeOptions() {
|
||||
const times = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 15) {
|
||||
const time = new Date(0, 0, 0, hour, minute).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
times.push(time);
|
||||
}
|
||||
}
|
||||
return times;
|
||||
}
|
||||
|
||||
// Validate manually entered time
|
||||
function validateTime() {
|
||||
if (!selectedTime.value) return;
|
||||
|
||||
const isValidTime = timeOptions.value.includes(selectedTime.value);
|
||||
if (!isValidTime) {
|
||||
// If the time is not valid, you might want to reset it or show an error
|
||||
console.log('Invalid time entered');
|
||||
}
|
||||
}
|
||||
|
||||
// Close the dialog
|
||||
function close() {
|
||||
emit('cancel', false);
|
||||
}
|
||||
|
||||
// Save Time, Duration, and Time Zone Settings
|
||||
function saveTimeSettings() {
|
||||
emit('ok', {
|
||||
time: selectedTime.value,
|
||||
duration: selectedDuration.value,
|
||||
timeZone: selectedTimeZone.value,
|
||||
});
|
||||
close();
|
||||
}
|
||||
|
||||
</script>
|
||||
49
Tasker.Ui/src/components/ProjectCreate.vue
Normal file
49
Tasker.Ui/src/components/ProjectCreate.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showForm = ref(false)
|
||||
const name = ref('')
|
||||
const description = ref(null)
|
||||
|
||||
function createProject() {
|
||||
console.log(`Create new project: ${name.value} - ${description.value}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn v-if="!showForm" @click="showForm = true">
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
Add New Project
|
||||
</v-btn>
|
||||
|
||||
<v-form v-else>
|
||||
|
||||
<v-card>
|
||||
<v-text-field
|
||||
label="Name"
|
||||
v-model="name"
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
label="Description"
|
||||
v-model="description"
|
||||
></v-text-field>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="success" @click="createProject">
|
||||
Submit
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="error" @click="showForm = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
26
Tasker.Ui/src/components/ProjectList.vue
Normal file
26
Tasker.Ui/src/components/ProjectList.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useProjectStore } from '@/stores/projectStore'
|
||||
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item
|
||||
v-for="(item, index) in projectStore.projects"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
17
Tasker.Ui/src/components/ProjectSection.vue
Normal file
17
Tasker.Ui/src/components/ProjectSection.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import ProjectList from '@/components/ProjectList.vue'
|
||||
import ProjectCreate from '@/components/ProjectCreate.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Projects</h1>
|
||||
|
||||
<project-list></project-list>
|
||||
|
||||
<project-create></project-create>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,22 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { type Task, useTasks } from '@/api/taskApi'
|
||||
import { uuidv7 } from 'uuidv7'
|
||||
import DueDatePicker from '@/components/DueDatePicker.vue'
|
||||
import { useTaskStore } from '@/stores/taskStore'
|
||||
|
||||
const name = ref('')
|
||||
const description = ref(null)
|
||||
const dueDate = ref(null)
|
||||
const name = ref<string>('')
|
||||
const description = ref<string | null>(null)
|
||||
const dueDate = ref<string | null>(null)
|
||||
|
||||
const { addTask } = useTasks()
|
||||
const modalDueDatePicker = ref<boolean>(false)
|
||||
|
||||
const doAddTask = () => {
|
||||
const newTask: Task = {
|
||||
function changeDueDate(): void {
|
||||
modalDueDatePicker.value = !modalDueDatePicker.value
|
||||
}
|
||||
|
||||
function onDueDateChanged(value: string): void {
|
||||
dueDate.value = value
|
||||
}
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
function doAddTask(): Task {
|
||||
taskStore.addTask({
|
||||
id: uuidv7(),
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
dueDate: dueDate.value
|
||||
}
|
||||
addTask(newTask)
|
||||
} as Task)
|
||||
}
|
||||
|
||||
const cancelAddTask = () => {
|
||||
@@ -39,7 +49,16 @@ const cancelAddTask = () => {
|
||||
|
||||
<div class="flex flex-row space-x-2">
|
||||
|
||||
<v-btn @click="changeDueDate()">
|
||||
<span>due-date</span>
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="modalDueDatePicker"
|
||||
width="400px">
|
||||
<due-date-picker :value="dueDate"
|
||||
@value-changed="onDueDateChanged"
|
||||
></due-date-picker>
|
||||
</v-dialog>
|
||||
|
||||
<span>priority</span>
|
||||
<span>tags</span>
|
||||
@@ -47,7 +66,7 @@ const cancelAddTask = () => {
|
||||
</div>
|
||||
|
||||
|
||||
<div class=" bg-teal-400 p-2 flex flex-row space-x-2 justify-end">
|
||||
<div class="bg-teal-400 p-2 flex flex-row space-x-2 justify-end">
|
||||
|
||||
<span class="justify-start">projects</span>
|
||||
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { useTaskStore } from '@/stores/taskStore'
|
||||
import type { Task } from '@/stores/task'
|
||||
|
||||
import { useTasks } from '@/api/taskApi'
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
const { completeTask, tasks, isLoading, error } = useTasks()
|
||||
|
||||
function itemCompleted(id: string) {
|
||||
console.log(`completing task: ${id}`)
|
||||
completeTask(id)
|
||||
function toggleTaskComplete(task: Task) {
|
||||
taskStore.completeTask(task) // Call the API to save the change
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list v-if="!isLoading && !error">
|
||||
<v-list-item v-for="task in tasks" :key="task.id">
|
||||
<div class="flex p-1 bg-amber">
|
||||
<v-form v-if="!taskStore.isLoading && !taskStore.error">
|
||||
<div class="flex p-0 m-0 bg-amber"
|
||||
v-for="task in taskStore.tasks" :key="task.id">
|
||||
|
||||
<div>
|
||||
<v-checkbox-btn @change="itemCompleted(task.id)"
|
||||
<v-checkbox-btn :model-value="task.completedOn !== null"
|
||||
@change="toggleTaskComplete(task)"
|
||||
></v-checkbox-btn>
|
||||
</div>
|
||||
|
||||
<div class="flex-column">
|
||||
<div class="">
|
||||
<div class="font-sans">{{ task.name }}</div>
|
||||
<div class="font-serif font-normal">Description {{ task.description }}</div>
|
||||
<div class="font-sans font-medium">{{ task.name }}</div>
|
||||
<div class="font-sans font-normal">{{ task.description }}</div>
|
||||
</div>
|
||||
|
||||
<div>{{ task.dueDate }}</div>
|
||||
<!-- <div>{{ task.project.Name }}</div>-->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-form>
|
||||
|
||||
<v-alert v-else-if="error" type="error">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<v-progress-circular v-else indeterminate></v-progress-circular>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
5
Tasker.Ui/src/components/Time.ts
Normal file
5
Tasker.Ui/src/components/Time.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Time{
|
||||
time: string,
|
||||
timeZone: string,
|
||||
duration: string,
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import {ref, computed} from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
50
Tasker.Ui/src/stores/httpClient.ts
Normal file
50
Tasker.Ui/src/stores/httpClient.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// src/api/axiosClient.ts
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
// Resolve API base URL from Vite environment variables.
|
||||
// Configure via `.env` (development) and `.env.production` (production):
|
||||
// VITE_API_BASE_URL=https://localhost:7055/
|
||||
// If not set, it will fall back to current origin.
|
||||
console.log(`BASE URL: ${import.meta.env.VITE_API_BASE_URL}`);
|
||||
const envBaseUrl = (import.meta as any)?.env?.VITE_API_BASE_URL as string | undefined
|
||||
const resolvedBaseUrl = envBaseUrl && envBaseUrl.trim().length > 0
|
||||
? envBaseUrl
|
||||
: (typeof window !== 'undefined' ? window.location.origin : '')
|
||||
|
||||
console.log(`RESOLVED BASE URL: ${resolvedBaseUrl}`);
|
||||
// Create an instance of axios
|
||||
const httpClient = axios.create({
|
||||
baseURL: resolvedBaseUrl, // API base URL from configuration
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // Default headers
|
||||
},
|
||||
timeout: 5000, // Set a timeout (optional)
|
||||
})
|
||||
|
||||
// Optionally, add request/response interceptors
|
||||
httpClient.interceptors.request.use(
|
||||
(config) => {
|
||||
// You can modify the request here, e.g., attach auth tokens
|
||||
// config.headers.Authorization = `Bearer ${yourToken}`
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
httpClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
// Handle errors globally, e.g., log out on 401
|
||||
if (error.response?.status === 401) {
|
||||
console.error('Unauthorized, redirect to login')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default httpClient
|
||||
17
Tasker.Ui/src/stores/project.ts
Normal file
17
Tasker.Ui/src/stores/project.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string; // [MaxLength(255)]
|
||||
description?: string | null; // [MaxLength(2048)]
|
||||
dueDate?: string | null;
|
||||
completedOn?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateProjectRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ChangeProjectDescription {
|
||||
id: string;
|
||||
completedOn: Date;
|
||||
}
|
||||
77
Tasker.Ui/src/stores/projectStore.ts
Normal file
77
Tasker.Ui/src/stores/projectStore.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { projectsApiClient } from '@/stores/projectsApiClient'
|
||||
import type { Project } from '@/stores/project'
|
||||
|
||||
export const useProjectStore = defineStore(
|
||||
'projectStore',
|
||||
() => {
|
||||
const projects = ref<Project[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
projectsApiClient
|
||||
.getProjects()
|
||||
.then(t => projects.value = t)
|
||||
.catch(e => error.value = (e as Error).message)
|
||||
.finally(isLoading.value = false)
|
||||
|
||||
const fetchProjects = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
projects.value = await projectsApiClient.getProjects()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addProject = async (project: project) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newProject = await projectsApiClient.createProject(project)
|
||||
projects.value.push(newProject)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const completeProject = async (project: Project) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
if (project.completedOn) {
|
||||
await projectsApiClient.uncompleteProject(project.id)
|
||||
project.completedOn = null
|
||||
} else {
|
||||
project.completedOn = new Date().toISOString()
|
||||
await projectsApiClient.completeProject(
|
||||
{
|
||||
id: project.id,
|
||||
completedOn: project.completedOn
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projects,
|
||||
isLoading,
|
||||
error,
|
||||
fetchProjects,
|
||||
addProject,
|
||||
completeProject
|
||||
}
|
||||
}
|
||||
)
|
||||
44
Tasker.Ui/src/stores/projectsApiClient.ts
Normal file
44
Tasker.Ui/src/stores/projectsApiClient.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Project, CreateProjectRequest, CompleteProjectRequest } from '@/stores/project'
|
||||
import httpClient from '@/stores/httpClient'
|
||||
|
||||
export const projectsApiClient = {
|
||||
// Fetch all Projects
|
||||
async getProjects(): Promise<Project[]> {
|
||||
const response = await httpClient.get<Project[]>('/projects')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Fetch a single Project by ID
|
||||
async getProjectById(id: string): Promise<Project> {
|
||||
const response = await httpClient.get<Project>(`/projects/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Create a new Project
|
||||
async createProject(request: CreateProjectRequest): Promise<Project> {
|
||||
const response = await httpClient.post<Project>('/projects', request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Update an existing Project
|
||||
async completeProject(request: CompleteProjectRequest): Promise<Project> {
|
||||
const response = await httpClient.put<Project>(
|
||||
`/projects/complete`,
|
||||
request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async uncompleteProject(id: string): Promise<Project> {
|
||||
const response = await httpClient.put<Project>(
|
||||
`/projects/uncomplete`,
|
||||
{
|
||||
id: id
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Delete a Project
|
||||
async deleteProject(id: string): Promise<void> {
|
||||
await httpClient.delete(`/projects/${id}`)
|
||||
},
|
||||
}
|
||||
17
Tasker.Ui/src/stores/task.ts
Normal file
17
Tasker.Ui/src/stores/task.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
name: string; // [MaxLength(255)]
|
||||
description?: string | null; // [MaxLength(2048)]
|
||||
dueDate?: string | null;
|
||||
completedOn?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateTaskRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CompleteTaskRequest {
|
||||
id: string;
|
||||
completedOn: Date;
|
||||
}
|
||||
77
Tasker.Ui/src/stores/taskStore.ts
Normal file
77
Tasker.Ui/src/stores/taskStore.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { tasksApiClient } from '@/stores/tasksApiClient'
|
||||
import type { Task } from '@/stores/task'
|
||||
|
||||
export const useTaskStore = defineStore(
|
||||
'taskStore',
|
||||
() => {
|
||||
const tasks = ref<Task[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
tasksApiClient
|
||||
.getTasks()
|
||||
.then(t => tasks.value = t)
|
||||
.catch(e => error.value = (e as Error).message)
|
||||
.finally(isLoading.value = false)
|
||||
|
||||
const fetchTasks = async () => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
tasks.value = await tasksApiClient.getTasks()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addTask = async (task: Task) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newTask = await tasksApiClient.createTask(task)
|
||||
tasks.value.push(newTask)
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const completeTask = async (task: Task) => {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
if (task.completedOn) {
|
||||
await tasksApiClient.uncompleteTask(task.id)
|
||||
task.completedOn = null
|
||||
} else {
|
||||
task.completedOn = new Date().toISOString()
|
||||
await tasksApiClient.completeTask(
|
||||
{
|
||||
id: task.id,
|
||||
completedOn: task.completedOn
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
isLoading,
|
||||
error,
|
||||
fetchTasks,
|
||||
addTask,
|
||||
completeTask
|
||||
}
|
||||
}
|
||||
)
|
||||
46
Tasker.Ui/src/stores/tasksApiClient.ts
Normal file
46
Tasker.Ui/src/stores/tasksApiClient.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Task, CreateTaskRequest, CompleteTaskRequest } from '@/stores/task'
|
||||
import httpClient from '@/stores/httpClient'
|
||||
|
||||
export const tasksApiClient = {
|
||||
// Fetch all Tasks
|
||||
async getTasks(): Promise<Task[]> {
|
||||
const response = await httpClient.get<Task[]>('/tasks')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Fetch a single Task by ID
|
||||
async getTaskById(id: string): Promise<Task> {
|
||||
const response = await httpClient.get<Task>(`/tasks/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Create a new Task
|
||||
async createTask(request: CreateTaskRequest): Promise<Task> {
|
||||
const response = await httpClient.post<Task>(
|
||||
'/tasks',
|
||||
request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Update an existing Task
|
||||
async completeTask(request: CompleteTaskRequest): Promise<Task> {
|
||||
const response = await httpClient.put<Task>(
|
||||
`/tasks/complete`,
|
||||
request)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async uncompleteTask(id: string): Promise<Task> {
|
||||
const response = await httpClient.put<Task>(
|
||||
`/tasks/uncomplete`,
|
||||
{
|
||||
id: id
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Delete a Task
|
||||
async deleteTask(id: string): Promise<void> {
|
||||
await httpClient.delete(`/tasks/${id}`)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import {useCounterStore} from "@/stores/counter";
|
||||
import TaskInput from "@/components/TaskInput.vue";
|
||||
import TaskInput from '@/components/TaskInput.vue'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
|
||||
const counterStore = useCounterStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<v-card class="m-2 p-2">
|
||||
<task-input></task-input>
|
||||
</v-card>
|
||||
|
||||
<v-card class="m-2 p-2">
|
||||
<task-list></task-list>
|
||||
</v-card>
|
||||
|
||||
</template>
|
||||
|
||||
122
Tasker/Tasker.Web.Tests/InfrastructureFixture.cs
Normal file
122
Tasker/Tasker.Web.Tests/InfrastructureFixture.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Npgsql;
|
||||
using Respawn;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tests;
|
||||
|
||||
public class TaskerWebAppFactory(
|
||||
PostgreSqlContainer postgres)
|
||||
: WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(
|
||||
IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var descriptor = services.Single(
|
||||
d => d.ServiceType == typeof(DbContextOptions<ProjectDbContext>));
|
||||
|
||||
services.Remove(descriptor);
|
||||
services.AddDbContext<ProjectDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(postgres.GetConnectionString());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InfrastructureFixture
|
||||
: IAsyncLifetime
|
||||
{
|
||||
public PostgreSqlContainer? Postgres { get; private set; }
|
||||
public TaskerWebAppFactory AppFactory { get; private set; } = null!;
|
||||
public HttpClient Client { get; private set; } = null!;
|
||||
public Respawner RespawnPoint { get; private set; } = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await CreateDatabaseContainerAsync();
|
||||
await CreateDatabaseAsync();
|
||||
await SeedDatabaseAsync();
|
||||
await CreateDatabaseSnapshotAsync();
|
||||
|
||||
AppFactory = new TaskerWebAppFactory(Postgres!);
|
||||
Client = AppFactory.CreateClient();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (Postgres is null) return;
|
||||
|
||||
await Postgres.StopAsync();
|
||||
await Postgres.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task CreateDatabaseContainerAsync()
|
||||
{
|
||||
Postgres = new PostgreSqlBuilder()
|
||||
.WithDatabase("Tasker")
|
||||
.WithUsername("sa")
|
||||
.WithPassword("P@ssword123!")
|
||||
.Build();
|
||||
|
||||
await Postgres.StartAsync();
|
||||
}
|
||||
|
||||
private async Task CreateDatabaseAsync()
|
||||
{
|
||||
await using var context = new ProjectDbContext(GetDbContextOptions());
|
||||
await context.Database.EnsureDeletedAsync();
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
private async Task SeedDatabaseAsync()
|
||||
{
|
||||
await using var context = new ProjectDbContext(GetDbContextOptions());
|
||||
await context.SeedTestDataAsync();
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task CreateDatabaseSnapshotAsync()
|
||||
{
|
||||
var connectionString = Postgres!.GetConnectionString();
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
RespawnPoint = await Respawner.CreateAsync(
|
||||
connection,
|
||||
new RespawnerOptions
|
||||
{
|
||||
SchemasToInclude = ["public"],
|
||||
DbAdapter = DbAdapter.Postgres
|
||||
});
|
||||
}
|
||||
|
||||
public async Task RespawnAsync()
|
||||
{
|
||||
await RestoreDatabaseSnapshotAsync();
|
||||
await SeedDatabaseAsync();
|
||||
}
|
||||
|
||||
private async Task RestoreDatabaseSnapshotAsync()
|
||||
{
|
||||
var connectionString = Postgres!.GetConnectionString();
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
await RespawnPoint.ResetAsync(connection);
|
||||
}
|
||||
|
||||
private DbContextOptions<ProjectDbContext> GetDbContextOptions()
|
||||
{
|
||||
var connectionString = Postgres!.GetConnectionString();
|
||||
|
||||
var builder = new DbContextOptionsBuilder<ProjectDbContext>();
|
||||
builder.UseNpgsql(connectionString);
|
||||
return builder.Options;
|
||||
}
|
||||
}
|
||||
36
Tasker/Tasker.Web.Tests/Tasker.Web.Tests.csproj
Normal file
36
Tasker/Tasker.Web.Tests/Tasker.Web.Tests.csproj
Normal file
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0-alpha.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.7.24406.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.0-preview.7.24406.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Respawn" Version="6.2.1" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="3.10.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tasker.Web\Tasker.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
21
Tasker/Tasker.Web.Tests/TaskerWebTestBase.cs
Normal file
21
Tasker/Tasker.Web.Tests/TaskerWebTestBase.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Tasker.Web.Tests;
|
||||
|
||||
public class TaskerWebTestBase(
|
||||
InfrastructureFixture fixture)
|
||||
: IClassFixture<InfrastructureFixture>, IAsyncLifetime
|
||||
{
|
||||
protected InfrastructureFixture Fixture { get; } = fixture;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Console.WriteLine("Before each test ?");
|
||||
|
||||
await Fixture.RespawnAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
Console.WriteLine("After each test ?");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
44
Tasker/Tasker.Web.Tests/TaskingDbContextExtensions.cs
Normal file
44
Tasker/Tasker.Web.Tests/TaskingDbContextExtensions.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tests;
|
||||
|
||||
public static class TestData
|
||||
{
|
||||
public static readonly Tasking.Data.Task[] Tasks =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
Name = "Test Task A - Completed",
|
||||
Description = "Test Task Description A - Completed",
|
||||
DueDate = DateTimeOffset.UtcNow,
|
||||
CompletedOn = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
Name = "Test Task B",
|
||||
Description = "Test Task Description B",
|
||||
DueDate = DateTimeOffset.UtcNow,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
Name = "Test Task C",
|
||||
Description = "Test Task Description C",
|
||||
DueDate = DateTimeOffset.UtcNow,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public static class TaskingDbContextExtensions
|
||||
{
|
||||
public static async Task SeedTestDataAsync(
|
||||
this ProjectDbContext? context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
await context.Tasks.AddRangeAsync(TestData.Tasks);
|
||||
}
|
||||
}
|
||||
63
Tasker/Tasker.Web.Tests/WhenGettingMyTasks.cs
Normal file
63
Tasker/Tasker.Web.Tests/WhenGettingMyTasks.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Tasker.Web.Tasking.Endpoints.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tests;
|
||||
|
||||
public class WhenGettingMyTasks(
|
||||
InfrastructureFixture fixture)
|
||||
: TaskerWebTestBase(fixture)
|
||||
{
|
||||
private const int StartingNumberOfTasks = 3;
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureWeHaveTasks()
|
||||
{
|
||||
var tasks = await Fixture.Client.GetFromJsonAsync<TaskDto[]>("tasks");
|
||||
|
||||
tasks.Should().NotBeNull();
|
||||
tasks.Should().HaveCount(StartingNumberOfTasks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureWeCanCreateNewTask()
|
||||
{
|
||||
var newTask = new TaskDto
|
||||
(
|
||||
Id: Guid.NewGuid(),
|
||||
CreatedOn: DateTimeOffset.UtcNow,
|
||||
Name: "New Task",
|
||||
Description: "New Task Description",
|
||||
DueDate: DateTimeOffset.UtcNow
|
||||
);
|
||||
|
||||
var response = await Fixture.Client.PostAsJsonAsync(
|
||||
"tasks",
|
||||
newTask);
|
||||
|
||||
var tasks = await Fixture.Client.GetFromJsonAsync<TaskDto[]>("tasks");
|
||||
|
||||
tasks.Should().Contain(c => c.Id == newTask.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureWeCanCreateNewTask_WithoutDueDate()
|
||||
{
|
||||
var newTask = new TaskDto
|
||||
(
|
||||
Id: Guid.NewGuid(),
|
||||
CreatedOn: DateTimeOffset.UtcNow,
|
||||
Name: "New Task",
|
||||
Description: "New Task Description"
|
||||
);
|
||||
|
||||
var response = await Fixture.Client.PostAsJsonAsync<TaskDto>(
|
||||
"tasks",
|
||||
newTask);
|
||||
|
||||
var tasks = await Fixture.Client.GetFromJsonAsync<TaskDto[]>("tasks");
|
||||
|
||||
tasks.Should().Contain(newTask);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Tasker.Web.Data;
|
||||
|
||||
public class ProjectingDbContext(
|
||||
DbContextOptions options)
|
||||
: DbContext(options)
|
||||
{
|
||||
public DbSet<Task> Tasks { get; set; }
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using Tasker.Web.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record AddTaskRequest(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
DateTimeOffset? DueDate);
|
||||
|
||||
[PublicAPI]
|
||||
public class AddTaskEndpoint(
|
||||
ProjectingDbContext dbContext)
|
||||
: Endpoint<AddTaskRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/tasks");
|
||||
Options(o => o.WithTags("Projecting"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
AddTaskRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var task = await dbContext.Tasks.AddAsync(
|
||||
new Data.Task
|
||||
{
|
||||
Id = req.Id,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
DueDate = req.DueDate
|
||||
},
|
||||
cancellationToken: ct);
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendCreatedAtAsync<GetTaskEndpoint>(
|
||||
task.Entity.Id,
|
||||
task.Entity,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tasker.Web.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public class GetTasksEndpoint(
|
||||
ProjectingDbContext dbContext)
|
||||
: EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/tasks");
|
||||
Options(o => o.WithTags("Projecting"));
|
||||
AllowAnonymous();
|
||||
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tasks = await dbContext
|
||||
.Tasks
|
||||
.Where(t => t.CompletedOn == null)
|
||||
.ToListAsync(cancellationToken: ct);
|
||||
|
||||
await SendOkAsync(
|
||||
tasks,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Tasker.Web.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Migrations
|
||||
{
|
||||
[DbContext(typeof(ProjectingDbContext))]
|
||||
partial class ProjectingDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Data.Task", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tasker.Web.Data;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -8,14 +8,14 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddFastEndpoints();
|
||||
builder.Services.AddDbContext<ProjectingDbContext>(o =>
|
||||
o.UseNpgsql(builder.Configuration.GetConnectionString("ProjectingDbContext")));
|
||||
builder.Services.AddDbContext<ProjectDbContext>(o =>
|
||||
o.UseNpgsql(builder.Configuration.GetConnectionString("TaskingDbContext")));
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowSpecificOrigin",
|
||||
policy =>
|
||||
{
|
||||
policy.WithOrigins("http://localhost:5173")
|
||||
policy.WithOrigins("http://localhost:5173", "http://localhost:5174")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
@@ -41,3 +41,8 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseFastEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program
|
||||
{
|
||||
|
||||
}
|
||||
@@ -4,15 +4,23 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastEndpoints" Version="5.28.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
|
||||
<PackageReference Include="FastEndpoints" Version="5.29.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.3" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Tasking\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Tasker.Web.Data;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Migrations
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ProjectingDbContext))]
|
||||
[DbContext(typeof(ProjectDbContext))]
|
||||
[Migration("20240811051653_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Migrations
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
@@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Tasker.Web.Data;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Migrations
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ProjectingDbContext))]
|
||||
[DbContext(typeof(ProjectDbContext))]
|
||||
[Migration("20240812185703_AddCompletedOn")]
|
||||
partial class AddCompletedOn
|
||||
{
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Migrations
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCompletedOn : Migration
|
||||
143
Tasker/Tasker.Web/Tasking/Data/Migrations/20240909205436_AddCreatedOn.Designer.cs
generated
Normal file
143
Tasker/Tasker.Web/Tasking/Data/Migrations/20240909205436_AddCreatedOn.Designer.cs
generated
Normal file
@@ -0,0 +1,143 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ProjectDbContext))]
|
||||
[Migration("20240909205436_AddCreatedOn")]
|
||||
partial class AddCreatedOn
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Sprint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Sprints");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Task", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("SprintId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("SprintId");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Sprint", b =>
|
||||
{
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Task", b =>
|
||||
{
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Sprint", "Sprint")
|
||||
.WithMany()
|
||||
.HasForeignKey("SprintId");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("Sprint");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCreatedOn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "CreatedOn",
|
||||
table: "Tasks",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ProjectId",
|
||||
table: "Tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "SprintId",
|
||||
table: "Tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Projects",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedOn = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Sprints",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
StartDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
EndDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Sprints", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Sprints_Projects_ProjectId",
|
||||
column: x => x.ProjectId,
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_ProjectId",
|
||||
table: "Tasks",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tasks_SprintId",
|
||||
table: "Tasks",
|
||||
column: "SprintId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sprints_ProjectId",
|
||||
table: "Sprints",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Tasks_Projects_ProjectId",
|
||||
table: "Tasks",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Tasks_Sprints_SprintId",
|
||||
table: "Tasks",
|
||||
column: "SprintId",
|
||||
principalTable: "Sprints",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Tasks_Projects_ProjectId",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Tasks_Sprints_SprintId",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sprints");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Projects");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Tasks_ProjectId",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Tasks_SprintId",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedOn",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProjectId",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SprintId",
|
||||
table: "Tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Tasker/Tasker.Web/Tasking/Data/Migrations/20240910155512_AddsDeletedOn.Designer.cs
generated
Normal file
158
Tasker/Tasker.Web/Tasking/Data/Migrations/20240910155512_AddsDeletedOn.Designer.cs
generated
Normal file
@@ -0,0 +1,158 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ProjectDbContext))]
|
||||
[Migration("20240910155512_AddsDeletedOn")]
|
||||
partial class AddsDeletedOn
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Sprint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Sprints");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Task", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("SprintId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("SprintId");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Sprint", b =>
|
||||
{
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Task", b =>
|
||||
{
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Sprint", "Sprint")
|
||||
.WithMany()
|
||||
.HasForeignKey("SprintId");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("Sprint");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddsDeletedOn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "DeletedOn",
|
||||
table: "Tasks",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "CreatedOn",
|
||||
table: "Sprints",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "DeletedOn",
|
||||
table: "Sprints",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "CompletedOn",
|
||||
table: "Projects",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "DeletedOn",
|
||||
table: "Projects",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletedOn",
|
||||
table: "Tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedOn",
|
||||
table: "Sprints");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletedOn",
|
||||
table: "Sprints");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CompletedOn",
|
||||
table: "Projects");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletedOn",
|
||||
table: "Projects");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tasker.Web.Tasking.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ProjectDbContext))]
|
||||
partial class TaskingDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Sprint", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Sprints");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Task", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("SprintId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("SprintId");
|
||||
|
||||
b.ToTable("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Sprint", b =>
|
||||
{
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tasker.Web.Tasking.Data.Task", b =>
|
||||
{
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.HasOne("Tasker.Web.Tasking.Data.Sprint", "Sprint")
|
||||
.WithMany()
|
||||
.HasForeignKey("SprintId");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("Sprint");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Tasker/Tasker.Web/Tasking/Data/Project.cs
Normal file
13
Tasker/Tasker.Web/Tasking/Data/Project.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tasker.Web.Tasking.Data;
|
||||
|
||||
public class Project
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset? CompletedOn { get; set; }
|
||||
public DateTimeOffset? DeletedOn { get; set; }
|
||||
[MaxLength(255)] public required string Name { get; set; }
|
||||
[MaxLength(2048)] public required string? Description { get; set; }
|
||||
}
|
||||
24
Tasker/Tasker.Web/Tasking/Data/ProjectDbContext.cs
Normal file
24
Tasker/Tasker.Web/Tasking/Data/ProjectDbContext.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Tasker.Web.Tasking.Data;
|
||||
|
||||
public class ProjectDbContext(
|
||||
DbContextOptions<ProjectDbContext> options)
|
||||
: DbContext(options)
|
||||
{
|
||||
public DbSet<Project> Projects { get; set; }
|
||||
public DbSet<Sprint> Sprints { get; set; }
|
||||
public DbSet<Task> Tasks { get; set; }
|
||||
|
||||
protected override void OnModelCreating(
|
||||
ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<Task>()
|
||||
.HasOne<Project>(t => t.Project);
|
||||
|
||||
modelBuilder
|
||||
.Entity<Task>()
|
||||
.HasIndex(t => t.ProjectId);
|
||||
}
|
||||
}
|
||||
15
Tasker/Tasker.Web/Tasking/Data/Sprint.cs
Normal file
15
Tasker/Tasker.Web/Tasking/Data/Sprint.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tasker.Web.Tasking.Data;
|
||||
|
||||
public class Sprint
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Project? Project { get; set; }
|
||||
[MaxLength(255)] public required string Name { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public DateTimeOffset? DeletedOn { get; set; }
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tasker.Web.Data;
|
||||
namespace Tasker.Web.Tasking.Data;
|
||||
|
||||
public class Task
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Project? Project { get; set; }
|
||||
|
||||
public Guid? SprintId { get; set; }
|
||||
public Sprint? Sprint { get; set; }
|
||||
|
||||
|
||||
[MaxLength(255)] public required string Name { get; set; }
|
||||
[MaxLength(2048)] public string? Description { get; set; }
|
||||
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset? DueDate { get; set; }
|
||||
public DateTimeOffset? CompletedOn { get; set; }
|
||||
public DateTimeOffset? DeletedOn { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Tasker.Web.Data;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Endpoints;
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record CompleteTaskRequest(
|
||||
@@ -10,13 +10,13 @@ public record CompleteTaskRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class CompleteTaskEndpoint(
|
||||
ProjectingDbContext dbContext)
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<CompleteTaskRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/tasks/{Id}/complete");
|
||||
Options(o => o.WithTags("Projecting"));
|
||||
Put("/tasks/complete");
|
||||
Options(o => o.WithTags("Tasks"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ public class CompleteTaskEndpoint(
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.CompletedOn is not null)
|
||||
{
|
||||
await SendResultAsync(Results.BadRequest("The task was already completed"));
|
||||
return;
|
||||
}
|
||||
|
||||
task.CompletedOn = req.CompleteOn;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
47
Tasker/Tasker.Web/Tasking/Endpoints/CreateProject.cs
Normal file
47
Tasker/Tasker.Web/Tasking/Endpoints/CreateProject.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record CreateProjectRequest(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description);
|
||||
|
||||
[PublicAPI]
|
||||
public class CreateProjectEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<CreateProjectRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/projects");
|
||||
Options(o => o.WithTags("Projects"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CreateProjectRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var project = await dbContext
|
||||
.Projects
|
||||
.AddAsync(
|
||||
new Project
|
||||
{
|
||||
Id = req.Id,
|
||||
CreatedOn = DateTimeOffset.UtcNow,
|
||||
Name = req.Name,
|
||||
Description = req.Description
|
||||
},
|
||||
cancellationToken: ct);
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendCreatedAtAsync<GetProjectEndpoint>(
|
||||
project.Entity.Id,
|
||||
project.Entity,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
49
Tasker/Tasker.Web/Tasking/Endpoints/CreateTask.cs
Normal file
49
Tasker/Tasker.Web/Tasking/Endpoints/CreateTask.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record CreateTaskRequest(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
DateTimeOffset? DueDate);
|
||||
|
||||
[PublicAPI]
|
||||
public class CreateTaskEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<CreateTaskRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/tasks");
|
||||
Options(o => o.WithTags("Tasks"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CreateTaskRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var task = await dbContext
|
||||
.Tasks
|
||||
.AddAsync(
|
||||
new Data.Task
|
||||
{
|
||||
Id = req.Id,
|
||||
CreatedOn = DateTimeOffset.UtcNow,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
DueDate = req.DueDate?.ToUniversalTime()
|
||||
},
|
||||
cancellationToken: ct);
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendCreatedAtAsync<GetTaskEndpoint>(
|
||||
task.Entity.Id,
|
||||
task.Entity,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
46
Tasker/Tasker.Web/Tasking/Endpoints/DeleteProject.cs
Normal file
46
Tasker/Tasker.Web/Tasking/Endpoints/DeleteProject.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record DeleteProjectRequest(
|
||||
Guid Id);
|
||||
|
||||
[PublicAPI]
|
||||
public class DeleteProjectEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<DeleteProjectRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/projects/{Id}");
|
||||
Options(o => o.WithTags("Projects"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
DeleteProjectRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var project = await dbContext
|
||||
.Projects
|
||||
.FindAsync(
|
||||
[req.Id],
|
||||
ct);
|
||||
|
||||
if (project is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
project.DeletedOn = DateTimeOffset.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
project,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
46
Tasker/Tasker.Web/Tasking/Endpoints/DeleteTask.cs
Normal file
46
Tasker/Tasker.Web/Tasking/Endpoints/DeleteTask.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record DeleteTaskRequest(
|
||||
Guid Id);
|
||||
|
||||
[PublicAPI]
|
||||
public class DeleteTaskEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<DeleteTaskRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/tasks/{Id}");
|
||||
Options(o => o.WithTags("Tasks"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
DeleteTaskRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var task = await dbContext
|
||||
.Tasks
|
||||
.FindAsync(
|
||||
[req.Id],
|
||||
ct);
|
||||
|
||||
if (task is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
task.DeletedOn = DateTimeOffset.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
task,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
44
Tasker/Tasker.Web/Tasking/Endpoints/GetProject.cs
Normal file
44
Tasker/Tasker.Web/Tasking/Endpoints/GetProject.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Tasker.Web.Tasking.Endpoints.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record GetProjectRequest(
|
||||
Guid Id,
|
||||
DateTimeOffset CreatedOn,
|
||||
string Name,
|
||||
string? Description);
|
||||
|
||||
[PublicAPI]
|
||||
public class GetProjectEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<GetProjectRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/projects/{Id}");
|
||||
Options(o => o.WithTags("Projects"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
GetProjectRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var task = await dbContext
|
||||
.Projects
|
||||
.FindAsync(
|
||||
[req.Id],
|
||||
cancellationToken: ct);
|
||||
|
||||
await SendOkAsync(
|
||||
new ProjectDto(
|
||||
task.Id,
|
||||
task.CreatedOn,
|
||||
task.Name,
|
||||
task.Description),
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
37
Tasker/Tasker.Web/Tasking/Endpoints/GetProjects.cs
Normal file
37
Tasker/Tasker.Web/Tasking/Endpoints/GetProjects.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Tasker.Web.Tasking.Endpoints.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public class GetProjectsEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: EndpointWithoutRequest<ProjectDto[]>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/projects");
|
||||
Options(o => o.WithTags("Projects"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var projects = await dbContext
|
||||
.Projects
|
||||
.OrderByDescending(task => task.CreatedOn)
|
||||
.Select(task => new ProjectDto(
|
||||
task.Id,
|
||||
task.CreatedOn,
|
||||
task.Name,
|
||||
task.Description))
|
||||
.ToArrayAsync(cancellationToken: ct);
|
||||
|
||||
await SendOkAsync(
|
||||
projects,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Tasker.Web.Data;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Endpoints;
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record GetTaskRequest(
|
||||
@@ -9,21 +9,25 @@ public record GetTaskRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class GetTaskEndpoint(
|
||||
ProjectingDbContext dbContext)
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<GetTaskRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/tasks/{Id}");
|
||||
Options(o => o.WithTags("Projecting"));
|
||||
Options(o => o.WithTags("Tasks"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
GetTaskRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var task = await dbContext.FindAsync<GetTaskRequest>(
|
||||
req.Id);
|
||||
var task = await dbContext
|
||||
.Tasks
|
||||
.FindAsync(
|
||||
[req.Id],
|
||||
cancellationToken: ct);
|
||||
|
||||
await SendOkAsync(
|
||||
task,
|
||||
39
Tasker/Tasker.Web/Tasking/Endpoints/GetTasks.cs
Normal file
39
Tasker/Tasker.Web/Tasking/Endpoints/GetTasks.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Tasker.Web.Tasking.Endpoints.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public class GetTasksEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: EndpointWithoutRequest<TaskDto[]>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/tasks");
|
||||
Options(o => o.WithTags("Tasks"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tasks = await dbContext
|
||||
.Tasks
|
||||
.OrderByDescending(task => task.CompletedOn)
|
||||
.Select(task => new TaskDto(
|
||||
task.Id,
|
||||
task.CreatedOn,
|
||||
task.Name,
|
||||
task.Description,
|
||||
task.DueDate,
|
||||
task.CompletedOn))
|
||||
.ToArrayAsync(cancellationToken: ct);
|
||||
|
||||
await SendOkAsync(
|
||||
tasks,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
8
Tasker/Tasker.Web/Tasking/Endpoints/Models/ProjectDto.cs
Normal file
8
Tasker/Tasker.Web/Tasking/Endpoints/Models/ProjectDto.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Tasker.Web.Tasking.Endpoints.Models;
|
||||
|
||||
[PublicAPI]
|
||||
public record struct ProjectDto(
|
||||
Guid Id,
|
||||
DateTimeOffset CreatedOn,
|
||||
string Name,
|
||||
string? Description);
|
||||
10
Tasker/Tasker.Web/Tasking/Endpoints/Models/TaskDto.cs
Normal file
10
Tasker/Tasker.Web/Tasking/Endpoints/Models/TaskDto.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Tasker.Web.Tasking.Endpoints.Models;
|
||||
|
||||
[PublicAPI]
|
||||
public record TaskDto(
|
||||
Guid Id,
|
||||
DateTimeOffset CreatedOn,
|
||||
string Name,
|
||||
string? Description,
|
||||
DateTimeOffset? DueDate = null,
|
||||
DateTimeOffset? CompletedOn = null);
|
||||
47
Tasker/Tasker.Web/Tasking/Endpoints/RenameDescription.cs
Normal file
47
Tasker/Tasker.Web/Tasking/Endpoints/RenameDescription.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record RenameDescriptionRequest(
|
||||
Guid Id,
|
||||
string? Description);
|
||||
|
||||
[PublicAPI]
|
||||
public class RenameDescriptionEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<RenameDescriptionRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/projects/{Id}/description");
|
||||
Options(o => o.WithTags("Projects"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
RenameDescriptionRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var project = await dbContext
|
||||
.Projects
|
||||
.FindAsync(
|
||||
[req.Id],
|
||||
cancellationToken: ct);
|
||||
|
||||
if (project is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
project.Description = req.Description;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
project,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
47
Tasker/Tasker.Web/Tasking/Endpoints/RenameProject.cs
Normal file
47
Tasker/Tasker.Web/Tasking/Endpoints/RenameProject.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record RenameProjectRequest(
|
||||
Guid Id,
|
||||
string Name);
|
||||
|
||||
[PublicAPI]
|
||||
public class RenameProjectEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<RenameProjectRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/projects/{Id}/name");
|
||||
Options(o => o.WithTags("Projects"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
RenameProjectRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var project = await dbContext
|
||||
.Projects
|
||||
.FindAsync(
|
||||
[req.Id],
|
||||
cancellationToken: ct);
|
||||
|
||||
if (project is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
project.Name = req.Name;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
project,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
52
Tasker/Tasker.Web/Tasking/Endpoints/UnCompleteTask.cs
Normal file
52
Tasker/Tasker.Web/Tasking/Endpoints/UnCompleteTask.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Tasker.Web.Tasking.Data;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tasker.Web.Tasking.Endpoints;
|
||||
|
||||
[PublicAPI]
|
||||
public record UnCompleteTaskRequest(
|
||||
Guid Id);
|
||||
|
||||
[PublicAPI]
|
||||
public class UnCompleteTaskEndpoint(
|
||||
ProjectDbContext dbContext)
|
||||
: Endpoint<UnCompleteTaskRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/tasks/uncomplete");
|
||||
Options(o => o.WithTags("Tasks"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
UnCompleteTaskRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var task = await dbContext
|
||||
.Tasks
|
||||
.FindAsync(
|
||||
[req.Id],
|
||||
cancellationToken: ct);
|
||||
|
||||
if (task is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.CompletedOn is null)
|
||||
{
|
||||
await SendResultAsync(Results.BadRequest("The task was already un-completed"));
|
||||
return;
|
||||
}
|
||||
|
||||
task.CompletedOn = null;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
task,
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,6 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"ProjectingDbContext": "Server=127.0.0.1;Port=5432;Database=Tasker;User Id=sa;Password=P@ssword123!;"
|
||||
"TaskingDbContext": "Server=127.0.0.1;Port=5400;Database=Tasker;User Id=sa;Password=P@ssword123!;"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,48 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tasker.Web", "Tasker.Web\Tasker.Web.csproj", "{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}"
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tasker.Web", "Tasker.Web\Tasker.Web.csproj", "{301B9679-0E9A-44F3-A83C-58ABF05F38E1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tasker.Web.Tests", "Tasker.Web.Tests\Tasker.Web.Tests.csproj", "{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Release|x64.Build.0 = Release|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F2F5446E-3DF3-4F9E-AD5C-4D90E4A44B97}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AActivatorUtilities_002Ecs_002Fl_003AC_0021_003FUsers_003Flowra_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb3bfd1dceaf91cf199c7d231f873541c39abbdffae998d84b37f373cf411_003FActivatorUtilities_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003Flowra_003FAppData_003FLocal_003FSymbols_003Fsrc_003Fdotnet_003Fefcore_003F0d1256be4658567c8a24b4c027bdbb3dbd6de656_003Fsrc_003FEFCore_003FExtensions_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIListSource_002Ecs_002Fl_003AC_0021_003FUsers_003Flowra_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F95d8801ed7894a44a3b73e8b963694f8bd910_003Fb0_003Ff108f363_003FIListSource_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOpenApiEndpointRouteBuilderExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003Flowra_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F5afdf3ad3bf2cb0b47575971c4deac89885ddeb3b2444d56faa3cbd78cd_003FOpenApiEndpointRouteBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollection_002Ecs_002Fl_003AC_0021_003FUsers_003Flowra_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F595e4f2361fd31cb66c7e9303f46cf8f2fe77d96eea9d0e42c17cae783aa74_003FServiceCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003AC_0021_003FUsers_003Flowra_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F658ad1fa108317d78bcae889b6e4f9dd5327babb6582972f2dbeabf36dafa5_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||
1868
Tasker/dotnet-install.sh
vendored
Normal file
1868
Tasker/dotnet-install.sh
vendored
Normal file
File diff suppressed because it is too large
Load Diff
24
start-infrastructure.sh
Normal file
24
start-infrastructure.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set global configuration
|
||||
DATA_ROOT="/var/tasker"
|
||||
POSTGRES_VERSION="latest"
|
||||
MONGODB_VERSION="latest"
|
||||
|
||||
# Start PostgreSQL
|
||||
docker run -d \
|
||||
--name TASKER_POSTGRES \
|
||||
-v "${DATA_ROOT}/postgres:/var/lib/postgresql/data" \
|
||||
-p 5400:5432 \
|
||||
-e POSTGRES_USER=sa \
|
||||
-e POSTGRES_PASSWORD=P@ssword123! \
|
||||
-e POSTGRES_DB=Tasker \
|
||||
postgres:$POSTGRES_VERSION
|
||||
|
||||
# Start MongoDB
|
||||
docker run -d \
|
||||
--name TASKER_MONGODB \
|
||||
-v "${DATA_ROOT}/mongodb:/data/db" \
|
||||
-p 5401:27017 \
|
||||
mongo:$MONGODB_VERSION
|
||||
|
||||
Reference in New Issue
Block a user