Skip to content

Commit

Permalink
Implement due dates
Browse files Browse the repository at this point in the history
  • Loading branch information
NoelDeMartin committed May 31, 2024
1 parent 0d23edc commit 5e4a9c8
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 61 deletions.
41 changes: 37 additions & 4 deletions cypress/e2e/tasks.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,53 @@ describe('Tasks', () => {
cy.ariaInput('Task name').type('Cook Ramen{enter}');
cy.see('Cook Ramen');

// Act
// Act - Make important
cy.ariaLabel('Select task \\"Cook Ramen\\"').click();
cy.ariaLabel('Make important').click();

// Assert
// Assert - Make important
cy.see('Cook Ramen (Important)');

// Act
// Act - Remove important
cy.ariaLabel('Remove important').click();

// Assert
// Assert - Remove important
cy.dontSee('Cook Ramen (Important)');
});

it('Edits due dates', () => {
// Arrange
cy.ariaInput('Task name').type('Cook Ramen{enter}');
cy.see('Cook Ramen');

// Act - Set due date
cy.ariaLabel('Select task \\"Cook Ramen\\"').click();
cy.ariaLabel('Edit due date').click();
cy.get(':focus').then((element) => {
const input = element[0] as HTMLInputElement;

input.valueAsDate = new Date();
input.dispatchEvent(new Event('input'));
});
cy.press('Save');

// Assert - Set due date
cy.see('Today');

// Act - Remove due date
cy.ariaLabel('Edit due date').click();
cy.get(':focus').then((element) => {
const input = element[0] as HTMLInputElement;

input.value = '';
input.dispatchEvent(new Event('input'));
});
cy.press('Save');

// Assert - Remove due date
cy.dontSee('Today');
});

