From e31d89b688156c5ec2c7b8d13b724c46686eeea4 Mon Sep 17 00:00:00 2001
From: Kacper Donat <kadet1090@gmail.com>
Date: Sat, 9 Jan 2021 22:47:39 +0100
Subject: [PATCH] Intership listening

---
 src/components/acceptance-action.tsx          | 117 ++++++++++-------
 src/management/api/index.ts                   |   2 +
 src/management/api/internship.ts              |  58 +++++++++
 src/management/edition/internship/common.tsx  |  47 +++++++
 src/management/edition/internship/details.tsx |  39 ++++++
 src/management/edition/internship/list.tsx    | 119 ++++++++++++++++++
 src/management/edition/manage.tsx             |   5 +-
 src/management/routing.tsx                    |   6 +-
 src/pages/base.tsx                            |   2 +-
 translations/management.pl.yaml               |  10 ++
 yarn.lock                                     |   6 +-
 11 files changed, 359 insertions(+), 52 deletions(-)
 create mode 100644 src/management/api/internship.ts
 create mode 100644 src/management/edition/internship/common.tsx
 create mode 100644 src/management/edition/internship/details.tsx
 create mode 100644 src/management/edition/internship/list.tsx

diff --git a/src/components/acceptance-action.tsx b/src/components/acceptance-action.tsx
index 670e2b9..b613db5 100644
--- a/src/components/acceptance-action.tsx
+++ b/src/components/acceptance-action.tsx
@@ -1,10 +1,79 @@
 import React, { useState } from "react";
