diff --git a/package.json b/package.json
index e44dba9..8d1b8d7 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@urql/exchange-graphcache": "^7.1.2",
"match-sorter": "^6.3.4",
"react": "^18.2.0",
+ "react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"react-icons": "^5.2.1",
"react-router-dom": "^6.18.0",
@@ -47,6 +48,7 @@
"@julr/vite-plugin-validate-env": "^1.0.1",
"@types/node": "^20.11.6",
"@types/react": "^18.0.28",
+ "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9d4c289..058d49e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,9 @@ dependencies:
react:
specifier: ^18.2.0
version: 18.2.0
+ react-beautiful-dnd:
+ specifier: ^13.1.1
+ version: 13.1.1(react-dom@18.2.0)(react@18.2.0)
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
@@ -82,6 +85,9 @@ devDependencies:
'@types/react':
specifier: ^18.0.28
version: 18.0.28
+ '@types/react-beautiful-dnd':
+ specifier: ^13.1.8
+ version: 13.1.8
'@types/react-dom':
specifier: ^18.0.11
version: 18.0.11
@@ -3828,6 +3834,13 @@ packages:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: true
+ /@types/hoist-non-react-statics@3.3.5:
+ resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
+ dependencies:
+ '@types/react': 18.0.28
+ hoist-non-react-statics: 3.3.2
+ dev: false
+
/@types/istanbul-lib-coverage@2.0.6:
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
dev: true
@@ -3852,6 +3865,11 @@ packages:
/@types/prop-types@15.7.12:
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
+
+ /@types/react-beautiful-dnd@13.1.8:
+ resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==}
+ dependencies:
+ '@types/react': 18.0.28
dev: true
/@types/react-dom@18.0.11:
@@ -3860,13 +3878,21 @@ packages:
'@types/react': 18.0.28
dev: true
+ /@types/react-redux@7.1.33:
+ resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==}
+ dependencies:
+ '@types/hoist-non-react-statics': 3.3.5
+ '@types/react': 18.0.28
+ hoist-non-react-statics: 3.3.2
+ redux: 4.2.1
+ dev: false
+
/@types/react@18.0.28:
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
dependencies:
'@types/prop-types': 15.7.12
'@types/scheduler': 0.23.0
csstype: 3.1.3
- dev: true
/@types/resolve@1.20.2:
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -3874,7 +3900,6 @@ packages:
/@types/scheduler@0.23.0:
resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==}
- dev: true
/@types/semver@7.5.8:
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
@@ -5254,6 +5279,12 @@ packages:
postcss-selector-parser: 6.1.1
dev: true
+ /css-box-model@1.2.1:
+ resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
+ dependencies:
+ tiny-invariant: 1.3.3
+ dev: false
+
/css-functions-list@3.2.2:
resolution: {integrity: sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==}
engines: {node: '>=12 || >=16'}
@@ -5315,7 +5346,6 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
- dev: true
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -7671,6 +7701,10 @@ packages:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
dev: true
+ /memoize-one@5.2.1:
+ resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
+ dev: false
+
/meow@13.2.0:
resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
engines: {node: '>=18'}
@@ -7883,7 +7917,6 @@ packages:
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
- dev: true
/object-inspect@1.13.2:
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
@@ -8745,7 +8778,6 @@ packages:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
- dev: true
/proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -8795,6 +8827,10 @@ packages:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
dev: true
+ /raf-schd@4.0.3:
+ resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
+ dev: false
+
/randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
dependencies:
@@ -8811,6 +8847,25 @@ packages:
strip-json-comments: 2.0.1
dev: true
+ /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
+ peerDependencies:
+ react: ^16.8.5 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@babel/runtime': 7.24.8
+ css-box-model: 1.2.1
+ memoize-one: 5.2.1
+ raf-schd: 4.0.3
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0)
+ redux: 4.2.1
+ use-memo-one: 1.1.3(react@18.2.0)
+ transitivePeerDependencies:
+ - react-native
+ dev: false
+
/react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
@@ -8832,10 +8887,36 @@ packages:
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ /react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ dev: false
+
/react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
dev: true
+ /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
+ peerDependencies:
+ react: ^16.8.3 || ^17 || ^18
+ react-dom: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.8
+ '@types/react-redux': 7.1.33
+ hoist-non-react-statics: 3.3.2
+ loose-envify: 1.4.0
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-is: 17.0.2
+ dev: false
+
/react-router-dom@6.18.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==}
engines: {node: '>=14.0.0'}
@@ -8882,6 +8963,12 @@ packages:
picomatch: 2.3.1
dev: true
+ /redux@4.2.1:
+ resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
+ dependencies:
+ '@babel/runtime': 7.24.8
+ dev: false
+
/regenerate-unicode-properties@10.1.1:
resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==}
engines: {node: '>=4'}
@@ -9937,7 +10024,6 @@ packages:
/tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
- dev: true
/tinybench@2.8.0:
resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==}
@@ -10306,6 +10392,14 @@ packages:
wonka: 6.3.4
dev: false
+ /use-memo-one@1.1.3(react@18.2.0):
+ resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
diff --git a/src/App/routes/PageError/index.tsx b/src/App/routes/PageError/index.tsx
index f93df11..d1e7916 100644
--- a/src/App/routes/PageError/index.tsx
+++ b/src/App/routes/PageError/index.tsx
@@ -3,8 +3,15 @@ import {
useEffect,
useState,
} from 'react';
+import {
+ IoCaretDown,
+ IoCaretUp,
+ IoHome,
+ IoReload,
+} from 'react-icons/io5';
import { useRouteError } from 'react-router-dom';
+import Button from '#components/Button';
import Link from '#components/Link';
import styles from './styles.module.css';
@@ -24,14 +31,7 @@ function PageError() {
const [
fullErrorVisible,
setFullErrorVisible,
- ] = useState(false);
-
- const handleErrorVisibleToggle = useCallback(
- () => {
- setFullErrorVisible((oldValue) => !oldValue);
- },
- [setFullErrorVisible],
- );
+ ] = useState(import.meta.env.DEV);
const handleReloadButtonClick = useCallback(
() => {
@@ -49,45 +49,53 @@ function PageError() {
Looks like we ran into some issue!
-
- {errorResponse?.error?.message
- ?? errorResponse?.message
- ?? 'Something unexpected happended!'}
-
-
- {fullErrorVisible ? 'Hide Error' : 'Show Error'}
-
+ {!fullErrorVisible && (
+
+ {errorResponse?.error?.message
+ ?? errorResponse?.message
+ ?? 'Something unexpected happended!'}
+
+ )}
{fullErrorVisible && (
- <>
-
- {errorResponse?.error?.stack
- ?? errorResponse?.stack ?? 'Stack trace not available!'}
-
-
- See the developer console for more details.
-
- >
+
+ {errorResponse?.error?.stack
+ ?? errorResponse?.stack ?? 'Stack trace not available!'}
+
)}
+
+ See the developer console for more details.
+
- {/* NOTE: using the anchor element as it will refresh the page */}
-
- Go back to homepage
-
-
:
}
>
- Reload
-
+ {fullErrorVisible ? 'Hide details' : 'Show details'}
+
+
+ {/* NOTE: using the anchor element as it will refresh the page */}
+ }
+ variant="quaternary"
+ >
+ Go to homepage
+
+ }
+ >
+ Reload
+
+
diff --git a/src/App/routes/PageError/styles.module.css b/src/App/routes/PageError/styles.module.css
index c495c1f..818ec32 100644
--- a/src/App/routes/PageError/styles.module.css
+++ b/src/App/routes/PageError/styles.module.css
@@ -8,56 +8,54 @@
.container {
display: flex;
flex-direction: column;
- /*
- border-top: var(--go-ui-width-separator-large) solid var(--go-ui-color-primary-red);
- border-radius: var(--go-ui-border-radius-xl);
- box-shadow: var(--go-ui-box-shadow-2xl);
- background-color: var(--go-ui-color-white);
- padding: var(--go-ui-spacing-2xl);
- width: calc(100% - var(--go-ui-spacing-2xl));
- */
+ border-top: var(--width-separator-lg) solid var(--color-primary);
+ border-radius: var(--border-radius-xl);
+ box-shadow: var(--box-shadow-lg);
+ background-color: var(--color-foreground);
+ padding: var(--spacing-2xl);
+ width: calc(100% - var(--spacing-2xl));
max-width: 60rem;
max-height: 60rem;
- /*
- gap: var(--go-ui-spacing-2xl);
- */
+ gap: var(--spacing-lg);
.content {
display: flex;
flex-direction: column;
- /*
- gap: var(--go-ui-spacing-md);
- */
+ gap: var(--spacing-md);
.heading {
margin: 0;
- /*
- font-weight: var(--go-ui-font-weight-medium);
- */
+ font-weight: var(--font-weight-semibold);
+ }
+
+ .message {
+ font-family: var(--font-family-mono);
}
.stack {
flex-grow: 1;
- /*
- background-color: var(--go-ui-color-background);
- padding: var(--go-ui-spacing-md);
- */
+ background-color: var(--color-background);
+ padding: var(--spacing-md);
width: 100%;
overflow: auto;
white-space: pre;
- /*
- font-family: var(--go-ui-font-family-mono);
- */
+ font-family: var(--font-family-mono);
}
}
.footer {
display: flex;
align-items: center;
- justify-content: flex-end;
- /*
- gap: var(--go-ui-spacing-md);
- */
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: var(--spacing-md);
+
+ .actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--spacing-sm);
+ }
}
}
}
diff --git a/src/index.css b/src/index.css
index 69b3d6a..7ace9af 100644
--- a/src/index.css
+++ b/src/index.css
@@ -4,6 +4,7 @@
:root {
--font-family-sans-serif: "Fira Sans", sans-serif;
+ --font-family-mono: "Oxygen Mono", monospace;
--color-primary: #c45332;
--color-secondary: #9cb56e;
@@ -159,6 +160,26 @@ h1, h2, h3, h4, h5, h6 {
font-weight: var(--font-weight-semibold);
}
+h1 {
+ font-size: var(--font-size-2xl);
+}
+
+h2 {
+ font-size: var(--font-size-xl);
+}
+
+h3 {
+ font-size: var(--font-size-lg);
+}
+
+h4 {
+ font-size: var(--font-size-md);
+}
+
+h5 {
+ font-size: var(--font-size-sm);
+}
+
p {
margin: 0;
}
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 3e0b7cf..c7e7c83 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -212,3 +212,119 @@ export function fuzzySearch(
rows,
);
}
+
+export function sortByAttributes(
+ list: LIST_ITEM[],
+ attributes: ATTRIBUTE[],
+ sortFn: (a: LIST_ITEM, b: LIST_ITEM, attr: ATTRIBUTE) => number,
+): LIST_ITEM[] {
+ const newList = [...list];
+ newList.sort(
+ (a, b) => {
+ let sortResult = 0;
+
+ for (let i = 0; i < attributes.length; i += 1) {
+ const currentSortResult = sortFn(
+ a,
+ b,
+ attributes[i],
+ );
+
+ if (currentSortResult !== 0) {
+ sortResult = currentSortResult;
+ break;
+ }
+ }
+
+ return sortResult;
+ },
+ );
+
+ return newList;
+}
+
+type GroupedItem = {
+ key: string;
+ type: 'heading';
+ value: LIST_ITEM;
+ attribute: ATTRIBUTE;
+ level: number;
+} | {
+ type: 'list-item';
+ value: LIST_ITEM;
+};
+
+// NOTE: the list must be sorted before grouping
+export function groupListByAttributes(
+ list: LIST_ITEM[],
+ attributes: ATTRIBUTE[],
+ compareItemAttributes: (a: LIST_ITEM, b: LIST_ITEM, attribute: ATTRIBUTE) => boolean,
+): GroupedItem[] {
+ if (isNotDefined(list) || list.length === 0) {
+ return [];
+ }
+
+ const groupedItems = list.flatMap((listItem, listIndex) => {
+ if (listIndex === 0) {
+ const headings = attributes.map((attribute, i) => ({
+ type: 'heading' as const,
+ value: listItem,
+ attribute,
+ level: i,
+ key: `heading-${listIndex}-${i}`,
+ }));
+
+ return [
+ ...headings,
+ {
+ type: 'list-item' as const,
+ value: listItem,
+ },
+ ];
+ }
+
+ const prevListItem = list[listIndex - 1];
+ const attributeMismatchIndex = attributes.findIndex((attribute) => {
+ const hasSameCurrentAttribute = compareItemAttributes(
+ listItem,
+ prevListItem,
+ attribute,
+ );
+
+ return !hasSameCurrentAttribute;
+ });
+
+ if (attributeMismatchIndex === -1) {
+ return [
+ {
+ type: 'list-item' as const,
+ value: listItem,
+ },
+ ];
+ }
+
+ const headings = attributes.map((attribute, i) => {
+ if (i < attributeMismatchIndex) {
+ return undefined;
+ }
+
+ return {
+ type: 'heading' as const,
+ value: listItem,
+ attribute,
+ level: i,
+ key: `heading-${listIndex}-${i}`,
+ };
+ }).filter(isDefined);
+
+ return [
+ ...headings,
+ {
+ type: 'list-item' as const,
+ value: listItem,
+ },
+ ];
+ });
+
+ return groupedItems;
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index ef93e69..0e798bd 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,4 +1,7 @@
-import { ConfigStorage } from './types';
+import {
+ ConfigStorage,
+ NumericOption,
+} from './types';
export const KEY_CONFIG_STORAGE = 'timur-config';
@@ -11,6 +14,16 @@ export const defaultConfigValue: ConfigStorage = {
showInputIcons: false,
startSidebarShown: window.innerWidth >= 900,
endSidebarShown: false,
+ dailyJournalGrouping: {
+ groupLevel: 2,
+ joinLevel: 2,
+ },
+ dailyJournalAttributeOrder: [
+ { key: 'project', sortDirection: 1 },
+ { key: 'contract', sortDirection: 1 },
+ { key: 'task', sortDirection: 1 },
+ { key: 'status', sortDirection: 1 },
+ ],
};
export const colorscheme: [string, string][] = [
@@ -33,3 +46,16 @@ export const colorscheme: [string, string][] = [
// horchata 8
['#7d5327', '#ecdecc'],
];
+
+export const numericOptions: NumericOption[] = [
+ { key: 1, label: '1' },
+ { key: 2, label: '2' },
+ { key: 3, label: '3' },
+ { key: 4, label: '4' },
+];
+export function numericOptionKeySelector(option: NumericOption) {
+ return option.key;
+}
+export function numericOptionLabelSelector(option: NumericOption) {
+ return option.label;
+}
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 1f11e8f..5ee4a48 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -30,6 +30,17 @@ export interface Note {
content: string | undefined;
}
+export type DailyJournalAttributeKeys = 'project' | 'contract' | 'task' | 'status';
+export interface DailyJournalAttributeOrder {
+ key: DailyJournalAttributeKeys;
+ sortDirection: number;
+}
+
+export interface DailyJournalGrouping {
+ groupLevel: number;
+ joinLevel: number;
+}
+
export type ConfigStorage = {
defaultTaskType: WorkItemType | undefined,
defaultTaskStatus: WorkItemStatus,
@@ -39,6 +50,8 @@ export type ConfigStorage = {
startSidebarShown: boolean,
endSidebarShown: boolean,
compactTextArea: boolean,
+ dailyJournalAttributeOrder: DailyJournalAttributeOrder[];
+ dailyJournalGrouping: DailyJournalGrouping;
}
export interface GeneralEvent {
@@ -50,3 +63,8 @@ export interface GeneralEvent {
date: string;
remainingDays: number;
}
+
+export interface NumericOption {
+ key: number;
+ label: string;
+}
diff --git a/src/views/DailyJournal/DayView/index.tsx b/src/views/DailyJournal/DayView/index.tsx
index a5fb25c..a750317 100644
--- a/src/views/DailyJournal/DayView/index.tsx
+++ b/src/views/DailyJournal/DayView/index.tsx
@@ -1,4 +1,5 @@
import {
+ Fragment,
useCallback,
useContext,
useMemo,
@@ -6,31 +7,33 @@ import {
import { FcClock } from 'react-icons/fc';
import {
_cs,
+ compareString,
isDefined,
isNotDefined,
- listToGroupList,
- mapToList,
sum,
} from '@togglecorp/fujs';
-import List from '#components/List';
+import DefaultMessage from '#components/DefaultMessage';
import EnumsContext from '#contexts/enums';
import useFormattedRelativeDate from '#hooks/useFormattedRelativeDate';
import useLocalStorage from '#hooks/useLocalStorage';
-import { getDurationString } from '#utils/common';
+import {
+ getDurationString,
+ groupListByAttributes,
+ sortByAttributes,
+} from '#utils/common';
import {
defaultConfigValue,
KEY_CONFIG_STORAGE,
} from '#utils/constants';
import {
ConfigStorage,
- Contract,
+ DailyJournalAttributeOrder,
EntriesAsList,
- Project,
WorkItem,
} from '#utils/types';
-import ProjectGroupedView, { Props as ProjectGroupedViewProps } from './ProjectGroupedView';
+import WorkItemRow from './ProjectGroupedView/ContractGroupedView/WorkItemRow';
import styles from './styles.module.css';
@@ -44,18 +47,6 @@ const dateFormatter = new Intl.DateTimeFormat(
},
);
-interface ProjectGroupedWorkItem {
- project: Project,
- contracts: {
- contract: Contract,
- workItems: WorkItem[],
- }[],
-}
-
-function getId(item: ProjectGroupedWorkItem) {
- return item.project.id;
-}
-
interface Props {
className?: string;
workItems: WorkItem[] | undefined;
@@ -85,54 +76,37 @@ function DayView(props: Props) {
defaultConfigValue,
);
- const groupedWorkItems = useMemo(
- (): ProjectGroupedWorkItem[] | undefined => {
- if (isNotDefined(workItems) || isNotDefined(taskById)) {
- return undefined;
- }
+ const getWorkItemAttribute = useCallback((
+ item: WorkItem,
+ attr: DailyJournalAttributeOrder,
+ ) => {
+ if (attr.key === 'status') {
+ return item.status;
+ }
- return mapToList(listToGroupList(
- mapToList(listToGroupList(
- workItems,
- (workItem) => taskById[workItem.task].contract.id,
- undefined,
- (list) => ({
- contract: taskById[list[0].task].contract,
- workItems: list,
- }),
- )),
- (contractGrouped) => contractGrouped.contract.project.id,
- undefined,
- (list) => ({
- project: list[0].contract.project,
- contracts: list,
- }),
- ));
- },
- [workItems, taskById],
- );
+ if (isNotDefined(taskById)) {
+ return undefined;
+ }
- type GroupedWorkItem = NonNullable<(typeof groupedWorkItems)>[number];
-
- const rendererParams = useCallback(
- (_: string, item: GroupedWorkItem): ProjectGroupedViewProps => ({
- contracts: item.contracts,
- project: item.project,
- onWorkItemClone,
- onWorkItemChange,
- onWorkItemDelete,
- }),
- [
- onWorkItemClone,
- onWorkItemChange,
- onWorkItemDelete,
- ],
- );
+ const taskDetails = taskById[item.task];
- const formattedDate = dateFormatter.format(new Date(selectedDate));
+ if (attr.key === 'task') {
+ return taskDetails.name;
+ }
- const formattedRelativeDate = useFormattedRelativeDate(selectedDate);
+ if (attr.key === 'contract') {
+ return taskDetails.contract.name;
+ }
+
+ if (attr.key === 'project') {
+ return taskDetails.contract.project.name;
+ }
+ return undefined;
+ }, [taskById]);
+
+ const formattedDate = dateFormatter.format(new Date(selectedDate));
+ const formattedRelativeDate = useFormattedRelativeDate(selectedDate);
const totalHours = useMemo(
() => {
if (isDefined(workItems)) {
@@ -144,6 +118,48 @@ function DayView(props: Props) {
[workItems],
);
+ const {
+ dailyJournalAttributeOrder,
+ dailyJournalGrouping,
+ } = storedConfig;
+
+ const { groupLevel, joinLevel } = dailyJournalGrouping;
+
+ const groupedItems = useMemo(() => {
+ if (isNotDefined(taskById) || isNotDefined(workItems)) {
+ return [];
+ }
+
+ const sortedWorkItems = sortByAttributes(
+ workItems,
+ dailyJournalAttributeOrder,
+ (a, b, attr) => (
+ compareString(
+ getWorkItemAttribute(a, attr),
+ getWorkItemAttribute(b, attr),
+ attr.sortDirection,
+ )
+ ),
+ );
+
+ return groupListByAttributes(
+ sortedWorkItems,
+ dailyJournalAttributeOrder.slice(0, groupLevel),
+ (a, b, attr) => {
+ const aValue = getWorkItemAttribute(a, attr);
+ const bValue = getWorkItemAttribute(b, attr);
+
+ return aValue === bValue;
+ },
+ );
+ }, [
+ taskById,
+ workItems,
+ getWorkItemAttribute,
+ dailyJournalAttributeOrder,
+ groupLevel,
+ ]);
+
return (
@@ -170,29 +186,90 @@ function DayView(props: Props) {
)}
-
- Click on
- {' '}
- Add entry
- {' '}
- to create a new entry.
- >
- )}
/>
+ {!errored && !loading && (
+
+ {groupedItems.map((groupedItem) => {
+ if (groupedItem.type === 'heading') {
+ const levelDiff = groupLevel - joinLevel;
+
+ const headingText = getWorkItemAttribute(
+ groupedItem.value,
+ groupedItem.attribute,
+ );
+
+ if (groupedItem.level < levelDiff) {
+ return (
+
+ {headingText}
+
+ );
+ }
+
+ if (groupedItem.level < (groupLevel - 1)) {
+ return null;
+ }
+
+ return (
+
+ {dailyJournalAttributeOrder.map((attribute, i) => {
+ if (i >= groupLevel) {
+ return null;
+ }
+
+ const currentLabel = getWorkItemAttribute(
+ groupedItem.value,
+ attribute,
+ );
+
+ if (i < (groupLevel - joinLevel)) {
+ return null;
+ }
+
+ return (
+
+ {i > (groupLevel - joinLevel) && (
+
+ )}
+ {currentLabel}
+
+ );
+ })}
+
+ );
+ }
+
+ const taskDetails = taskById?.[groupedItem.value.task];
+
+ if (!taskDetails) {
+ return null;
+ }
+
+ return (
+
+ );
+ })}
+
+ )}
);
}
diff --git a/src/views/DailyJournal/DayView/styles.module.css b/src/views/DailyJournal/DayView/styles.module.css
index 13a4709..b4c8305 100644
--- a/src/views/DailyJournal/DayView/styles.module.css
+++ b/src/views/DailyJournal/DayView/styles.module.css
@@ -54,4 +54,31 @@
flex-direction: column;
gap: var(--spacing-md);
}
+
+ .new-group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--width-separator-md);
+
+ .nested-heading {
+ margin-top: var(--spacing-md);
+ }
+
+ .joined-heading {
+ display: flex;
+ align-items: baseline;
+ gap: var(--spacing-sm);
+ margin-top: var(--spacing-sm);
+ margin-bottom: var(--spacing-2xs);
+
+ .separator {
+ border: var(--width-separator-sm) solid var(--color-separator);
+ height: 1rem;
+ }
+ }
+
+ .work-item {
+ background-color: var(--color-foreground);
+ }
+ }
}
diff --git a/src/views/DailyJournal/StartSidebar/index.tsx b/src/views/DailyJournal/StartSidebar/index.tsx
index 23fd6d6..469ccec 100644
--- a/src/views/DailyJournal/StartSidebar/index.tsx
+++ b/src/views/DailyJournal/StartSidebar/index.tsx
@@ -1,19 +1,27 @@
import {
useCallback,
useContext,
+ useEffect,
+ useId,
+ useState,
} from 'react';
import {
- isDefined,
- isFalsyString,
- isNotDefined,
- listToGroupList,
- mapToList,
-} from '@togglecorp/fujs';
-
-import Button from '#components/Button';
+ DragDropContext,
+ Draggable,
+ DraggableProvided,
+ DraggableStateSnapshot,
+ Droppable,
+ DroppableProps,
+ DroppableProvided,
+ DroppableStateSnapshot,
+ DropResult,
+} from 'react-beautiful-dnd';
+import { MdDragIndicator } from 'react-icons/md';
+import { _cs } from '@togglecorp/fujs';
+
import Checkbox from '#components/Checkbox';
-import Link from '#components/Link';
import MonthlyCalendar from '#components/MonthlyCalendar';
+import RadioInput from '#components/RadioInput';
import SelectInput from '#components/SelectInput';
import EnumsContext from '#contexts/enums';
import { EnumsQuery } from '#generated/types/graphql';
@@ -23,16 +31,42 @@ import {
colorscheme,
defaultConfigValue,
KEY_CONFIG_STORAGE,
+ numericOptionKeySelector,
+ numericOptionLabelSelector,
+ numericOptions,
} from '#utils/constants';
import {
ConfigStorage,
+ DailyJournalAttributeKeys,
+ DailyJournalAttributeOrder,
+ DailyJournalGrouping,
EditingMode,
WorkItem,
- WorkItemStatus,
} from '#utils/types';
import styles from './styles.module.css';
+// TODO: Move to separate component
+function StrictModeDroppable({ children, ...props }: DroppableProps) {
+ const [enabled, setEnabled] = useState(false);
+ useEffect(() => {
+ const animation = requestAnimationFrame(() => setEnabled(true));
+ return () => {
+ cancelAnimationFrame(animation);
+ setEnabled(false);
+ };
+ }, []);
+ if (!enabled) {
+ return null;
+ }
+ return (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+ {children}
+
+ );
+}
+
type EditingOption = { key: EditingMode, label: string };
function editingOptionKeySelector(item: EditingOption) {
return item.key;
@@ -74,6 +108,13 @@ function defaultColorSelector(_: T, i: number): [string, string] {
return colorscheme[i % colorscheme.length];
}
+const dailyJournalAttributeDetails: Record = {
+ project: { label: 'Project' },
+ contract: { label: 'Contract' },
+ task: { label: 'Task' },
+ status: { label: 'Status' },
+};
+
interface Props {
workItems: WorkItem[];
selectedDate: string;
@@ -86,12 +127,10 @@ interface Props {
function StartSidebar(props: Props) {
const {
calendarComponentRef,
- workItems,
selectedDate,
setSelectedDate,
} = props;
- const { taskById } = useContext(EnumsContext);
const { enums } = useContext(EnumsContext);
const [storedConfig, setStoredConfig] = useLocalStorage(
@@ -101,53 +140,100 @@ function StartSidebar(props: Props) {
const setConfigFieldValue = useSetFieldValue(setStoredConfig);
- const handleCopyTextButtonClick = useCallback(
- () => {
- function toSubItem(workItem: WorkItem) {
- const description = workItem.description ?? '??';
- const status: WorkItemStatus = workItem.status ?? 'TODO';
- const task = taskById?.[workItem.task]?.name ?? '??';
-
- return description
- .split('\n')
- .map((item, i) => ([
- i === 0 ? ' -' : ' ',
- status !== 'DONE' ? `\`${status.toUpperCase()}\`` : undefined,
- i === 0 ? `${task}: ${item}` : item,
- ].filter(isDefined).join(' ')))
- .join('\n');
- }
-
- if (isNotDefined(taskById)) {
- return;
- }
-
- const groupedWorkItems = mapToList(listToGroupList(
- workItems,
- (workItem) => taskById[workItem.task].contract.project.id,
- undefined,
- (list) => ({
- project: taskById?.[list[0].task].contract.project,
- workItems: list,
- }),
- ));
-
- const text = groupedWorkItems.map((projectGrouped) => {
- const { project, workItems: projectWorkItems } = projectGrouped;
-
- return `- ${project.name}\n${projectWorkItems.map((workItem) => toSubItem(workItem)).join('\n')}`;
- }).join('\n');
-
- if (isFalsyString(text)) {
- return;
- }
-
- window.navigator.clipboard.writeText(text);
- },
- [workItems, taskById],
+ const date = new Date(selectedDate);
+
+ const droppableId = useId();
+ const handleDragEnd = useCallback((result: DropResult) => {
+ const oldAttributes = storedConfig.dailyJournalAttributeOrder
+ ?? defaultConfigValue.dailyJournalAttributeOrder;
+
+ if (!result.destination) {
+ return;
+ }
+
+ const newAttributes = [...oldAttributes];
+ const [removedItem] = newAttributes.splice(result.source.index, 1);
+ newAttributes.splice(result.destination.index, 0, removedItem);
+
+ setConfigFieldValue(newAttributes, 'dailyJournalAttributeOrder');
+ }, [storedConfig.dailyJournalAttributeOrder, setConfigFieldValue]);
+
+ const getDraggableChildren = useCallback(
+ (
+ attribute: DailyJournalAttributeOrder,
+ provided: DraggableProvided,
+ snapshot: DraggableStateSnapshot,
+ ) => (
+
+
+
+
+
+ {dailyJournalAttributeDetails[attribute.key].label}
+
+
+ ),
+ [],
);
- const date = new Date(selectedDate);
+ const droppableChildren = useCallback(
+ (droppableProvided: DroppableProvided, droppableSnapshot: DroppableStateSnapshot) => (
+
+ {storedConfig.dailyJournalAttributeOrder.map((attribute, index) => (
+
+ {(draggableProvided, draggableSnapshot) => getDraggableChildren(
+ attribute,
+ draggableProvided,
+ draggableSnapshot,
+ )}
+
+ ))}
+ {droppableProvided.placeholder}
+
+ ),
+ [getDraggableChildren, storedConfig.dailyJournalAttributeOrder],
+ );
+
+ const updateJournalGrouping = useCallback((value: number, name: 'groupLevel' | 'joinLevel') => {
+ const oldValue = storedConfig.dailyJournalGrouping
+ ?? defaultConfigValue.dailyJournalGrouping;
+
+ if (name === 'groupLevel') {
+ setConfigFieldValue({
+ groupLevel: value,
+ joinLevel: Math.min(oldValue.joinLevel, value),
+ } satisfies DailyJournalGrouping, 'dailyJournalGrouping');
+
+ return;
+ }
+
+ setConfigFieldValue({
+ groupLevel: oldValue.groupLevel,
+ joinLevel: Math.min(oldValue.groupLevel, value),
+ } satisfies DailyJournalGrouping, 'dailyJournalGrouping');
+ }, [storedConfig.dailyJournalGrouping, setConfigFieldValue]);
return (
-
-
- Go to today
-
-
- Copy standup text
-
+
+
Ordering
+
+
+ {droppableChildren}
+
+
+
+
+
+ Grouping
+
+
+
diff --git a/src/views/DailyJournal/StartSidebar/styles.module.css b/src/views/DailyJournal/StartSidebar/styles.module.css
index 5d69438..1eea26d 100644
--- a/src/views/DailyJournal/StartSidebar/styles.module.css
+++ b/src/views/DailyJournal/StartSidebar/styles.module.css
@@ -10,6 +10,49 @@
gap: var(--spacing-sm);
}
+ .attributes {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+
+ .attribute-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--width-separator-md);
+
+ &.dragging-over {
+ outline: var(--width-separator-md) solid var(--color-separator);
+ }
+
+ .attribute {
+ display: flex;
+ align-items: center;
+ background-color: var(--color-foreground);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ gap: var(--spacing-xs);
+
+ .drag-handle {
+ cursor: grab;
+ }
+
+ &:hover {
+ background-color: var(--color-separator);
+ }
+
+ .label {
+ flex-grow: 1;
+ }
+ }
+ }
+ }
+
+ .grouping {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ }
+
+
.quick-settings {
display: flex;
flex-direction: column;
diff --git a/src/views/DailyJournal/index.tsx b/src/views/DailyJournal/index.tsx
index 98675ce..31a1dae 100644
--- a/src/views/DailyJournal/index.tsx
+++ b/src/views/DailyJournal/index.tsx
@@ -745,6 +745,12 @@ export function Component() {
>
Add entry
+
+ Go to today
+