- Hit
- {' '}
-
Ctrl+Shift+Right
- {' '}
- to go to next day.
+
+ Ctrl+Shift+Left
+
+
+ Next day
-
- Hit
- {' '}
-
Ctrl+Shift+Down
- {' '}
- to go to present day.
+
+ Ctrl+Shift+Right
+
+
+ Present day
-
- Hit
- {' '}
-
Ctrl+Shift+?
- {' '}
- to view shortcuts.
+
+ Ctrl+Shift+Down
+
+
+
+ Help
+
+
+ View shortcuts
+
+ Ctrl+Shift+?
+
);
}
diff --git a/src/views/DailyJournal/ShortcutsDialog/styles.module.css b/src/views/DailyJournal/ShortcutsDialog/styles.module.css
index 0977bfc..c1103f0 100644
--- a/src/views/DailyJournal/ShortcutsDialog/styles.module.css
+++ b/src/views/DailyJournal/ShortcutsDialog/styles.module.css
@@ -1,8 +1,32 @@
.shortcuts-dialog {
.modal-content {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-md);
- font-size: var(--font-size-md);
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ column-gap: var(--spacing-md);
+ row-gap: var(--spacing-sm);
+
+ .subheading {
+ padding-top: var(--spacing-md);
+ grid-column: 1 / -1;
+ }
+
+ .description {
+ grid-column: 1;
+ }
+
+ .key {
+ display: inline-flex;
+ grid-column: 3;
+ justify-self: end;
+ align-items: center;
+ border: var(--width-separator-sm) solid var(--color-separator);
+ border-radius: var(--border-radius-md);
+ background-color: var(--color-foreground);
+ padding: var(--spacing-3xs) var(--spacing-xs);
+ color: var(--color-primary-text);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ }
}
}
diff --git a/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx b/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx
index 0a4602e..ac25bc0 100644
--- a/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx
+++ b/src/views/DailyJournal/StartSidebar/GoogleCalendarSection/index.tsx
@@ -123,7 +123,7 @@ function ScheduleRow(props: ScheduleRowProps) {
interface Props {
date: string;
- addedDescriptions: Set
;
+ addedDescriptions: string[];
onWorkItemCreateFromCalendar: (override: Partial) => void;
loading?: boolean;
googleEvents: GoogleCalendarEvent[];
@@ -226,9 +226,9 @@ function GoogleCalendarSection(props: Props) {
)}
{timedGoogleEvents.map((event, index) => {
- const isAdded = event.summary
- ? addedDescriptions.has(event.summary.trim().toLowerCase())
- : false;
+ const normalizedSummary = event.summary?.trim().toLowerCase() ?? '';
+ const isAdded = normalizedSummary.length > 0
+ && addedDescriptions.some((d) => d.includes(normalizedSummary));
return (
diff --git a/src/views/DailyJournal/StartSidebar/index.tsx b/src/views/DailyJournal/StartSidebar/index.tsx
index 6b50b56..aadc832 100644
--- a/src/views/DailyJournal/StartSidebar/index.tsx
+++ b/src/views/DailyJournal/StartSidebar/index.tsx
@@ -4,7 +4,7 @@ import {
useMemo,
} from 'react';
import {
- RiSettingsLine,
+ RiGoogleFill,
RiTerminalBoxLine,
} from 'react-icons/ri';
import { useSuspenseQuery } from '@tanstack/react-query';
@@ -15,7 +15,6 @@ import {
import Button from '#components/Button';
import DefaultMessage from '#components/DefaultMessage';
-import Link from '#components/Link';
import MonthlyCalendar from '#components/MonthlyCalendar';
import {
type DayEventsAndDeadlinesQuery,
@@ -73,7 +72,7 @@ const QUERY_CONTEXT = { suspense: true } as const;
interface DayEventsAndCalendarProps {
selectedDate: string;
- addedDescriptions: Set;
+ addedDescriptions: string[];
onWorkItemCreateFromCalendar: (override: Partial) => void;
}
@@ -145,14 +144,36 @@ function StartSidebar(props: Props) {
lastEditedAt,
} = props;
+ const {
+ isAvailable: isGoogleCalendarAvailable,
+ isConnected: isGoogleCalendarConnected,
+ expiresAt: googleCalendarExpiresAt,
+ connect: connectGoogleCalendar,
+ disconnect: disconnectGoogleCalendar,
+ } = useGoogleCalendar();
+
+ const googleCalendarStatusMessage = useMemo(() => {
+ if (!isGoogleCalendarConnected) {
+ return 'Connect to view events from Google Calendar. ✨';
+ }
+ if (!googleCalendarExpiresAt) {
+ return 'Connected to Google Calendar.';
+ }
+ const expiresOn = new Date(googleCalendarExpiresAt).toLocaleString([], {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ });
+ return `Connected. Integration expires on ${expiresOn}.`;
+ }, [isGoogleCalendarConnected, googleCalendarExpiresAt]);
+
const addedDescriptions = useMemo(() => {
- const set = new Set();
+ const list: string[] = [];
dayWorkItems.forEach((item) => {
if (item.description) {
- set.add(item.description.trim().toLowerCase());
+ list.push(item.description.trim().toLowerCase());
}
});
- return set;
+ return list;
}, [dayWorkItems]);
return (
@@ -190,14 +211,19 @@ function StartSidebar(props: Props) {
>
Shortcuts
- }
- >
- Settings
-
+ {isGoogleCalendarAvailable && (
+ }
+ >
+ {isGoogleCalendarConnected ? 'Disconnect' : 'Connect'}
+
+ )}
);
diff --git a/src/views/DailyJournal/index.tsx b/src/views/DailyJournal/index.tsx
index 28658d3..c5c2b73 100644
--- a/src/views/DailyJournal/index.tsx
+++ b/src/views/DailyJournal/index.tsx
@@ -45,7 +45,6 @@ import RouteContext from '#contexts/route';
import {
MyTimeEntriesQuery,
MyTimeEntriesQueryVariables,
- TimeEntryTypeEnum,
} from '#generated/types/graphql';
import useCommand from '#hooks/useCommand';
import { useFocusManager } from '#hooks/useFocus';
@@ -63,6 +62,7 @@ import {
Task,
WorkItem,
} from '#utils/types';
+import useWorkItemClassifier from '#utils/workItemClassifier';
import AddWorkItemDialog from './AddWorkItemDialog';
import AvailabilityDialog from './AvailabilityDialog';
@@ -75,56 +75,6 @@ import UpdateNoteDialog from './UpdateNoteDialog';
import styles from './styles.module.css';
-function inferTypeFromDescription(desc: string): TimeEntryTypeEnum | undefined {
- const lower = desc.toLowerCase();
- const matches = (pattern: RegExp) => pattern.test(lower);
-
- if (matches(/\b(client meeting|client call|external meeting)\b/)) {
- return 'EXTERNAL_MEETING';
- }
-
- if (
- matches(/\b(meeting|standup|stand-up|all hands|all-hands)\b/)
- || matches(/\b1:1\b/)
- ) {
- return 'INTERNAL_MEETING';
- }
- if (matches(/\b(client discussion|external discussion)\b/)) {
- return 'EXTERNAL_DISCUSSION';
- }
- if (matches(/\b(discuss|discussion|brainstorm)\b/)) {
- return 'INTERNAL_DISCUSSION';
- }
- if (matches(/\b(review|pull request|merge request)\b/)) {
- return 'REVIEW';
- }
- if (matches(/\b(deploy|deployment|pipeline|ci|cd|infra|release)\b/)) {
- return 'DEV_OPS';
- }
- if (matches(/\b(test|tests|testing|qa|qc|regression)\b/)) {
- return 'TESTING';
- }
- if (matches(/\b(design|wireframe|mockup|ux|ui)\b/)) {
- return 'DESIGN';
- }
- if (matches(/\b(research|study|investigate|spike|explore)\b/)) {
- return 'RESEARCH';
- }
- if (matches(/\b(documentation|docs|readme|wiki|document)\b/)) {
- return 'DOCUMENTATION';
- }
- if (matches(/\b(planning|project board|plan|roadmap|backlog|estimate|estimation)\b/)) {
- return 'PROJECT_MANAGEMENT';
- }
- if (matches(/\b(annotation|annotate|labelling|labeling|label)\b/)) {
- return 'ANNOTATION';
- }
- if (matches(/\b(refactor|fix|fixing|fixed|fixes|bugfix|hotfix|debug|implement|implementation|feature)\b/)) {
- return 'DEVELOPMENT';
- }
- return undefined;
-}
-
const MY_TIME_ENTRIES_QUERY = gql`
query MyTimeEntries($date: Date!) {
private {
@@ -174,6 +124,8 @@ export function Component() {
const { midActionsRef } = useContext(NavbarContext);
+ const inferTypeFromDescription = useWorkItemClassifier();
+
const { date: dateFromParams } = useParams<{ date: string | undefined}>();
const { fullDate } = useContext(DateContext);
const selectedDate = useMemo(() => {
@@ -385,7 +337,7 @@ export function Component() {
dialogOpenTriggerRef.current(override.description ?? undefined);
}
},
- [],
+ [inferTypeFromDescription],
);
const handleWorkItemClone = useCallback(
@@ -488,7 +440,7 @@ export function Component() {
);
}
},
- [workItems, setWorkItemChange, selectedDate],
+ [workItems, setWorkItemChange, selectedDate, inferTypeFromDescription],
);
const handleWorkItemDelete = useCallback(
@@ -774,7 +726,6 @@ export function Component() {
diff --git a/vite.config.ts b/vite.config.ts
index 67c6036..cf2a7bb 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -86,7 +86,7 @@ export default defineConfig(({ mode }) => {
scope: '/',
display: 'standalone',
display_override: ['standalone'],
- orientation: 'any',
+ orientation: 'portrait',
// NOTE: keep in sync with terracotta --color-background in src/themes.css
theme_color: '#fafaf0',
background_color: '#fafaf0',