Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notify users about possibility to connect to Jira #119

Merged
merged 14 commits into from
Oct 18, 2024
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
1 change: 1 addition & 0 deletions e2e/tests/controls.mobile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ test.describe("Connect Jira Mobile", () => {
test("Connect to Jira", async ({ page, t }) => {
await page.getByRole("banner").getByLabel(t("controls.openMenu")).click();
await page.getByRole("button", { name: t("controls.jiraConnect") }).click();
await page.getByRole("button", { name: t("controls.jiraConnect") }).click();
await expect(page).toHaveURL(/.*id\.atlassian\.com\/login.*/);
});

Expand Down
1 change: 1 addition & 0 deletions e2e/tests/controls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ test.describe("Connect Jira", () => {
test("Connect to Jira", async ({ page, t }) => {
await page.getByLabel(t("controls.settingsMenu")).click();
await page.getByRole("menuitem", { name: t("controls.jiraConnect") }).click();
await page.getByRole("button", { name: t("controls.jiraConnect") }).click();
await expect(page).toHaveURL(/.*id\.atlassian\.com\/login.*/);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";

export class Migrations1721115925197 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"user_settings",
new TableColumn({
name: "jiraNotificationIgnore",
type: "boolean",
isNullable: true,
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("user_settings", "jiraNotificationIgnore");
kurukimi marked this conversation as resolved.
Show resolved Hide resolved
}
}
3 changes: 3 additions & 0 deletions server/src/user-settings/dto/update-settings.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ export class UpdateSettingsDto {

@Field({ nullable: true })
activityPreset?: string;

@Field({ nullable: true })
jiraNotificationIgnore?: boolean;
}
4 changes: 4 additions & 0 deletions server/src/user-settings/user-settings.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ export class UserSettings {
@Column({ nullable: true })
@Field({ nullable: true })
activityPreset: string;

@Column({ nullable: true })
@Field({ nullable: true })
jiraNotificationIgnore: boolean;
}
25 changes: 23 additions & 2 deletions web/src/components/entry-dialog/EntryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import { Controller, UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import useDayjs from "../../common/useDayjs";
import { AcceptanceStatus, Entry } from "../../graphql/generated/graphql";
import {
AcceptanceStatus,
Entry,
GetMySettingsDocument,
UpdateSettingsDocument,
} from "../../graphql/generated/graphql";
import usePreferSetRemainingHours from "../user-preferences/usePreferSetRemainingHours";
import BigDeleteEntryButton from "./BigDeleteEntryButton";
import DimensionComboBox from "./DimensionComboBox";
Expand All @@ -31,6 +36,8 @@ import WorkdayHours from "./WorkdayHours";
import useEntryForm, { EntryFormSchema } from "./useEntryForm";
import { useIsJiraAuthenticated } from "../../jira/jiraApi";
import JiraIssueComboBox from "../../jira/components/JiraIssueComboBox";
import { JiraIntegrationAlert } from "../../jira/components/JiraIntegrationAlert";
import { useMutation, useQuery } from "@apollo/client";

export type EntryFormProps = {
form: UseFormReturn<EntryFormSchema>;
Expand Down Expand Up @@ -73,7 +80,12 @@ const EntryForm = () => {
const { userPrefersSetRemainingHours, toggleRemainingHours } = usePreferSetRemainingHours();
const { control, watch } = form;
const dateWatch = dayjs(watch("date")).locale(dayjs.locale());
const { isJiraAuth } = useIsJiraAuthenticated();
const { isJiraAuth, isLoading } = useIsJiraAuthenticated();

const { data } = useQuery(GetMySettingsDocument);
const [updateSettings] = useMutation(UpdateSettingsDocument, {
refetchQueries: [GetMySettingsDocument],
});

return (
<form onSubmit={handleSubmit(onSubmit)}>
Expand Down Expand Up @@ -132,6 +144,15 @@ const EntryForm = () => {
)}
/>
</Grid>
{data && !data.getMySettings.jiraNotificationIgnore && !isJiraAuth && !isLoading ? (
<Grid item>
<JiraIntegrationAlert
onHide={() =>
updateSettings({ variables: { settings: { jiraNotificationIgnore: true } } })
}
/>
</Grid>
) : null}
</Grid>
</Grid>
<Grid item xs={12} md={6}>
Expand Down
13 changes: 8 additions & 5 deletions web/src/components/layout/AppMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import useDarkMode from "../../theme/useDarkMode";
import LabelledIconButton from "../LabelledIconButton";
import { keijoJiraApiUrl } from "../../jira/jiraConfig";
import { useIsJiraAuthenticated } from "../../jira/jiraApi";
import { JiraInfoDialog } from "../../jira/components/JiraInfoDialog";

const AppMenuButton = () => {
const navigate = useNavigate();
Expand All @@ -38,6 +39,12 @@ const AppMenuButton = () => {

const { isJiraAuth } = useIsJiraAuthenticated();

const [infoDialogOpen, setInfoDialogOpen] = useState(false);

const handleConnectToJira = () => {
setInfoDialogOpen(true);
};

const toggleMenu = () => {
setMenuOpen((prev) => !prev);
};
Expand All @@ -47,11 +54,6 @@ const AppMenuButton = () => {
document.documentElement.lang = languageCode;
};

const handleConnectToJira = () => {
toggleMenu();
window.location.href = keijoJiraApiUrl;
};

const handleDisconnectJira = () => {
toggleMenu();
window.location.href = keijoJiraApiUrl + "/remove-session";
Expand Down Expand Up @@ -139,6 +141,7 @@ const AppMenuButton = () => {
</ListItem>
</List>
</Drawer>
<JiraInfoDialog open={infoDialogOpen} handleClose={() => setInfoDialogOpen(false)} />
</Box>
);
};
Expand Down
11 changes: 7 additions & 4 deletions web/src/components/settings/SettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { useTranslation } from "react-i18next";
import { generatePath, useLocation, useNavigate } from "react-router-dom";
import { keijoJiraApiUrl } from "../../jira/jiraConfig";
import { useIsJiraAuthenticated } from "../../jira/jiraApi";
import { disconnectJira } from "../../jira/jiraUtils";
import { JiraInfoDialog } from "../../jira/components/JiraInfoDialog";
import { useState } from "react";

type SettingsMenuProps = {
anchor: HTMLElement | null;
Expand All @@ -19,20 +21,20 @@ const SettingsMenu = ({ anchor, onClose }: SettingsMenuProps) => {
const navigate = useNavigate();
const location = useLocation();
const { isJiraAuth } = useIsJiraAuthenticated();
const [infoDialogOpen, setInfoDialogOpen] = useState(false);

const handleSetDefaultValues = () => {
onClose();
navigate(generatePath(`${location.pathname}/set-defaults`));
};

const handleConnectToJira = () => {
onClose();
window.location.href = keijoJiraApiUrl;
setInfoDialogOpen(true);
};

const handleDisconnectJira = () => {
onClose();
window.location.href = keijoJiraApiUrl + "/remove-session";
disconnectJira();
};

return (
Expand Down Expand Up @@ -63,6 +65,7 @@ const SettingsMenu = ({ anchor, onClose }: SettingsMenuProps) => {
{t("controls.jiraConnect")}
</MenuItem>
)}
<JiraInfoDialog open={infoDialogOpen} handleClose={() => setInfoDialogOpen(false)} />
</Menu>
);
};
Expand Down
3 changes: 1 addition & 2 deletions web/src/components/workday-browser/WorkdayList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,11 @@ const WorkdayList = () => {
return (
<>
<TotalHours workdays={workdays} />

<Paper>
{dividedWorkdays.map((wdArr) => {
if (isWeekend(wdArr[0].date)) {
return (
<Collapse in={checked}>
<Collapse in={checked} key={wdArr[0].date.toString()}>
{wdArr.map((wd) => (
<WorkdayAccordion workday={wd} key={wd.date.toString()} />
))}
Expand Down
6 changes: 4 additions & 2 deletions web/src/graphql/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ export type SessionStatus = {

export type UpdateSettingsDto = {
activityPreset?: InputMaybe<Scalars['String']['input']>;
jiraNotificationIgnore?: InputMaybe<Scalars['Boolean']['input']>;
productPreset?: InputMaybe<Scalars['String']['input']>;
};

export type UserSettings = {
__typename?: 'UserSettings';
activityPreset?: Maybe<Scalars['String']['output']>;
employeeNumber: Scalars['Float']['output'];
jiraNotificationIgnore?: Maybe<Scalars['Boolean']['output']>;
productPreset?: Maybe<Scalars['String']['output']>;
};

Expand Down Expand Up @@ -174,7 +176,7 @@ export type ReplaceWorkdayEntryMutation = { __typename?: 'Mutation', replaceWork
export type GetMySettingsQueryVariables = Exact<{ [key: string]: never; }>;


export type GetMySettingsQuery = { __typename?: 'Query', getMySettings: { __typename?: 'UserSettings', employeeNumber: number, productPreset?: string | null, activityPreset?: string | null } };
export type GetMySettingsQuery = { __typename?: 'Query', getMySettings: { __typename?: 'UserSettings', employeeNumber: number, productPreset?: string | null, activityPreset?: string | null, jiraNotificationIgnore?: boolean | null } };

export type UpdateSettingsMutationVariables = Exact<{
settings: UpdateSettingsDto;
Expand All @@ -190,5 +192,5 @@ export const FindWorkdaysDocument = {"kind":"Document","definitions":[{"kind":"O
export const GetSessionStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSessionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getSessionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"employeeNumber"}}]}}]}}]} as unknown as DocumentNode<GetSessionStatusQuery, GetSessionStatusQueryVariables>;
export const RemoveWorkdayEntryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveWorkdayEntry"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"entry"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoveWorkdayEntryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeWorkdayEntry"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"entry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"entry"}}}]}]}}]} as unknown as DocumentNode<RemoveWorkdayEntryMutation, RemoveWorkdayEntryMutationVariables>;
export const ReplaceWorkdayEntryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReplaceWorkdayEntry"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"originalEntry"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoveWorkdayEntryInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"replacementEntry"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AddWorkdayEntryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"replaceWorkdayEntry"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"originalEntry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"originalEntry"}}},{"kind":"Argument","name":{"kind":"Name","value":"replacementEntry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"replacementEntry"}}}]}]}}]} as unknown as DocumentNode<ReplaceWorkdayEntryMutation, ReplaceWorkdayEntryMutationVariables>;
export const GetMySettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMySettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getMySettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"employeeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"productPreset"}},{"kind":"Field","name":{"kind":"Name","value":"activityPreset"}}]}}]}}]} as unknown as DocumentNode<GetMySettingsQuery, GetMySettingsQueryVariables>;
export const GetMySettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMySettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getMySettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"employeeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"productPreset"}},{"kind":"Field","name":{"kind":"Name","value":"activityPreset"}},{"kind":"Field","name":{"kind":"Name","value":"jiraNotificationIgnore"}}]}}]}}]} as unknown as DocumentNode<GetMySettingsQuery, GetMySettingsQueryVariables>;
export const UpdateSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"settings"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateSettingsDto"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"settings"},"value":{"kind":"Variable","name":{"kind":"Name","value":"settings"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"employeeNumber"}}]}}]}}]} as unknown as DocumentNode<UpdateSettingsMutation, UpdateSettingsMutationVariables>;
1 change: 1 addition & 0 deletions web/src/graphql/user-settings.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ query GetMySettings {
employeeNumber
productPreset
activityPreset
jiraNotificationIgnore
}
}

Expand Down
11 changes: 11 additions & 0 deletions web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ const en = {
all: "All",
recent: "Recent",
},
connectNotificationTitle: "New: Jira Integration",
connectNotification1:
"Keijo now supports reading issue data from Jira to make issue selection easier. You have to connect Keijo to Jira using your own Atlassian account to enable these features.",
connectNotification2:
"If you hide this notification, you can always connect later via the settings menu.",
infoDialog: {
title: "Keijo-Jira",
// Site could also be set dynamically from server or simply by <your-company>.atlassian.net
content:
"You will be redirected to authorize Keijo to use Jira. Login on Atlassian site and choose to authorize appropriate site e.g., funidata.atlassian.net from the dropdown.",
},
},
};

Expand Down
11 changes: 11 additions & 0 deletions web/src/i18n/fi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ const fi = {
all: "Kaikki",
recent: "Viimeaikaiset",
},
connectNotificationTitle: "Uutta: Jira-integraatio",
connectNotification1:
"Keijo voi lukea tikettien tietoja Jirasta kirjausten helpottamiseksi. Voit ottaa Jira-ominaisuudet käyttöön yhdistämällä Keijon Jiraan henkilökohtaisen Atlassian-tilisi kautta.",
connectNotification2:
"Voit myös sulkea tämän huomautuksen ja halutessasi yhdistää Jiraan myöhemmin asetusvalikon kautta.",
infoDialog: {
title: "Keijo-Jira",
// Site could also be set dynamically from server or simply by <your-company>.atlassian.net
content:
"Sinut ohjataan valtuuttamaan Jira-integraatio käyttäjälläsi. Kirjaudu Atlassianin sivulla ja valitse valikosta Keijolle oikeudet käytettävälle sivulle esim. funidata.atlassian.net.",
},
},
};

Expand Down
32 changes: 32 additions & 0 deletions web/src/jira/components/JiraInfoDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import { t } from "i18next";
import { connectToJira } from "../jiraUtils";

type JiraInfoDialogProps = {
open: boolean;
handleClose: () => void;
};

export const JiraInfoDialog = ({ open, handleClose }: JiraInfoDialogProps) => {
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{t("jira.infoDialog.title")}</DialogTitle>
<DialogContent>
<DialogContentText>{t("jira.infoDialog.content")}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("controls.cancel")}
</Button>
<Button onClick={connectToJira} autoFocus variant="contained">
{t("controls.jiraConnect")}
</Button>
</DialogActions>
</Dialog>
);
};
41 changes: 41 additions & 0 deletions web/src/jira/components/JiraIntegrationAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Alert, AlertTitle, Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { t } from "i18next";
import { useState } from "react";
import { JiraInfoDialog } from "./JiraInfoDialog";

type JiraIntegrationAlertProps = {
onHide: () => void;
};

export const JiraIntegrationAlert = ({ onHide }: JiraIntegrationAlertProps) => {
const [infoDialogOpen, setInfoDialogOpen] = useState(false);
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down("md"));

return (
<>
<Alert severity="info" onClose={onHide}>
<AlertTitle>{t("jira.connectNotificationTitle")}</AlertTitle>
<Box sx={{ mt: 1 }}>{t("jira.connectNotification1")}</Box>
<Box sx={{ mt: 1 }}>{t("jira.connectNotification2")}</Box>
<Box sx={{ mt: 2, display: "flex", justifyContent: "end" }}>
<Button
onClick={() => setInfoDialogOpen(true)}
variant="contained"
size="medium"
color="info"
fullWidth={mobile}
>
{t("controls.jiraConnect")}
</Button>
</Box>
</Alert>
<JiraInfoDialog
open={infoDialogOpen}
handleClose={() => {
setInfoDialogOpen(false);
}}
/>
</>
);
};
2 changes: 1 addition & 1 deletion web/src/jira/jiraApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const getIssues = async (

export const useIsJiraAuthenticated = () => {
const { data, error, isLoading } = useGetAccessToken();
return { isJiraAuth: !isLoading && data && !error, data, error };
return { isJiraAuth: !isLoading && !error && data, data, error, isLoading };
};

/**
Expand Down
10 changes: 10 additions & 0 deletions web/src/jira/jiraUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { keijoJiraApiUrl } from "./jiraConfig";

export const mergePages = <T>(...arrays: T[][][]): T[][] => {
const maxLength = Math.max(...arrays.map((arr) => arr.length));
return Array.from({ length: maxLength }, (_, i) => {
Expand Down Expand Up @@ -50,3 +52,11 @@ export const removeWord = (searchFilter: string, word: string) =>
.filter((text) => text.trim() !== word)
.join(" ")
: searchFilter;

export const connectToJira = () => {
window.location.href = keijoJiraApiUrl;
};

export const disconnectJira = () => {
window.location.href = keijoJiraApiUrl + "/remove-session";
};