Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions TODO
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
Quick list

Better bullet points
- hitting enter creates next line with new point of same style
- nested bullet points
Revisit typing. We should have internal types not bound to PouchDB structures that mean the slug against the repeatable are cleanly typed with no ambiguiity.

Button to edit template on repeatable view. Will edit existing repeatables. This has downsides because repeatable data is just an array of booleans, not a map. Think about that but probably don't care for now.

Expand Down
23 changes: 0 additions & 23 deletions jest.config.js

This file was deleted.

6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
"version": "0.5.0",
"packageManager": "yarn@4.0.2",
"//": [
" TODO update everything else thing by thing",
" TODO migrate off heroku (fly.io?)",
" TODO redux -> https://github.com/pmndrs/zustand"
],
"scripts": {
Expand All @@ -19,8 +17,8 @@
"build:server": "parcel build --target server --reporter @parcel/reporter-bundle-analyzer",
"build": "yarn clean && parcel build --reporter @parcel/reporter-bundle-analyzer && node ./scripts/inject-manifest.mjs",
"dev": "parcel --target client",
"test:client": "vitest --config src/client/vitest.config.js src/client",
"test:server": "vitest --config src/server/vitest.config.js src/server",
"test:client": "vitest --config src/client/vitest.config.ts src/client",
"test:server": "vitest --config src/server/vitest.config.ts src/server",
"test": "yarn test:client --run && yarn test:server --run"
},
"targets": {
Expand Down
2 changes: 1 addition & 1 deletion src/client/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('App Routing', () => {
title: 'Test Template',
markdown: 'Test markdown',
values: [],
slug: { type: SlugType.Date, placeholder: '' },
slug: { type: SlugType.Date },
created: Date.now(),
updated: Date.now(),
versioned: Date.now(),
Expand Down
19 changes: 17 additions & 2 deletions src/client/db/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { vi } from 'vitest';
import { type Mock, vi } from 'vitest';
import type { Doc } from '../../../shared/types';

const mockDb = {
export interface MockDatabase {
find: Mock<(options?: unknown) => Promise<{ docs: unknown[] }>>;
get: Mock<(docId: string) => Promise<unknown>>;
userPut: Mock<(doc: Doc) => Promise<Doc>>;
}

const mockDb: MockDatabase = {
find: vi.fn(),
get: vi.fn(),
userPut: vi.fn(),
};

/**
* Get the mock database handle. Use this in tests after vi.mock('../db').
* Returns the same mock instance regardless of the user argument.
*/
export function getMockDb(): MockDatabase {
return mockDb;
}

const db = () => mockDb;

export default db;
58 changes: 0 additions & 58 deletions src/client/features/Repeatable/RepeatableListItem.jsx

This file was deleted.

24 changes: 12 additions & 12 deletions src/client/features/Repeatable/RepeatableListItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import userEvent from '@testing-library/user-event';

import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { SlugType } from '../../../shared/types';
import { render } from '../../test-utils';
import RepeatableListItem from './RepeatableListItem';
import RepeatableListItem, { type RepeatableListItemProps } from './RepeatableListItem';

vi.mock('react-router-dom');

Expand All @@ -18,15 +19,14 @@ describe('Repeatable', () => {
});

it('renders without crashing', async () => {
const params = {
const params: RepeatableListItemProps = {
_id: 'abc',
timestamp: Date.now(),
slug: 'http://example.com',
template: {
title: 'Checklist ListItem',
slug: {
type: 'url',
label: 'For',
type: SlugType.URL,
},
},
};
Expand All @@ -38,14 +38,14 @@ describe('Repeatable', () => {
});

it('url slug renders (and does not effect slug click)', async () => {
const params = {
const params: RepeatableListItemProps = {
_id: 'abc',
timestamp: Date.now(),
slug: 'http://example.com',
template: {
title: 'URL Test',
slug: {
type: 'url',
type: SlugType.URL,
},
},
};
Expand All @@ -58,14 +58,14 @@ describe('Repeatable', () => {
});

it('date slug', async () => {
const params = {
const params: RepeatableListItemProps = {
_id: 'abc',
timestamp: Date.now(),
slug: new Date(2020, 0, 1).getTime(),
template: {
title: 'Date test',
slug: {
type: 'date',
type: SlugType.Date,
},
},
};
Expand All @@ -77,14 +77,14 @@ describe('Repeatable', () => {
});

it('timestamp slug', async () => {
const params = {
const params: RepeatableListItemProps = {
_id: 'abc',
timestamp: Date.now(),
slug: new Date(2020, 0, 1, 10, 20).getTime(),
template: {
title: 'Timestamp test',
slug: {
type: 'timestamp',
type: SlugType.Timestamp,
},
},
};
Expand All @@ -95,14 +95,14 @@ describe('Repeatable', () => {
expect(screen.getByText(expectedTimestamp)).toBeTruthy();
});
it('plain text slug', async () => {
const params = {
const params: RepeatableListItemProps = {
_id: 'abc',
timestamp: Date.now(),
slug: 'some text for you',
template: {
title: 'Plain Text test',
slug: {
type: 'string',
type: SlugType.String,
},
},
};
Expand Down
95 changes: 95 additions & 0 deletions src/client/features/Repeatable/RepeatableListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Link, ListItem, ListItemButton, ListItemText } from '@mui/material';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { type SlugConfig, type SlugData, SlugType, type TemplateDoc } from '../../../shared/types';
import RelativeTime from '../../components/RelativeTime';

type TemplateInfo = Pick<TemplateDoc, 'title' | 'slug'>;

export interface RepeatableListItemProps {
_id: string;
slug: string | number | undefined;
timestamp?: number;
// template can be string during loading, but should be TemplateInfo when rendered
template: string | TemplateInfo | undefined;
}

function isTemplateInfo(template: string | TemplateInfo | undefined): template is TemplateInfo {
return typeof template === 'object' && template !== null;
}

/**
* Construct a type-safe SlugData from config and value.
* Returns undefined if the value type doesn't match the config type.
*/
function toSlugData(config: SlugConfig, value: string | number | undefined): SlugData | undefined {
if (value === undefined) return undefined;

switch (config.type) {
case SlugType.String:
return typeof value === 'string'
? { type: SlugType.String, placeholder: config.placeholder, value }
: undefined;
case SlugType.URL:
return typeof value === 'string'
? { type: SlugType.URL, placeholder: config.placeholder, value }
: undefined;
case SlugType.Date:
return typeof value === 'number' ? { type: SlugType.Date, value } : undefined;
case SlugType.Timestamp:
return typeof value === 'number' ? { type: SlugType.Timestamp, value } : undefined;
}
}

function renderSlug(slugData: SlugData) {
switch (slugData.type) {
case SlugType.String:
return slugData.value;
case SlugType.URL:
return (
<Link href={slugData.value} target="_blank">
{slugData.value}
</Link>
);
case SlugType.Date:
return new Date(slugData.value).toLocaleDateString();
case SlugType.Timestamp:
return new Date(slugData.value).toLocaleString();
}
}

/**
* Display an instance of a repeatable in a list
*/
function RepeatableListItem(props: RepeatableListItemProps) {
const navigate = useNavigate();
const { _id, slug, timestamp, template } = props;

// Handle case where template is still a string (loading) or undefined
const templateObj = isTemplateInfo(template) ? template : undefined;
const slugData = templateObj ? toSlugData(templateObj.slug, slug) : undefined;
const displaySlug = slugData ? renderSlug(slugData) : null;

return (
<ListItem disablePadding>
<ListItemButton
disableRipple
onClick={(e) => {
// To let URL slugs (displayed inside this "button") have links that don't also trigger
// this navigation
if ((e.target as HTMLElement).nodeName !== 'A') {
navigate(`/repeatable/${_id}`);
}
}}
>
<ListItemText
primary={templateObj?.title}
secondary={timestamp && <RelativeTime date={timestamp} />}
/>
{displaySlug}
</ListItemButton>
</ListItem>
);
}

export default React.memo(RepeatableListItem);
14 changes: 8 additions & 6 deletions src/client/features/Repeatable/RepeatableSlug.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Input } from '@mui/material';
import { type ChangeEvent, useState } from 'react';
import type { RepeatableDoc, TemplateDoc } from '../../../shared/types';
import { type RepeatableDoc, SlugType, type TemplateDoc } from '../../../shared/types';
import db from '../../db';
import { setRepeatable } from '../../state/docsSlice';
import { type RootState, useDispatch, useSelector } from '../../store';
Expand All @@ -25,9 +25,11 @@ function RepeatableSlug() {
// @ts-expect-error FIXME: check if nodeValue works
const targetValue = target.value;
// we know that if anyone calls this function template has a value
const value = ['date', 'timestamp'].includes((template as TemplateDoc).slug.type)
? new Date(targetValue).getTime()
: targetValue;
const slugType = (template as TemplateDoc).slug.type;
const value =
slugType === SlugType.Date || slugType === SlugType.Timestamp
? new Date(targetValue).getTime()
: targetValue;

setSlug(value);
}
Expand All @@ -47,7 +49,7 @@ function RepeatableSlug() {
}

let slugInput: React.ReactElement;
if (['url', 'string'].includes(template.slug.type)) {
if (template.slug.type === SlugType.URL || template.slug.type === SlugType.String) {
slugInput = (
<Input
type="text"
Expand All @@ -58,7 +60,7 @@ function RepeatableSlug() {
onBlur={storeSlugChange}
/>
);
} else if ('date' === template.slug.type) {
} else if (template.slug.type === SlugType.Date) {
// FIXME: Clean This Up! The format required for the native date input type cannot
// be manufactured from the native JavaScript date type. If we were in raw HTML
// we could post-set it with Javascript by using valueAsNumber, but not in situ
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { screen } from '@testing-library/react';
// biome-ignore lint/correctness/noUnusedImports: React is required for JSX transform in .jsx files
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { expect, test } from 'vitest';
import { render } from '../../test-utils';

import TemplateListItem from './TemplateListItem';
Expand Down
Loading