-import { Button, ButtonGroup, Dialog, DialogActions, DialogContent, DialogTitle, Menu, MenuItem, TextField, Typography } from "@material-ui/core";
+import {
+    Button,
+    ButtonGroup,
+    ButtonProps,
+    Dialog,
+    DialogActions,
+    DialogContent,
+    DialogProps,
+    DialogTitle,
+    Menu,
+    MenuItem,
+    TextField,
+    Typography
+} from "@material-ui/core";
 import { MenuDown, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
 import { useTranslation } from "react-i18next";
 import { useVerticalSpacing } from "@/styles";
 import { createPortal } from "react-dom";
 
+type AcceptSubmissionDialogProps = {
+    onAccept: (comment?: string) => void;
+    label: string;
+} & DialogProps;
+
+export function AcceptSubmissionDialog({ onAccept, label, ...props }: AcceptSubmissionDialogProps) {
+    const { t } = useTranslation();
+    const [comment, setComment] = useState<string>("");
+    const classes = useVerticalSpacing(3);
+
+    return <Dialog maxWidth="md" { ...props }>
+        <DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
+        <DialogContent className={ classes.root }>
+            <Typography variant="body1">{ t(label + ".accept.info") }</Typography>
+            <TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
+
+            <DialogActions>
+                <Button onClick={ ev => props.onClose?.(ev, "backdropClick") }>
+                    { t('cancel') }
+                </Button>
+                <Button onClick={ () => onAccept?.(comment) } color="primary" variant="contained">
+                    { t('confirm') }
+                </Button>
+            </DialogActions>
+        </DialogContent>
+    </Dialog>
+}
+
+type DiscardSubmissionDialogProps = {
+    onDiscard: (comment: string) => void;
+    label: string;
+} & DialogProps;
+
+export function DiscardSubmissionDialog({ onDiscard, label, ...props }: DiscardSubmissionDialogProps) {
+    const { t } = useTranslation();
+    const [comment, setComment] = useState<string>("");
+    const classes = useVerticalSpacing(3);
+
+    return <Dialog maxWidth="md" { ...props }>
+        <DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
+        <DialogContent className={ classes.root }>
+            <Typography variant="body1">{ t(label + ".accept.info") }</Typography>
+            <TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
+
+            <DialogActions>
+                <Button onClick={ ev => props.onClose?.(ev, "backdropClick") }>
+                    { t('cancel') }
+                </Button>
+                <Button onClick={ () => onDiscard?.(comment) } color="primary" variant="contained">
+                    { t('confirm') }
+                </Button>
+            </DialogActions>
+        </DialogContent>
+    </Dialog>
+}
+
 type AcceptanceActionsProps = {
     onAccept: (comment?: string) => void;
     onDiscard: (comment: string) => void;
@@ -17,19 +86,8 @@ export function AcceptanceActions({ onAccept, onDiscard, label }: AcceptanceActi
     const [isDiscardModalOpen, setDiscardModelOpen] = useState<boolean>(false);
     const [isAcceptModalOpen, setAcceptModelOpen] = useState<boolean>(false);
 
-    const [comment, setComment] = useState<string>("");
     const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
 
-    const classes = useVerticalSpacing(3);
-
-    const handleAccept = () => {
-        onAccept(comment);
-    }
-
-    const handleDiscard = () => {
-        onDiscard(comment);
-    }
-
     const handleAcceptModalClose = () => {
         setAcceptModelOpen(false);
     }
@@ -77,39 +135,8 @@ export function AcceptanceActions({ onAccept, onDiscard, label }: AcceptanceActi
         </Button>
 
         { createPortal(<>
-            <Dialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md">
-                <DialogTitle>{ t(label + ".discard.title") }</DialogTitle>
-                <DialogContent className={ classes.root }>
-                    <Typography variant="body1">{ t(label + ".discard.info") }</Typography>
-                    <TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
-
-                    <DialogActions>
-                        <Button onClick={ handleDiscardModalClose }>
-                            { t('cancel') }
-                        </Button>
-                        <Button onClick={ handleDiscard } color="primary" variant="contained">
-                            { t('confirm') }
-                        </Button>
-                    </DialogActions>
-                </DialogContent>
-            </Dialog>
-
-            <Dialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md">
-                <DialogTitle>{ t(label + ".accept.title") }</DialogTitle>
-                <DialogContent className={ classes.root }>
-                    <Typography variant="body1">{ t(label + ".accept.info") }</Typography>
-                    <TextField multiline value={ comment } onChange={ ev => setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>
-
-                    <DialogActions>
-                        <Button onClick={ handleAcceptModalClose }>
-                            { t('cancel') }
-                        </Button>
-                        <Button onClick={ handleAccept } color="primary" variant="contained">
-                            { t('confirm') }
-                        </Button>
-                    </DialogActions>
-                </DialogContent>
-            </Dialog>
+            <DiscardSubmissionDialog open={ isDiscardModalOpen } onClose={ handleDiscardModalClose } maxWidth="md" onDiscard={ onDiscard } label={ label }/>
+            <AcceptSubmissionDialog open={ isAcceptModalOpen } onClose={ handleAcceptModalClose } maxWidth="md" onAccept={ onAccept } label={ label }/>
         </>, document.getElementById("modals") as Element) }
     </>
 }
diff --git a/src/management/api/index.ts b/src/management/api/index.ts
index 62e75d7..4215e92 100644
--- a/src/management/api/index.ts
+++ b/src/management/api/index.ts
@@ -2,12 +2,14 @@ import * as edition from "./edition"
 import * as page from "./page"
 import * as type from "./type"
 import * as course from "./course"
+import * as internship from "./internship"
 
 export const api = {
     edition,
     page,
     type,
     course,
+    internship
 }
 
 export default api;
diff --git a/src/management/api/internship.ts b/src/management/api/internship.ts
new file mode 100644
index 0000000..3f626d9
--- /dev/null
+++ b/src/management/api/internship.ts
@@ -0,0 +1,58 @@
+import { Identifiable, Identifier, Internship } from "@/data";
+import { sampleCompanies, sampleStudent } from "@/provider/dummy";
+import moment, { Moment } from "moment-timezone";
+import { OneOrMany } from "@/helpers";
+import { SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
+
+export type InternshipSubmission = Internship & {
+    state: SubmissionStatus,
+    changed: Moment
+}
+
+const sampleInternship: InternshipSubmission = {
+    id: "test",
+    company: sampleCompanies[0],
+    startDate: moment('2020-07-01'),
+    endDate: moment('2020-08-31'),
+    hours: 180,
+    intern: sampleStudent,
+    isAccepted: false,
+    lengthInWeeks: 0,
+    mentor: {
+        email: "test@test.com",
+        name: "Test",
+        surname: "Testowy",
+        phone: "+48 123 456 789"
+    },
+    office: sampleCompanies[0].offices[0],
+    program: [
+        { description: "Test" },
+    ],
+    type: {
+        description: {
+            pl: "Przykładowy typ",
+            en: "Example type"
+        },
+        label: {
+            pl: "Przykładowy",
+            en: "Example"
+        },
+        requiresDeanApproval: true,
+        requiresInsurance: false
+    },
+    state: "declined",
+    changed: moment(),
+}
+
+export async function all(edition: Identifiable): Promise<InternshipSubmission[]> {
+    return [
+        sampleInternship,
+    ]
+}
+
+export async function get(id: Identifier): Promise<InternshipSubmission> {
+    return sampleInternship;
+}
+
+export async function approve(internship: OneOrMany<Internship>, comment?: string): Promise<void> {}
+export async function decline(internship: OneOrMany<Internship>, comment: string): Promise<void> {}
diff --git a/src/management/edition/internship/common.tsx b/src/management/edition/internship/common.tsx
new file mode 100644
index 0000000..e289b3b
--- /dev/null
+++ b/src/management/edition/internship/common.tsx
@@ -0,0 +1,47 @@
+import { SubmissionStatus } from "@/state/reducer/submission";
+import React from "react";
+import { ClockOutline, NotebookCheckOutline, NotebookEditOutline, NotebookRemoveOutline } from "mdi-material-ui";
+import { useTranslation } from "react-i18next";
+import { Chip } from "@material-ui/core";
+import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
+import { green, orange, red } from "@material-ui/core/colors";
+import { InternshipSubmission } from "@/management/api/internship";
+
+const useStateLabelStyles = makeStyles((theme: Theme) => createStyles<SubmissionStatus, {}>({
+    awaiting: {
+        borderColor: orange["800"],
+        color: orange["800"],
+    },
+    declined: {
+        borderColor: red["600"],
+        color: red["600"],
+    },
+    draft: {},
+    accepted: {
+        borderColor: green["600"],
+        color: green["600"]
+    }
+}))
+
+export type StateLabelProps = {
+    state: SubmissionStatus;
+};
+
+export const StateLabel = ({ state }: StateLabelProps) => {
+    const icons: { [sate in SubmissionStatus]: React.ReactElement } = {
+        accepted: <NotebookCheckOutline/>,
+        awaiting: <ClockOutline/>,
+        declined: <NotebookRemoveOutline/>,
+        draft: <NotebookEditOutline/>
+    }
+
+    const classes = useStateLabelStyles();
+
+    const { t } = useTranslation();
+
+    return <Chip icon={ icons[state] } label={ t(`translation:submission.status.${ state }`) } variant="outlined" className={ classes[state] }/>
+}
+
+export const canEdit = (internship: InternshipSubmission) => internship.state != "draft";
+export const canAccept = (internship: InternshipSubmission) => ["declined", "awaiting"].includes(internship.state);
+export const canDiscard = (internship: InternshipSubmission) => ["accepted", "awaiting"].includes(internship.state);
diff --git a/src/management/edition/internship/details.tsx b/src/management/edition/internship/details.tsx
new file mode 100644
index 0000000..fa65b4a
--- /dev/null
+++ b/src/management/edition/internship/details.tsx
@@ -0,0 +1,39 @@
+import React, { useCallback } from "react";
+import { Link as RouterLink, useRouteMatch } from "react-router-dom";
+import api from "@/management/api";
+import { Page } from "@/pages/base";
+import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
+import { Container, Link, Typography } from "@material-ui/core";
+import { Async } from "@/components/async";
+import { useTranslation } from "react-i18next";
+import { useAsync } from "@/hooks";
+import { Student } from "@/data";
+import { route } from "@/routing";
+import { ProposalPreview } from "@/components/proposalPreview";
+import { AcceptanceActions } from "@/components/acceptance-action";
+import { useSpacing } from "@/styles";
+
+const fullname = (student: Student) => `${student.name} ${student.surname}`;
+
+export const InternshipDetails = ({ edition }: EditionManagementProps) => {
+    const { params } = useRouteMatch();
+    const internship = useAsync(useCallback(() => api.internship.get(params.internship), [ params.internship ]));
+    const { t } = useTranslation("management");
+    const spacing = useSpacing(2);
+
+    return <Async async={ internship }>
+        { internship => <Page>
+            <Page.Header maxWidth="lg">
+                <EditionManagement.Breadcrumbs>
+                    <Link to={ route("management:edition_internships", { edition: edition.id || "" }) } component={ RouterLink }>{ t("edition.internships.title") }</Link>
+                    <Typography color="textPrimary">{ fullname(internship.intern) }</Typography>
+                </EditionManagement.Breadcrumbs>
+                <Page.Title>{ fullname(internship.intern) }</Page.Title>
+            </Page.Header>
+            <Container maxWidth="lg" className={ spacing.vertical }>
+                <ProposalPreview proposal={ internship } />
+                <AcceptanceActions onAccept={ () => {} } onDiscard={ () => {} } label="internship" />
+            </Container>
+        </Page> }
+    </Async>
+}
diff --git a/src/management/edition/internship/list.tsx b/src/management/edition/internship/list.tsx
new file mode 100644
index 0000000..389f9c3
--- /dev/null
+++ b/src/management/edition/internship/list.tsx
@@ -0,0 +1,119 @@
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useAsyncState } from "@/hooks";
+import { useSpacing } from "@/styles";
+import api from "@/management/api";
+import { Box, Button, Container, IconButton, Typography } from "@material-ui/core";
+import MaterialTable, { Column } from "material-table";
+import { actionsColumn } from "@/management/common/helpers";
+import { FileFind, Refresh, StickerCheckOutline, StickerRemoveOutline } from "mdi-material-ui";
+import { Page } from "@/pages/base";
+import { Actions } from "@/components";
+import { BulkActions } from "@/management/common/BulkActions";
+import { Async } from "@/components/async";
+import { MaterialTableTitle } from "@/management/common/MaterialTableTitle";
+import { EditionManagement, EditionManagementProps } from "@/management/edition/manage";
+import { Link as RouterLink } from "react-router-dom";
+import { route } from "@/routing";
+import { AcceptanceActions, AcceptSubmissionDialog, DiscardSubmissionDialog } from "@/components/acceptance-action";
+import { ProposalPreview } from "@/components/proposalPreview";
+import { InternshipSubmission } from "@/management/api/internship";
+import { canAccept, canDiscard, StateLabel } from "@/management/edition/internship/common";
+import { createPortal } from "react-dom";
+
+const title = "edition.internships.title";
+
+export const InternshipManagement = ({ edition }: EditionManagementProps) => {
+    const { t } = useTranslation("management");
+    const [result, setInternshipsPromise] = useAsyncState<InternshipSubmission[]>();
+    const [selected, setSelected] = useState<InternshipSubmission[]>([]);
+    const spacing = useSpacing(2);
+
+    const updateInternshipList = () => {
+        setInternshipsPromise(api.internship.all(edition));
+    }
+
+    useEffect(updateInternshipList, []);
+
+    const handleSubmissionAccept = api.internship.approve
+    const handleSubmissionDiscard = api.internship.decline
+
+    const AcceptAction = ({ internship }: { internship: InternshipSubmission }) => {
+        const [open, setOpen] = useState(false);
+
+        return <>
+            <IconButton onClick={ () => setOpen(true) }><StickerCheckOutline /></IconButton>
+            { createPortal(
+                <AcceptSubmissionDialog onAccept={ comment => handleSubmissionAccept(internship, comment) } label="internship" open={ open } onClose={ () => setOpen(false) }/>,
+                document.getElementById("modals") as Element,
+            ) }
+        </>;
+    }
+
+    const DiscardAction = ({ internship }: { internship: InternshipSubmission }) => {
+        const [open, setOpen] = useState(false);
+
+        return <>
+            <IconButton onClick={ () => setOpen(true) }><StickerRemoveOutline /></IconButton>
+            { createPortal(
+                <DiscardSubmissionDialog onDiscard={ comment => handleSubmissionDiscard(internship, comment) } label="internship" open={ open } onClose={ () => setOpen(false) }/>,
+                document.getElementById("modals") as Element,
+            ) }
+        </>;
+    }
+
+    const columns: Column<InternshipSubmission>[] = [
+        {
+            title: t("internship.column.student"),
+            render: internship => <>{internship.intern.name} {internship.intern.surname}</>,
+        },
+        {
+            title: t("internship.column.album"),
+            field: "intern.albumNumber",
+        },
+        {
+            title: t("internship.column.type"),
+            field: "type.label.pl",
+        },
+        {
+            title: t("internship.column.changed"),
+            render: summary => summary.changed.format("yyyy-MM-DD HH:mm")
+        },
+        {
+            title: t("internship.column.status"),
+            render: summary => <StateLabel state={ summary.state } />
+        },
+        actionsColumn(internship => <>
+            { canAccept(internship) && <AcceptAction internship={ internship } /> }
+            { canDiscard(internship) && <DiscardAction internship={ internship } /> }
+            <IconButton component={ RouterLink } to={ route("management:edition_internship", { edition: edition.id || "", internship: internship.id || "" }) }><FileFind /></IconButton>
+        </>)
+    ];
+
+    return <Page>
+        <Page.Header maxWidth="lg">
+            <EditionManagement.Breadcrumbs>
+                <Typography color="textPrimary">{ t(title) }</Typography>
+            </EditionManagement.Breadcrumbs>
+            <Page.Title>{ t(title) }</Page.Title>
+        </Page.Header>
+        <Container maxWidth="lg" className={ spacing.vertical }>
+            <Actions>
+                <Button onClick={ updateInternshipList } startIcon={ <Refresh /> }>{ t("refresh") }</Button>
+            </Actions>
+            { selected.length > 0 && <BulkActions>
+                <AcceptanceActions label="internship" onAccept={ comment => handleSubmissionAccept(selected, comment) } onDiscard={ comment => handleSubmissionDiscard(selected, comment) } />
+            </BulkActions> }
+            <Async async={ result } keepValue>{
+                internships => <MaterialTable
+                    title={ <MaterialTableTitle result={ result } label={ t(title) }/> }
+                    columns={ columns }
+                    data={ internships }
+                    onSelectionChange={ internships => setSelected(internships) }
+                    options={ { selection: true, pageSize: 10 } }
+                    detailPanel={ internship => <Box m={ 3 }><ProposalPreview proposal={ internship } /></Box> }
+                />
+            }</Async>
+        </Container>
+    </Page>
+}
diff --git a/src/management/edition/manage.tsx b/src/management/edition/manage.tsx
index d1a8b0f..fefbf77 100644
--- a/src/management/edition/manage.tsx
+++ b/src/management/edition/manage.tsx
@@ -12,6 +12,7 @@ import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
 import { useAsync } from "@/hooks";
 import api from "@/management/api";
 import { Async } from "@/components/async";
+import { OneOrMany } from "@/helpers";
 
 const useSectionStyles = makeStyles((theme: Theme) => createStyles({
     header: {
@@ -49,7 +50,7 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
                     <ManagementLink icon={ <AccountMultiple/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
                         { t("management:edition.students.title") }
                     </ManagementLink>
-                    <ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
+                    <ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_internships", { edition: edition.id || "" }) }>
                         { t("management:edition.internships.title") }
                     </ManagementLink>
                     <ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }>
@@ -78,7 +79,7 @@ export const EditionManagement = ({ edition }: EditionManagementProps) => {
     </Page>
 }
 
-EditionManagement.Breadcrumbs = ({ children }: { children: React.ReactChild }) => {
+EditionManagement.Breadcrumbs = ({ children }: { children: OneOrMany<React.ReactChild> }) => {
     const edition = useContext<Edition | null>(EditionContext);
 
     return <Management.Breadcrumbs>
diff --git a/src/management/routing.tsx b/src/management/routing.tsx
index 525e944..86e8505 100644
--- a/src/management/routing.tsx
+++ b/src/management/routing.tsx
@@ -8,6 +8,8 @@ import { InternshipTypeManagement } from "@/management/type/list";
 import { EditionRouter, EditionManagement } from "@/management/edition/manage";
 import { EditionReportFields } from "@/management/edition/report/fields/list";
 import { EditionSettings } from "@/management/edition/settings";
+import { InternshipManagement } from "@/management/edition/internship/list";
+import { InternshipDetails } from "@/management/edition/internship/details";
 
 export const managementRoutes: Route[] = ([
     { name: "index", path: "/", content: ManagementIndex, exact: true },
@@ -15,7 +17,9 @@ export const managementRoutes: Route[] = ([
     { name: "edition_router", path: "/editions/:edition", content: EditionRouter },
     { name: "edition_report_form", path: "/editions/:edition/report", content: EditionReportFields, tags: ["edition"] },
     { name: "edition_settings", path: "/editions/:edition/settings", content: EditionSettings, tags: ["edition"] },
-    { name: "edition_manage", path: "/editions/:edition", content: EditionManagement, tags: ["edition"] },
+    { name: "edition_manage", path: "/editions/:edition", content: EditionManagement, tags: ["edition"], exact: true },
+    { name: "edition_internship", path: "/editions/:edition/internships/:internship", content: InternshipDetails, tags: ["edition"] },
+    { name: "edition_internships", path: "/editions/:edition/internships", content: InternshipManagement, tags: ["edition"] },
     { name: "editions", path: "/editions", content: EditionsManagement },
 
     { name: "types", path: "/types", content: InternshipTypeManagement },
diff --git a/src/pages/base.tsx b/src/pages/base.tsx
index bc6e5cc..ab07980 100644
--- a/src/pages/base.tsx
+++ b/src/pages/base.tsx
@@ -8,7 +8,7 @@ export type PageProps = {
 } & BoxProps;
 
 export type PageHeaderProps = {
-    maxWidth?: "sm" | "md" | "lg" | false
+    maxWidth?: "sm" | "md" | "lg" | "xl" | false
 } & HTMLProps<HTMLDivElement>
 
 export const Page = ({ title, children, ...props }: PageProps) => {
diff --git a/translations/management.pl.yaml b/translations/management.pl.yaml
index 7b63159..f026967 100644
--- a/translations/management.pl.yaml
+++ b/translations/management.pl.yaml
@@ -13,7 +13,17 @@ actions:
   edit: Edytuj
   add: Dodaj
 
+internship:
+  column:
+    student: Imię i Nazwisko
+    album: Numer Albumu
+    type: Rodzaj praktyki
+    status: Status
+    changed: Data aktualizacji
+
 edition:
+  internships:
+    title: Zgłoszenia praktyk
   index:
     title: "Edycje praktyk"
   field:
diff --git a/yarn.lock b/yarn.lock
index 1ee59b7..6246448 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6842,9 +6842,9 @@ md5.js@^1.3.4:
     safe-buffer "^5.1.2"
 
 mdi-material-ui@^6.17.0:
-  version "6.17.0"
-  resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.17.0.tgz#da69f0b7d7c6fc2255e6007ed8b8ca858c1aede7"
-  integrity sha512-eOprRu31lklPIS1WGe3cM0G/8glKl1WKRvewxjDrgXH2Ryxxg7uQ+uwDUwUEONtLku0p2ZOLzgXUIy2uRy5rLg==
+  version "6.21.0"
+  resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.21.0.tgz#e052215b0534e6c20abeb7e89c3fd8a421a519fd"
+  integrity sha512-rcO7KmaOhZq4H7vHYpwnjMqHfuJh0PmpEJNssEofWaqoSEABmIwRHUNmdJDPrjrBCTUm4m7tpYexqPOYzkb1Eg==
 
 mdn-data@2.0.4:
   version "2.0.4"