From 2cedb4402bef97b7e8e94dbf8c52039a1b127461 Mon Sep 17 00:00:00 2001 From: frozenhelium Date: Mon, 2 Sep 2024 08:01:12 +0545 Subject: [PATCH] Add dynamic nesting --- package.json | 4 + pnpm-lock.yaml | 56 +++- src/App/routes/PageError/index.tsx | 90 +++--- src/App/routes/PageError/styles.module.css | 54 ++-- src/components/Indent/index.tsx | 23 ++ src/components/Indent/styles.module.css | 12 + src/index.css | 22 ++ src/utils/common.ts | 120 +++++++ src/utils/constants.ts | 33 +- src/utils/types.ts | 18 ++ .../WorkItemRow/styles.module.css | 2 +- src/views/DailyJournal/DayView/index.tsx | 251 ++++++++++----- .../DailyJournal/DayView/styles.module.css | 38 +++ src/views/DailyJournal/StartSidebar/index.tsx | 297 +++++++++++++----- .../StartSidebar/styles.module.css | 49 +++ src/views/DailyJournal/index.tsx | 9 + 16 files changed, 853 insertions(+), 225 deletions(-) create mode 100644 src/components/Indent/index.tsx create mode 100644 src/components/Indent/styles.module.css diff --git a/package.json b/package.json index e44dba9..ec44f81 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ }, "dependencies": { "@codemirror/lang-markdown": "^6.2.5", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@replit/codemirror-vim": "^6.2.1", "@sentry/react": "^8.26.0", "@togglecorp/fujs": "^2.2.0", @@ -55,6 +57,7 @@ "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/coverage-v8": "^1.2.2", "autoprefixer": "^10.4.14", + "core": "link:@types/dnd-kit/core", "eslint": "^8.40.0", "eslint-config-airbnb": "^19.0.4", "eslint-import-resolver-typescript": "^3.5.5", @@ -74,6 +77,7 @@ "postcss-preset-env": "^8.3.2", "postinstall-postinstall": "^2.1.0", "rollup-plugin-visualizer": "^5.9.0", + "sortable": "link:@types/dnd-kit/sortable", "stylelint": "^16.7.0", "stylelint-config-concentric": "^2.0.2", "stylelint-config-recommended": "^14.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d4c289..266fc0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ dependencies: '@codemirror/lang-markdown': specifier: ^6.2.5 version: 6.2.5 + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) '@replit/codemirror-vim': specifier: ^6.2.1 version: 6.2.1(@codemirror/commands@6.6.0)(@codemirror/language@6.10.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.29.1) @@ -106,6 +112,9 @@ devDependencies: autoprefixer: specifier: ^10.4.14 version: 10.4.14(postcss@8.3.0) + core: + specifier: link:@types/dnd-kit/core + version: link:@types/dnd-kit/core eslint: specifier: ^8.40.0 version: 8.40.0 @@ -163,6 +172,9 @@ devDependencies: rollup-plugin-visualizer: specifier: ^5.9.0 version: 5.9.0(rollup@2.79.1) + sortable: + specifier: link:@types/dnd-kit/sortable + version: link:@types/dnd-kit/sortable stylelint: specifier: ^16.7.0 version: 16.7.0(typescript@5.0.4) @@ -2132,6 +2144,49 @@ packages: postcss: 8.3.0 dev: true + /@dnd-kit/accessibility@3.1.0(react@18.2.0): + resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.3 + dev: false + + /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@dnd-kit/accessibility': 3.1.0(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.3 + dev: false + + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.3 + dev: false + + /@dnd-kit/utilities@3.2.2(react@18.2.0): + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.3 + dev: false + /@dual-bundle/import-meta-resolve@4.1.0: resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==} dev: true @@ -10053,7 +10108,6 @@ packages: /tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - dev: true /tsutils@3.21.0(typescript@5.0.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} 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 && ( +
+ {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 - - + {fullErrorVisible ? 'Hide details' : 'Show details'} + +
+ {/* NOTE: using the anchor element as it will refresh the page */} + } + variant="quaternary" + > + Go to homepage + + +
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/components/Indent/index.tsx b/src/components/Indent/index.tsx new file mode 100644 index 0000000..7c2fc42 --- /dev/null +++ b/src/components/Indent/index.tsx @@ -0,0 +1,23 @@ +import styles from './styles.module.css'; + +interface Props { + level: number; +} + +function Indent(props: Props) { + const { level } = props; + + if (level === 0) { + return null; + } + + return ( +
+ {Array.from(new Array(level).keys()).map((key) => ( + + ))} +
+ ); +} + +export default Indent; diff --git a/src/components/Indent/styles.module.css b/src/components/Indent/styles.module.css new file mode 100644 index 0000000..ec33bff --- /dev/null +++ b/src/components/Indent/styles.module.css @@ -0,0 +1,12 @@ +.indent { + display: flex; + height: 100%; + gap: var(--spacing-sm); + min-height: 1.5rem; + + .item { + height: 100%; + width: var(--spacing-xs); + border-right: var(--width-separator-sm) solid var(--color-separator-dark); + } +} diff --git a/src/index.css b/src/index.css index 69b3d6a..57c58be 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; @@ -18,6 +19,7 @@ --color-text-on-dark: #ffffff; --color-separator: rgba(0, 0, 0, 0.1); + --color-separator-dark: rgba(0, 0, 0, 0.2); --base-font-size: 1rem; @@ -159,6 +161,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..551ebc3 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -212,3 +212,123 @@ 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; + level: number; +}; + +// 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, + level: attributes.length, + }, + ]; + } + + 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, + level: attributes.length, + }, + ]; + } + + 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, + level: attributes.length, + }, + ]; + }); + + return groupedItems; +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index ef93e69..3e61e37 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,21 @@ 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' }, + { key: 5, label: '5' }, + { key: 6, label: '6' }, + { key: 7, label: '7' }, + { key: 8, label: '8' }, + { key: 9, label: '9' }, +]; +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/ProjectGroupedView/ContractGroupedView/WorkItemRow/styles.module.css b/src/views/DailyJournal/DayView/ProjectGroupedView/ContractGroupedView/WorkItemRow/styles.module.css index b727bc2..e21b83a 100644 --- a/src/views/DailyJournal/DayView/ProjectGroupedView/ContractGroupedView/WorkItemRow/styles.module.css +++ b/src/views/DailyJournal/DayView/ProjectGroupedView/ContractGroupedView/WorkItemRow/styles.module.css @@ -2,7 +2,7 @@ display: grid; align-items: flex-start; grid-template-columns: 5rem 2fr 5fr 9rem 4rem 4rem; - padding: var(--spacing-2xs) var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-sm); gap: var(--spacing-xs); &.with-icons { diff --git a/src/views/DailyJournal/DayView/index.tsx b/src/views/DailyJournal/DayView/index.tsx index a5fb25c..3f6fb45 100644 --- a/src/views/DailyJournal/DayView/index.tsx +++ b/src/views/DailyJournal/DayView/index.tsx @@ -1,4 +1,6 @@ import { + ElementType, + Fragment, useCallback, useContext, useMemo, @@ -6,33 +8,37 @@ import { import { FcClock } from 'react-icons/fc'; import { _cs, + bound, + 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'; +import Indent from '#components/Indent'; const dateFormatter = new Intl.DateTimeFormat( [], @@ -44,18 +50,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 +79,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 +121,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 +189,97 @@ 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) { + const Heading = `h${bound(groupedItem.level + 2, 2, 4)}` as unknown as ElementType; + + 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..9322115 100644 --- a/src/views/DailyJournal/DayView/styles.module.css +++ b/src/views/DailyJournal/DayView/styles.module.css @@ -54,4 +54,42 @@ flex-direction: column; gap: var(--spacing-md); } + + .new-group { + display: flex; + flex-direction: column; + + .nested-heading { + display: flex; + align-items: center; + gap: var(--spacing-sm); + color: var(--color-text-light); + font-weight: var(--font-weight-semibold); + } + + .joined-heading { + display: flex; + align-items: center; + gap: var(--spacing-sm); + color: var(--color-secondary); + font-weight: var(--font-weight-semibold); + + .separator { + background-color: var(--color-text-light); + width: 0.5rem; + height: 0.5rem; + border-radius: 0.5rem; + } + } + + .work-item-container { + display: flex; + gap: var(--spacing-sm); + + .work-item { + flex-grow: 1; + background-color: var(--color-foreground); + } + } + } } diff --git a/src/views/DailyJournal/StartSidebar/index.tsx b/src/views/DailyJournal/StartSidebar/index.tsx index 23fd6d6..c8bee20 100644 --- a/src/views/DailyJournal/StartSidebar/index.tsx +++ b/src/views/DailyJournal/StartSidebar/index.tsx @@ -1,19 +1,33 @@ import { useCallback, useContext, + useMemo, } from 'react'; +import { MdDragIndicator } from 'react-icons/md'; import { + closestCenter, + DndContext, + DragEndEvent, + DraggableAttributes, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities'; +import { + SortableContext, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { + _cs, isDefined, - isFalsyString, isNotDefined, - listToGroupList, - mapToList, } from '@togglecorp/fujs'; -import Button from '#components/Button'; 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 +37,126 @@ 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'; +const dailyJournalAttributeDetails: Record = { + project: { label: 'Project' }, + contract: { label: 'Contract' }, + task: { label: 'Task' }, + status: { label: 'Status' }, +}; + +interface ItemProps { + className?: string; + attribute: DailyJournalAttributeOrder; + setNodeRef?: (node: HTMLElement | null) => void; + draggableAttributes?: DraggableAttributes; + draggableListeners?: SyntheticListenerMap | undefined; + transformStyle?: string | undefined; + transitionStyle?: string | undefined; +} + +function Item(props: ItemProps) { + const { + className, + setNodeRef, + attribute, + draggableAttributes, + draggableListeners, + transformStyle, + transitionStyle, + } = props; + + return ( +
+
+ +
+
+ {dailyJournalAttributeDetails[attribute.key].label} +
+
+ ); +} + +interface SortableItemProps { + className?: string; + attribute: DailyJournalAttributeOrder; +} + +function SortableItem(props: SortableItemProps) { + const { + attribute, + className, + } = props; + + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + over, + } = useSortable({ id: attribute.key }); + + const transformStyle = useMemo(() => { + if (isNotDefined(transform)) { + return undefined; + } + + const transformations = [ + // isDefined(transform.x) && `translateX(${transform.x}px)`, + isDefined(transform.y) && `translateY(${transform.y}px)`, + isDefined(transform.scaleX) && `scaleY(${transform.scaleX})`, + isDefined(transform.scaleY) && `scaleY(${transform.scaleY})`, + ]; + + return transformations.filter(Boolean).join(' '); + }, [transform]); + + return ( + + ); +} + type EditingOption = { key: EditingMode, label: string }; function editingOptionKeySelector(item: EditingOption) { return item.key; @@ -75,7 +199,6 @@ function defaultColorSelector(_: T, i: number): [string, string] { } interface Props { - workItems: WorkItem[]; selectedDate: string; setSelectedDate: (newDate: string) => void; calendarComponentRef?: React.MutableRefObject<{ @@ -86,12 +209,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 +222,57 @@ 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 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]); + + const sensors = useSensors( + useSensor(PointerSensor), ); - const date = new Date(selectedDate); + const handleDndEnd = useCallback((dragEndEvent: DragEndEvent) => { + const { + active, + over, + } = dragEndEvent; + + const oldAttributes = storedConfig.dailyJournalAttributeOrder + ?? defaultConfigValue.dailyJournalAttributeOrder; + + if (isNotDefined(active) || isNotDefined(over)) { + return; + } + + const newAttributes = [...oldAttributes]; + const sourceIndex = newAttributes.findIndex(({ key }) => active.id === key); + const destinationIndex = newAttributes.findIndex(({ key }) => over.id === key); + + if (sourceIndex === -1 || destinationIndex === -1) { + return; + } + + const [removedItem] = newAttributes.splice(sourceIndex, 1); + newAttributes.splice(destinationIndex, 0, removedItem); + + setConfigFieldValue(newAttributes, 'dailyJournalAttributeOrder'); + }, [setConfigFieldValue, storedConfig.dailyJournalAttributeOrder]); return (
-
- - Go to today - - +
+

Ordering

+
+ + ({ id: key }), + )} + strategy={verticalListSortingStrategy} + > + {storedConfig.dailyJournalAttributeOrder.map((attribute) => ( + + ))} + + +
+
+
+

+ Grouping +

+ +

diff --git a/src/views/DailyJournal/StartSidebar/styles.module.css b/src/views/DailyJournal/StartSidebar/styles.module.css index 5d69438..1982374 100644 --- a/src/views/DailyJournal/StartSidebar/styles.module.css +++ b/src/views/DailyJournal/StartSidebar/styles.module.css @@ -10,6 +10,55 @@ 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); + + &.dragging { + z-index: 1; + box-shadow: var(--box-shadow-md); + opacity: 0.8; + } + + .drag-handle { + cursor: grab; + } + + &:hover { + background-color: var(--color-tertiary); + } + + .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..76efc55 100644 --- a/src/views/DailyJournal/index.tsx +++ b/src/views/DailyJournal/index.tsx @@ -27,6 +27,7 @@ import { encodeDate, isDefined, isNotDefined, + isTruthyString, } from '@togglecorp/fujs'; import { gql, @@ -745,6 +746,14 @@ export function Component() { > Add entry + {!isTruthyString(dateFromParams) && ( + + Go to today + + )}