it('Completes tasks', () => {
// Arrange
cy.ariaInput('Task name').type('Cook Ramen{enter}');
Expand Down
28 changes: 20 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
"test:serve-pod": "community-solid-server -l warn"
},
"dependencies": {
"@aerogel/core": "0.0.0-next.69eebcf68812a1337f39653f33322a964264443f",
"@aerogel/core": "0.0.0-next.6285bb4a6b117b7b2d9c3e953c603619988fe23a",
"@aerogel/plugin-i18n": "0.0.0-next.464f8d4bc58710df35b52ea396ccd8c40b73c664",
"@aerogel/plugin-offline-first": "0.0.0-next.8e6b2bcc764fa682decbb41aa6848c77a744dec3",
"@aerogel/plugin-routing": "next",
"@aerogel/plugin-solid": "0.0.1-next.9a02fcd3bcf698211dd7a71d4c48257c96dd7832",
"@aerogel/plugin-soukai": "0.0.0-next.71f28064caa2ea968f0e99396b672de218176260",
"@intlify/unplugin-vue-i18n": "^0.12.2",
"@noeldemartin/solid-utils": "0.4.0-next.852c9f9e65275fc2a2e67a9750784fb43a0fd64b",
"@noeldemartin/utils": "0.5.1-next.d01c5dd7a42656e2ed6f6a0279c98dd6e2ff5a10",
"@noeldemartin/utils": "0.5.1-next.92ae7373c654e3b09dad133ccda7b28f678e98b4",
"@solid/community-server": "^7.0.5",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
Expand Down
56 changes: 56 additions & 0 deletions src/components/DateInput.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<Story group="forms" :layout="{ type: 'grid' }">
<Variant title="Playground">
<AGForm :form="form">
<DateInput name="date" :label="label" />
</AGForm>

<template #controls>
<HstText v-model="label" title="Label" />
<HstCheckbox v-model="hasErrors" title="Errors" />
</template>
</Variant>

<Variant title="Default">
<DateInput label="What time is it?" />
</Variant>

<Variant title="Hover">
<DateInput label="What time is it?" input-class=":hover" />
</Variant>

<Variant title="Focus">
<DateInput label="What time is it?" input-class=":focus :focus-visible" />
</Variant>

<Variant title="Error">
<AGForm :form="errorForm">
<DateInput name="date" label="What time is it?" class=":focus :focus-visible" />
</AGForm>
</Variant>
</Story>
</template>

<script setup lang="ts">
import { requiredDateInput, useForm } from '@aerogel/core';
import { ref, watchEffect } from 'vue';
const form = useForm({ date: requiredDateInput() });
const errorForm = useForm({ date: requiredDateInput() });
const label = ref('What time is it?');
const hasErrors = ref(false);
errorForm.submit();
watchEffect(() => (hasErrors.value ? form.submit() : form.reset()));
</script>

<style>
.story-dateinput {
grid-template-columns: repeat(2, 300px) !important;
}
.story-dateinput .variant-playground {
grid-column: 1 / -1;
}
</style>
36 changes: 36 additions & 0 deletions src/components/DateInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<template>
<AGHeadlessInput ref="$input" v-bind="inputProps" :class="className">
<AGHeadlessInputLabel class="block text-sm font-medium leading-6 text-gray-900" />
<div :class="$input?.label && 'mt-2'">
<AGHeadlessInputInput type="date" v-bind="attrs" :class="renderedInputClass" />
<AGHeadlessInputDescription />
<AGHeadlessInputError class="mt-1 text-sm text-red-600" />
</div>
</AGHeadlessInput>
</template>

<script setup lang="ts">
import { componentRef, extractInputProps, stringProp, useInputAttrs, useInputProps } from '@aerogel/core';
import { twMerge } from 'tailwind-merge';
import { computed } from 'vue';
import type { IAGHeadlessInput } from '@aerogel/core';
defineOptions({ inheritAttrs: false });
const props = defineProps({
...useInputProps(),
inputClass: stringProp(''),
});
const inputProps = extractInputProps(props);
const $input = componentRef<IAGHeadlessInput>();
const [attrs, className] = useInputAttrs();
const renderedInputClass = computed(() =>
twMerge(
[
'w-full rounded-md border-0 px-2 py-1.5',
'text-base text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300',
'placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-[--primary-600]',
].join(' '),
props.inputClass,
));
</script>
15 changes: 10 additions & 5 deletions src/components/TextInput.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<template>
<AGHeadlessInput ref="$input" v-bind="inputProps">
<AGHeadlessInput ref="$input" v-bind="inputProps" :class="className">
<AGHeadlessInputLabel class="block text-sm font-medium leading-6 text-gray-900" />
<div :class="$input?.label && 'mt-2'">
<div class="relative">
<div :class="renderedFillerClass">
{{ $input?.value }}&nbsp;
</div>
<AGHeadlessInputTextArea v-bind="attrs" :class="renderedInputClass" />
<AGHeadlessInputTextArea
v-bind="attrs"
:class="renderedInputClass"
@keydown.enter.prevent="form?.submit()"
/>
</div>
<AGHeadlessInputDescription />
<AGHeadlessInputError class="mt-1 text-sm text-red-600" />
Expand All @@ -16,19 +20,20 @@

<script setup lang="ts">
import { componentRef, extractInputProps, stringProp, useInputAttrs, useInputProps } from '@aerogel/core';
import { computed, inject } from 'vue';
import { twMerge } from 'tailwind-merge';
import { computed } from 'vue';
import type { IAGHeadlessInput } from '@aerogel/core';
import type { Form, IAGHeadlessInput } from '@aerogel/core';
defineOptions({ inheritAttrs: false });
const props = defineProps({
...useInputProps(),
inputClass: stringProp(''),
});
const form = inject<Form | null>('form', null);
const inputProps = extractInputProps(props);
const $input = componentRef<IAGHeadlessInput>();
const [attrs] = useInputAttrs();
const [attrs, className] = useInputAttrs();
const renderedFillerClass = computed(() => twMerge('invisible whitespace-pre-wrap px-2 py-1.5', props.inputClass));
const renderedInputClass = computed(() =>
twMerge(
Expand Down
10 changes: 9 additions & 1 deletion src/lang/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,28 @@ tasks:
completed: Completed
showCompleted: Show completed
hideCompleted: Hide completed
created: Created on {date}

task:
emptyDescription: No description
editName: Edit name
editDescription: Edit description
editDueDate: Edit due date
save: Save
cancel: Cancel
close: Close
created: Created on {date}
remove: Remove
confirmRemove: Are you sure you want to delete this task?
due: Due on {date}
notDue: No due date
important: Important
notImportant: Not important
importantA11y: This task is marked as important
notImportantA11y: This task is not marked as important
makeImportant: Make important
removeImportant: Remove important

time:
yesterday: Yesterday
today: Today
tomorrow: Tomorrow
4 changes: 4 additions & 0 deletions src/models/Task.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export default defineSolidModelSchema({
rdfProperty: 'tasks:important',
type: FieldType.Boolean,
},
dueDate: {
rdfProperty: 'tasks:dueDate',
type: FieldType.Date,
},
completedAt: {
rdfProperty: 'tasks:completedAt',
type: FieldType.Date,
Expand Down
24 changes: 18 additions & 6 deletions src/pages/workspace/WorkspaceContentBody.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<div class="px-4">
<TaskCreateForm @submit="createTask" />
<TasksList :tasks="tasks.pending ?? []" :disable-editing="disableEditing" class="mt-4" />
<div v-if="tasks.completed?.length" class="mt-4">
<TasksList :tasks="tasks.pending" :disable-editing="disableEditing" class="mt-4" />
<div v-if="tasks.completed.length" class="mt-4">
<TextButton
color="clear"
class="ml-1 rounded-lg pl-1 pr-2 font-medium uppercase tracking-wider"
Expand All @@ -26,10 +26,10 @@
</template>

<script setup lang="ts">
import { arrayGroupBy } from '@noeldemartin/utils';
import { arrayGroupBy, compare } from '@noeldemartin/utils';
import { Cloud } from '@aerogel/plugin-offline-first';
import { computed, ref } from 'vue';
import { computedModels } from '@aerogel/plugin-soukai';
import { ref } from 'vue';
import Task from '@/models/Task';
import TasksLists from '@/services/TasksLists';
Expand All @@ -38,8 +38,20 @@ import { watchKeyboardShortcut } from '@/utils/composables';
const showCompleted = ref(false);
const disableEditing = ref(false);
const tasks = computedModels(Task, () =>
const groupedTasks = computedModels(Task, () =>
arrayGroupBy(TasksLists.current?.tasks ?? [], (task) => (task.completed ? 'completed' : 'pending')));
const tasks = computed(() => ({
pending: groupedTasks.value.pending?.toSorted(compareTasks) ?? [],
completed: groupedTasks.value.completed?.toSorted(compareTasks) ?? [],
}));
function compareTasks(a: Task, b: Task): number {
const importantComparison = compare(b.important, a.important);
const dueDateComparison = !a.dueDate || !b.dueDate ? compare(b.dueDate, a.dueDate) : compare(a.dueDate, b.dueDate);
const dateComparison = a.completed ? compare(b.completedAt, a.completedAt) : compare(a.createdAt, b.createdAt);
return [importantComparison, dueDateComparison, dateComparison].find((result) => result !== 0) ?? 0;
}
async function createTask(name: string) {
const tasksList = TasksLists.current;
Expand All @@ -54,7 +66,7 @@ async function createTask(name: string) {
}
function changeTask(delta: 1 | -1) {
const tasksList = tasks.value.pending?.concat(showCompleted.value ? tasks.value.completed ?? [] : []) ?? [];
const tasksList = tasks.value.pending.concat(showCompleted.value ? tasks.value.completed : []);
const select = (task?: Task) => task && Workspaces.select(task);
if (!Workspaces.task) {
Expand Down
Loading

0 comments on commit 5e4a9c8

Please sign in to comment.