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" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Welcome to Vuetify 3</title>
|
<title>Done By Daylight</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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": {
|
"dependencies": {
|
||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.7.7",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"uuidv7": "^1.0.1",
|
"uuidv7": "^1.0.1",
|
||||||
"vue": "^3.4.29",
|
"vue": "^3.4.29",
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
value="users"
|
value="users"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
|
<project-section></project-section>
|
||||||
|
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<v-app-bar app
|
<v-app-bar app
|
||||||
@@ -67,31 +70,14 @@
|
|||||||
</v-app>
|
</v-app>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
m
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import ProjectSection from '@/components/ProjectSection.vue'
|
||||||
|
|
||||||
const drawer = ref(true)
|
const drawer = ref(true)
|
||||||
const rail = ref(false)
|
const rail = ref(false)
|
||||||
const group = ref(null)
|
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, () => {
|
watch(group, () => {
|
||||||
drawer.value = false
|
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">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { type Task, useTasks } from '@/api/taskApi'
|
|
||||||
import { uuidv7 } from 'uuidv7'
|
import { uuidv7 } from 'uuidv7'
|
||||||
|
import DueDatePicker from '@/components/DueDatePicker.vue'
|
||||||
|
import { useTaskStore } from '@/stores/taskStore'
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref<string>('')
|
||||||
const description = ref(null)
|
const description = ref<string | null>(null)
|
||||||
const dueDate = ref(null)
|
const dueDate = ref<string | null>(null)
|
||||||
|
|
||||||
const { addTask } = useTasks()
|
const modalDueDatePicker = ref<boolean>(false)
|
||||||
|
|
||||||
const doAddTask = () => {
|
function changeDueDate(): void {
|
||||||
const newTask: Task = {
|
modalDueDatePicker.value = !modalDueDatePicker.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDueDateChanged(value: string): void {
|
||||||
|
dueDate.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskStore = useTaskStore()
|
||||||
|
|
||||||
|
function doAddTask(): Task {
|
||||||
|
taskStore.addTask({
|
||||||
id: uuidv7(),
|
id: uuidv7(),
|
||||||
name: name.value,
|
name: name.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
dueDate: dueDate.value
|
dueDate: dueDate.value
|
||||||
}
|
} as Task)
|
||||||
addTask(newTask)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelAddTask = () => {
|
const cancelAddTask = () => {
|
||||||
@@ -39,7 +49,16 @@ const cancelAddTask = () => {
|
|||||||
|
|
||||||
<div class="flex flex-row space-x-2">
|
<div class="flex flex-row space-x-2">
|
||||||
|
|
||||||
|
<v-btn @click="changeDueDate()">
|
||||||
<span>due-date</span>
|
<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>priority</span>
|
||||||
<span>tags</span>
|
<span>tags</span>
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
<script setup lang="ts">
|
<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 toggleTaskComplete(task: Task) {
|
||||||
|
taskStore.completeTask(task) // Call the API to save the change
|
||||||
function itemCompleted(id: string) {
|
|
||||||
console.log(`completing task: ${id}`)
|
|
||||||
completeTask(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-list v-if="!isLoading && !error">
|
<v-form v-if="!taskStore.isLoading && !taskStore.error">
|
||||||
<v-list-item v-for="task in tasks" :key="task.id">
|
<div class="flex p-0 m-0 bg-amber"
|
||||||
<div class="flex p-1 bg-amber">
|
v-for="task in taskStore.tasks" :key="task.id">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<v-checkbox-btn @change="itemCompleted(task.id)"
|
<v-checkbox-btn :model-value="task.completedOn !== null"
|
||||||
|
@change="toggleTaskComplete(task)"
|
||||||
></v-checkbox-btn>
|
></v-checkbox-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-column">
|
<div class="flex-column">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="font-sans">{{ task.name }}</div>
|
<div class="font-sans font-medium">{{ task.name }}</div>
|
||||||
<div class="font-serif font-normal">Description {{ task.description }}</div>
|
<div class="font-sans font-normal">{{ task.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>{{ task.dueDate }}</div>
|
<div>{{ task.dueDate }}</div>
|
||||||
<!-- <div>{{ task.project.Name }}</div>-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</v-list-item>
|
</v-form>
|
||||||
</v-list>
|
|
||||||
<v-alert v-else-if="error" type="error">
|
<v-alert v-else-if="error" type="error">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<v-progress-circular v-else indeterminate></v-progress-circular>
|
<v-progress-circular v-else indeterminate></v-progress-circular>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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">
|
<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'
|
import TaskList from '@/components/TaskList.vue'
|
||||||
|
|
||||||
const counterStore = useCounterStore()
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
|
<v-card class="m-2 p-2">
|
||||||
<task-input></task-input>
|
<task-input></task-input>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card class="m-2 p-2">
|
||||||
<task-list></task-list>
|
<task-list></task-list>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
</template>
|
</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 Microsoft.EntityFrameworkCore;
|
||||||
using Tasker.Web.Data;
|
using Tasker.Web.Tasking.Data;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -8,14 +8,14 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
builder.Services.AddFastEndpoints();
|
builder.Services.AddFastEndpoints();
|
||||||
builder.Services.AddDbContext<ProjectingDbContext>(o =>
|
builder.Services.AddDbContext<ProjectDbContext>(o =>
|
||||||
o.UseNpgsql(builder.Configuration.GetConnectionString("ProjectingDbContext")));
|
o.UseNpgsql(builder.Configuration.GetConnectionString("TaskingDbContext")));
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("AllowSpecificOrigin",
|
options.AddPolicy("AllowSpecificOrigin",
|
||||||
policy =>
|
policy =>
|
||||||
{
|
{
|
||||||
policy.WithOrigins("http://localhost:5173")
|
policy.WithOrigins("http://localhost:5173", "http://localhost:5174")
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod();
|
.AllowAnyMethod();
|
||||||
});
|
});
|
||||||
@@ -41,3 +41,8 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.UseFastEndpoints();
|
app.UseFastEndpoints();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
public partial class Program
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,15 +4,23 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FastEndpoints" Version="5.28.0" />
|
<PackageReference Include="FastEndpoints" Version="5.29.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.3" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
|
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Tasker.Web.Data;
|
using Tasker.Web.Tasking.Data;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Tasker.Web.Migrations
|
namespace Tasker.Web.Tasking.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ProjectingDbContext))]
|
[DbContext(typeof(ProjectDbContext))]
|
||||||
[Migration("20240811051653_Initial")]
|
[Migration("20240811051653_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Tasker.Web.Migrations
|
namespace Tasker.Web.Tasking.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class Initial : Migration
|
public partial class Initial : Migration
|
||||||
@@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Tasker.Web.Data;
|
using Tasker.Web.Tasking.Data;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Tasker.Web.Migrations
|
namespace Tasker.Web.Tasking.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ProjectingDbContext))]
|
[DbContext(typeof(ProjectDbContext))]
|
||||||
[Migration("20240812185703_AddCompletedOn")]
|
[Migration("20240812185703_AddCompletedOn")]
|
||||||
partial class AddCompletedOn
|
partial class AddCompletedOn
|
||||||
{
|
{
|
||||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Tasker.Web.Migrations
|
namespace Tasker.Web.Tasking.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class AddCompletedOn : Migration
|
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;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Tasker.Web.Data;
|
namespace Tasker.Web.Tasking.Data;
|
||||||
|
|
||||||
public class Task
|
public class Task
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
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(255)] public required string Name { get; set; }
|
||||||
[MaxLength(2048)] public string? Description { get; set; }
|
[MaxLength(2048)] public string? Description { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedOn { get; set; }
|
||||||
public DateTimeOffset? DueDate { get; set; }
|
public DateTimeOffset? DueDate { get; set; }
|
||||||
public DateTimeOffset? CompletedOn { 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;
|
using Task = System.Threading.Tasks.Task;
|
||||||
|
|
||||||
namespace Tasker.Web.Endpoints;
|
namespace Tasker.Web.Tasking.Endpoints;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record CompleteTaskRequest(
|
public record CompleteTaskRequest(
|
||||||
@@ -10,13 +10,13 @@ public record CompleteTaskRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class CompleteTaskEndpoint(
|
public class CompleteTaskEndpoint(
|
||||||
ProjectingDbContext dbContext)
|
ProjectDbContext dbContext)
|
||||||
: Endpoint<CompleteTaskRequest>
|
: Endpoint<CompleteTaskRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
Post("/tasks/{Id}/complete");
|
Put("/tasks/complete");
|
||||||
Options(o => o.WithTags("Projecting"));
|
Options(o => o.WithTags("Tasks"));
|
||||||
AllowAnonymous();
|
AllowAnonymous();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +36,12 @@ public class CompleteTaskEndpoint(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (task.CompletedOn is not null)
|
||||||
|
{
|
||||||
|
await SendResultAsync(Results.BadRequest("The task was already completed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
task.CompletedOn = req.CompleteOn;
|
task.CompletedOn = req.CompleteOn;
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
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;
|
using Task = System.Threading.Tasks.Task;
|
||||||
|
|
||||||
namespace Tasker.Web.Endpoints;
|
namespace Tasker.Web.Tasking.Endpoints;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record GetTaskRequest(
|
public record GetTaskRequest(
|
||||||
@@ -9,21 +9,25 @@ public record GetTaskRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetTaskEndpoint(
|
public class GetTaskEndpoint(
|
||||||
ProjectingDbContext dbContext)
|
ProjectDbContext dbContext)
|
||||||
: Endpoint<GetTaskRequest>
|
: Endpoint<GetTaskRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
Get("/tasks/{Id}");
|
Get("/tasks/{Id}");
|
||||||
Options(o => o.WithTags("Projecting"));
|
Options(o => o.WithTags("Tasks"));
|
||||||
|
AllowAnonymous();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task HandleAsync(
|
public override async Task HandleAsync(
|
||||||
GetTaskRequest req,
|
GetTaskRequest req,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var task = await dbContext.FindAsync<GetTaskRequest>(
|
var task = await dbContext
|
||||||
req.Id);
|
.Tasks
|
||||||
|
.FindAsync(
|
||||||
|
[req.Id],
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
await SendOkAsync(
|
await SendOkAsync(
|
||||||
task,
|
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": {
|
"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
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{301B9679-0E9A-44F3-A83C-58ABF05F38E1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{5718F5B9-8EF2-4FE6-ABA4-21BC699F4023}.Release|Any CPU.Build.0 = Release|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
|
EndGlobalSection
|
||||||
EndGlobal
|
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