diff options
author | LASER-Yi <[email protected]> | 2021-08-25 00:33:59 +0800 |
---|---|---|
committer | LASER-Yi <[email protected]> | 2021-08-25 00:33:59 +0800 |
commit | 4a890b25617b0a50f3882f95e626c0f226382c7f (patch) | |
tree | 3925e7ab519e836208b5685b89726f3c39f1db67 | |
parent | e0b988b20f71d6fb6cc8bf5b55be1cbaf436d227 (diff) | |
download | bazarr-4a890b25617b0a50f3882f95e626c0f226382c7f.tar.gz bazarr-4a890b25617b0a50f3882f95e626c0f226382c7f.zip |
Support multi-language in subtitle upload modal
-rw-r--r-- | frontend/src/components/LanguageSelector.tsx | 2 | ||||
-rw-r--r-- | frontend/src/components/modals/MovieUploadModal.tsx | 242 | ||||
-rw-r--r-- | frontend/src/components/modals/SeriesUploadModal.tsx | 352 | ||||
-rw-r--r-- | frontend/src/components/modals/SubtitleUploadModal.tsx | 290 |
4 files changed, 472 insertions, 414 deletions
diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx index 17e17d8ac..f2466e1bc 100644 --- a/frontend/src/components/LanguageSelector.tsx +++ b/frontend/src/components/LanguageSelector.tsx @@ -7,7 +7,7 @@ interface Props { type RemovedSelectorProps<M extends boolean> = Omit< SelectorProps<Language.Info, M>, - "label" | "placeholder" + "label" >; export type LanguageSelectorProps<M extends boolean> = Override< diff --git a/frontend/src/components/modals/MovieUploadModal.tsx b/frontend/src/components/modals/MovieUploadModal.tsx index a0557e7b7..85e852f93 100644 --- a/frontend/src/components/modals/MovieUploadModal.tsx +++ b/frontend/src/components/modals/MovieUploadModal.tsx @@ -1,27 +1,19 @@ -import { faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { - FunctionComponent, - useCallback, - useMemo, - useState, -} from "react"; -import { Button, Container, Form } from "react-bootstrap"; -import { Column, Row } from "react-table"; +import React, { FunctionComponent, useCallback, useMemo } from "react"; +import { Form } from "react-bootstrap"; +import { Column } from "react-table"; import { dispatchTask } from "../../@modules/task"; import { createTask } from "../../@modules/task/utilities"; import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks"; import { MoviesApi } from "../../apis"; import { BuildKey } from "../../utilities"; -import { FileForm } from "../inputs"; -import { LanguageSelector } from "../LanguageSelector"; -import { SimpleTable } from "../tables"; -import BaseModal, { BaseModalProps } from "./BaseModal"; +import { BaseModalProps } from "./BaseModal"; import { useModalInformation } from "./hooks"; +import SubtitleUploadModal, { + PendingSubtitle, + Validator, +} from "./SubtitleUploadModal"; -interface PendingSubtitle { - file: File; - language: Language.Info; +interface Payload { forced: boolean; } @@ -30,178 +22,114 @@ export const TaskGroupName = "Uploading Subtitles..."; const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => { const modal = props; - const { payload, closeModal } = useModalInformation<Item.Movie>( - modal.modalKey - ); + const { payload } = useModalInformation<Item.Movie>(modal.modalKey); const profile = useProfileBy(payload?.profileId); const availableLanguages = useProfileItemsToLanguages(profile); - const [pending, setPending] = useState<PendingSubtitle[]>([]); - - const filelist = useMemo(() => pending.map((v) => v.file), [pending]); - - const setFiles = useCallback( - (files: File[]) => { - const list: PendingSubtitle[] = files.map((v) => ({ - file: v, - forced: availableLanguages[0].forced ?? false, - language: availableLanguages[0], - })); - setPending(list); + const update = useCallback(async (list: PendingSubtitle<Payload>[]) => { + return list; + }, []); + + const validate = useCallback<Validator<Payload>>( + (item) => { + if (item.language === null) { + return { + state: "error", + messages: ["Language is not selected"], + }; + } else if ( + payload?.subtitles.find((v) => v.code2 === item.language?.code2) !== + undefined + ) { + return { + state: "warning", + messages: ["Override existing subtitle"], + }; + } + return { + state: "valid", + messages: [], + }; }, - [availableLanguages] + [payload?.subtitles] ); - const upload = useCallback(() => { - if (payload === null || pending.length === 0) { - return; - } - - const { radarrId } = payload; - - const tasks = pending.map((v) => { - const { file, language, forced } = v; - - return createTask( - file.name, - radarrId, - MoviesApi.uploadSubtitles.bind(MoviesApi), - radarrId, - { - file: file, - forced, - hi: false, - language: language.code2, - } - ); - }); - - dispatchTask(TaskGroupName, tasks, "Uploading..."); - setFiles([]); - closeModal(); - }, [payload, closeModal, pending, setFiles]); - - const modify = useCallback( - (row: Row<PendingSubtitle>, info?: PendingSubtitle) => { - setPending((pd) => { - const newPending = [...pd]; - if (info) { - newPending[row.index] = info; - } else { - newPending.splice(row.index, 1); - } - return newPending; - }); + const upload = useCallback( + (items: PendingSubtitle<Payload>[]) => { + if (payload === null) { + return; + } + + const { radarrId } = payload; + + const tasks = items + .filter((v) => v.language !== null) + .map((v) => { + const { + file, + language, + payload: { forced }, + } = v; + + return createTask( + file.name, + radarrId, + MoviesApi.uploadSubtitles.bind(MoviesApi), + radarrId, + { + file: file, + forced, + hi: false, + language: language!.code2, + } + ); + }); + + dispatchTask(TaskGroupName, tasks, "Uploading..."); }, - [] + [payload] ); - const columns = useMemo<Column<PendingSubtitle>[]>( + const columns = useMemo<Column<PendingSubtitle<Payload>>[]>( () => [ { - id: "state", - Cell: () => { - return "hello"; - }, - }, - { - id: "name", - Header: "File", - accessor: (d) => d.file.name, - }, - { + id: "forced", Header: "Forced", - accessor: "forced", + accessor: "payload", Cell: ({ row, value, update }) => { const { original, index } = row; return ( <Form.Check custom + disabled={original.state === "fetching"} id={BuildKey(index, original.file.name, "forced")} - checked={value} + checked={value.forced} onChange={(v) => { const newInfo = { ...row.original }; - newInfo.forced = v.target.checked; + newInfo.payload.forced = v.target.checked; update && update(row, newInfo); }} ></Form.Check> ); }, }, - { - Header: "Language", - accessor: "language", - className: "w-25", - Cell: ({ row, update, value }) => { - return ( - <LanguageSelector - options={availableLanguages} - value={value} - onChange={(lang) => { - if (lang && update) { - const newInfo = { ...row.original }; - newInfo.language = lang; - update(row, newInfo); - } - }} - ></LanguageSelector> - ); - }, - }, - { - accessor: "file", - Cell: ({ row, update }) => { - return ( - <Button - size="sm" - variant="light" - onClick={() => { - update && update(row); - }} - > - <FontAwesomeIcon icon={faTrash}></FontAwesomeIcon> - </Button> - ); - }, - }, ], - [availableLanguages] - ); - - const canUpload = pending.length > 0; - - const footer = ( - <Button disabled={!canUpload} onClick={upload}> - Upload - </Button> + [] ); return ( - <BaseModal title={`Upload - ${payload?.title}`} footer={footer} {...modal}> - <Container fluid className="flex-column"> - <Form> - <Form.Group> - <FileForm - emptyText="Select..." - disabled={canUpload || availableLanguages.length === 0} - multiple - value={filelist} - onChange={setFiles} - ></FileForm> - </Form.Group> - </Form> - <div hidden={!canUpload}> - <SimpleTable - columns={columns} - data={pending} - responsive={false} - update={modify} - ></SimpleTable> - </div> - </Container> - </BaseModal> + <SubtitleUploadModal + hideAllLanguages + initial={{ forced: false }} + availableLanguages={availableLanguages} + columns={columns} + upload={upload} + update={update} + validate={validate} + {...modal} + ></SubtitleUploadModal> ); }; diff --git a/frontend/src/components/modals/SeriesUploadModal.tsx b/frontend/src/components/modals/SeriesUploadModal.tsx index e91e7fb04..dd5a9040e 100644 --- a/frontend/src/components/modals/SeriesUploadModal.tsx +++ b/frontend/src/components/modals/SeriesUploadModal.tsx @@ -1,213 +1,126 @@ -import { - faCheck, - faCircleNotch, - faInfoCircle, - faTrash, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { - FunctionComponent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; -import { Button, Container, Form } from "react-bootstrap"; -import { Column, TableUpdater } from "react-table"; -import { FileForm, LanguageSelector, MessageIcon, SimpleTable } from ".."; +import React, { FunctionComponent, useCallback, useMemo } from "react"; +import { Column } from "react-table"; import { dispatchTask } from "../../@modules/task"; import { createTask } from "../../@modules/task/utilities"; import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks"; import { EpisodesApi, SubtitlesApi } from "../../apis"; import { Selector } from "../inputs"; -import BaseModal, { BaseModalProps } from "./BaseModal"; +import { BaseModalProps } from "./BaseModal"; import { useModalInformation } from "./hooks"; +import SubtitleUploadModal, { + PendingSubtitle, + Validator, +} from "./SubtitleUploadModal"; -interface PendingSubtitle { - file: File; - fetching: boolean; - instance?: Item.Episode; +interface Payload { + instance: Item.Episode | null; } -type EpisodeMap = { - [name: string]: Item.Episode; -}; - -interface SerieProps { +interface SeriesProps { episodes: readonly Item.Episode[]; } export const TaskGroupName = "Uploading Subtitles..."; -const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({ +const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({ episodes, ...modal }) => { - const { payload, closeModal } = useModalInformation<Item.Series>( - modal.modalKey - ); - - const [pending, setPending] = useState<PendingSubtitle[]>([]); + const { payload } = useModalInformation<Item.Series>(modal.modalKey); const profile = useProfileBy(payload?.profileId); - const avaliableLanguages = useProfileItemsToLanguages(profile); - - const [language, setLanguage] = useState<Language.Info | null>(null); - - useEffect(() => { - if (avaliableLanguages.length > 0) { - setLanguage(avaliableLanguages[0]); - } - }, [avaliableLanguages]); - - const filelist = useMemo(() => pending.map((v) => v.file), [pending]); + const availableLanguages = useProfileItemsToLanguages(profile); - const checkEpisodes = useCallback( - async (list: PendingSubtitle[]) => { + const update = useCallback( + async (list: PendingSubtitle<Payload>[]) => { + const newList = [...list]; const names = list.map((v) => v.file.name); if (names.length > 0) { const results = await SubtitlesApi.info(names); - const episodeMap = results.reduce<EpisodeMap>((prev, curr) => { - const ep = episodes.find( - (v) => v.season === curr.season && v.episode === curr.episode - ); - if (ep) { - prev[curr.filename] = ep; + // TODO: Optimization + newList.forEach((v) => { + const info = results.find((f) => f.filename === v.file.name); + if (info) { + v.payload.instance = + episodes.find( + (e) => e.season === info.season && e.episode === info.episode + ) ?? null; } - return prev; - }, {}); - - setPending((pd) => - pd.map((v) => { - const instance = episodeMap[v.file.name]; - return { - ...v, - instance, - fetching: false, - }; - }) - ); + }); } - }, - [episodes] - ); - const setFiles = useCallback( - (files: File[]) => { - // At lease 1 language is required - const list: PendingSubtitle[] = files.map((f) => { - return { - file: f, - didCheck: false, - fetching: true, - }; - }); - setPending(list); - checkEpisodes(list); + return newList; }, - [checkEpisodes] + [episodes] ); - const upload = useCallback(() => { - if (payload === null || language === null) { - return; + const validate = useCallback<Validator<Payload>>((item) => { + const { language } = item; + const { instance } = item.payload; + if (language === null || instance === null) { + return { + state: "error", + messages: ["Language or Episode is not selected"], + }; + } else if ( + instance.subtitles.find((v) => v.code2 === language.code2) !== undefined + ) { + return { + state: "warning", + messages: ["Override existing subtitle"], + }; } + return { + state: "valid", + messages: [], + }; + }, []); + + const upload = useCallback( + (items: PendingSubtitle<Payload>[]) => { + if (payload === null) { + return; + } - const { sonarrSeriesId: seriesid } = payload; - const { code2, hi, forced } = language; - - const tasks = pending - .filter((v) => v.instance !== undefined) - .map((v) => { - const { sonarrEpisodeId: episodeid } = v.instance!; - - const form: FormType.UploadSubtitle = { - file: v.file, - language: code2, - hi: hi ?? false, - forced: forced ?? false, - }; - - return createTask( - v.file.name, - episodeid, - EpisodesApi.uploadSubtitles.bind(EpisodesApi), - seriesid, - episodeid, - form - ); - }); - - dispatchTask(TaskGroupName, tasks, "Uploading subtitles..."); - setFiles([]); - closeModal(); - }, [payload, pending, language, closeModal, setFiles]); + const { sonarrSeriesId: seriesid } = payload; + + const tasks = items + .filter((v) => v.payload.instance !== undefined) + .map((v) => { + const { code2, hi, forced } = v.language!; + const { sonarrEpisodeId: episodeid } = v.payload.instance!; + + const form: FormType.UploadSubtitle = { + file: v.file, + language: code2, + hi: hi ?? false, + forced: forced ?? false, + }; + + return createTask( + v.file.name, + episodeid, + EpisodesApi.uploadSubtitles.bind(EpisodesApi), + seriesid, + episodeid, + form + ); + }); - const canUpload = useMemo( - () => - pending.length > 0 && - pending.every((v) => v.instance !== undefined) && - language, - [pending, language] + dispatchTask(TaskGroupName, tasks, "Uploading subtitles..."); + }, + [payload] ); - const showTable = pending.length > 0; - - const columns = useMemo<Column<PendingSubtitle>[]>( + const columns = useMemo<Column<PendingSubtitle<Payload>>[]>( () => [ { - id: "Icon", - accessor: "fetching", - className: "text-center", - Cell: ({ value: fetching, row: { original } }) => { - let icon = faCircleNotch; - let color: string | undefined = undefined; - let spin = false; - let msgs: string[] = []; - - const override = useMemo( - () => - original.instance?.subtitles.find( - (v) => v.code2 === language?.code2 - ) !== undefined, - [original.instance?.subtitles] - ); - - if (fetching) { - spin = true; - } else if (override) { - icon = faInfoCircle; - color = "var(--warning)"; - msgs.push("Overwrite existing subtitle"); - } else if (original.instance) { - icon = faCheck; - color = "var(--success)"; - } else { - icon = faInfoCircle; - color = "var(--warning)"; - msgs.push("Season or episode info is missing"); - } - - return ( - <MessageIcon - messages={msgs} - color={color} - icon={icon} - spin={spin} - ></MessageIcon> - ); - }, - }, - { - Header: "File", - accessor: (d) => d.file.name, - }, - { + id: "instance", Header: "Episode", - accessor: "instance", + accessor: "payload", className: "vw-1", Cell: ({ value, row, update }) => { const options = episodes.map<SelectorOption<Item.Episode>>((ep) => ({ @@ -219,7 +132,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({ (ep: Nullable<Item.Episode>) => { if (ep) { const newInfo = { ...row.original }; - newInfo.instance = ep; + newInfo.payload.instance = ep; update && update(row, newInfo); } }, @@ -228,101 +141,28 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({ return ( <Selector + disabled={row.original.state === "fetching"} options={options} - value={value ?? null} + value={value.instance} onChange={change} ></Selector> ); }, }, - { - accessor: "file", - Cell: ({ row, update }) => { - return ( - <Button - size="sm" - variant="light" - onClick={() => { - update && update(row); - }} - > - <FontAwesomeIcon icon={faTrash}></FontAwesomeIcon> - </Button> - ); - }, - }, ], - [language?.code2, episodes] - ); - - const updateItem = useCallback<TableUpdater<PendingSubtitle>>( - (row, info?: PendingSubtitle) => { - setPending((pd) => { - const newPending = [...pd]; - if (info) { - newPending[row.index] = info; - } else { - newPending.splice(row.index, 1); - } - return newPending; - }); - }, - [] - ); - - const footer = ( - <div className="d-flex flex-row flex-grow-1 justify-content-between"> - <div className="w-25"> - <LanguageSelector - options={avaliableLanguages} - value={language} - onChange={(l) => { - if (l) { - setLanguage(l); - } - }} - ></LanguageSelector> - </div> - <div> - <Button - disabled={pending.length === 0} - variant="outline-secondary" - className="mr-2" - onClick={() => setFiles([])} - > - Clean - </Button> - <Button disabled={!canUpload} onClick={upload}> - Upload - </Button> - </div> - </div> + [episodes] ); return ( - <BaseModal size="lg" title="Upload Subtitles" footer={footer} {...modal}> - <Container fluid className="flex-column"> - <Form> - <Form.Group> - <FileForm - emptyText="Select..." - disabled={showTable || avaliableLanguages.length === 0} - multiple - value={filelist} - onChange={setFiles} - ></FileForm> - </Form.Group> - </Form> - <div hidden={!showTable}> - <SimpleTable - columns={columns} - data={pending} - responsive={false} - update={updateItem} - ></SimpleTable> - </div> - </Container> - </BaseModal> + <SubtitleUploadModal + columns={columns} + initial={{ instance: null }} + availableLanguages={availableLanguages} + upload={upload} + update={update} + validate={validate} + {...modal} + ></SubtitleUploadModal> ); }; diff --git a/frontend/src/components/modals/SubtitleUploadModal.tsx b/frontend/src/components/modals/SubtitleUploadModal.tsx new file mode 100644 index 000000000..4584826fb --- /dev/null +++ b/frontend/src/components/modals/SubtitleUploadModal.tsx @@ -0,0 +1,290 @@ +import { + faCheck, + faCircleNotch, + faInfoCircle, + faTimes, + faTrash, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Button, Container, Form } from "react-bootstrap"; +import { Column, TableUpdater } from "react-table"; +import { LanguageSelector, MessageIcon } from ".."; +import { FileForm } from "../inputs"; +import { SimpleTable } from "../tables"; +import BaseModal, { BaseModalProps } from "./BaseModal"; +import { useCloseModal } from "./hooks"; + +export interface PendingSubtitle<P> { + file: File; + state: "valid" | "fetching" | "warning" | "error"; + messages: string[]; + language: Language.Info | null; + payload: P; +} + +export type Validator<T> = ( + item: PendingSubtitle<T> +) => Pick<PendingSubtitle<T>, "state" | "messages">; + +interface Props<T> { + initial: T; + availableLanguages: Language.Info[]; + upload: (items: PendingSubtitle<T>[]) => void; + update: (items: PendingSubtitle<T>[]) => Promise<PendingSubtitle<T>[]>; + validate: Validator<T>; + columns: Column<PendingSubtitle<T>>[]; + hideAllLanguages?: boolean; +} + +export default function SubtitleUploadModal<T>( + props: Props<T> & Omit<BaseModalProps, "footer" | "title" | "size"> +) { + const { + initial, + columns, + upload, + update, + validate, + availableLanguages, + hideAllLanguages, + } = props; + + const closeModal = useCloseModal(); + + const [pending, setPending] = useState<PendingSubtitle<T>[]>([]); + + const fileList = useMemo(() => pending.map((v) => v.file), [pending]); + + const initialRef = useRef(initial); + + const setFiles = useCallback( + async (files: File[]) => { + const initialLanguage = + availableLanguages.length > 0 ? availableLanguages[0] : null; + let list = files.map<PendingSubtitle<T>>((file) => ({ + file, + state: "fetching", + messages: [], + language: initialLanguage, + payload: { ...initialRef.current }, + })); + + if (update) { + setPending(list); + list = await update(list); + } else { + list = list.map<PendingSubtitle<T>>((v) => ({ + ...v, + state: "valid", + })); + } + + list = list.map((v) => ({ + ...v, + ...validate(v), + })); + + setPending(list); + }, + [update, validate, availableLanguages] + ); + + const modify = useCallback<TableUpdater<PendingSubtitle<T>>>( + (row, info?: PendingSubtitle<T>) => { + setPending((pd) => { + const newPending = [...pd]; + if (info) { + info = { ...info, ...validate(info) }; + newPending[row.index] = info; + } else { + newPending.splice(row.index, 1); + } + return newPending; + }); + }, + [validate] + ); + + useEffect(() => { + setPending((pd) => { + const newPd = pd.map((v) => { + if (v.state !== "fetching") { + return { ...v, ...validate(v) }; + } else { + return v; + } + }); + + return newPd; + }); + }, [validate]); + + const columnsWithAction = useMemo<Column<PendingSubtitle<T>>[]>( + () => [ + { + id: "icon", + accessor: "state", + className: "text-center", + Cell: ({ value, row }) => { + let icon = faCircleNotch; + let color: string | undefined = undefined; + let spin = false; + + switch (value) { + case "fetching": + spin = true; + break; + case "warning": + icon = faInfoCircle; + color = "var(--warning)"; + break; + case "valid": + icon = faCheck; + color = "var(--success)"; + break; + default: + icon = faTimes; + color = "var(--danger)"; + break; + } + + const messages = row.original.messages; + + return ( + <MessageIcon + messages={messages} + color={color} + icon={icon} + spin={spin} + ></MessageIcon> + ); + }, + }, + { + Header: "File", + accessor: (d) => d.file.name, + }, + ...columns, + { + id: "language", + Header: "Language", + accessor: "language", + className: "w-25", + Cell: ({ row, update, value }) => { + return ( + <LanguageSelector + disabled={row.original.state === "fetching"} + options={availableLanguages} + value={value} + onChange={(lang) => { + if (lang && update) { + const newInfo = { ...row.original }; + newInfo.language = lang; + update(row, newInfo); + } + }} + ></LanguageSelector> + ); + }, + }, + { + id: "action", + accessor: "file", + Cell: ({ row, update }) => ( + <Button + size="sm" + variant="light" + disabled={row.original.state === "fetching"} + onClick={() => { + update && update(row); + }} + > + <FontAwesomeIcon icon={faTrash}></FontAwesomeIcon> + </Button> + ), + }, + ], + [columns, availableLanguages] + ); + + const showTable = pending.length > 0; + + const canUpload = useMemo( + () => + pending.length > 0 && + pending.every((v) => v.state === "valid" || v.state === "warning"), + [pending] + ); + + const footer = ( + <div className="d-flex flex-row-reverse flex-grow-1 justify-content-between"> + <div> + <Button + hidden={!showTable} + variant="outline-secondary" + className="mr-2" + onClick={() => setFiles([])} + > + Clean + </Button> + <Button + disabled={!canUpload || !showTable} + onClick={() => { + upload(pending); + closeModal(); + }} + > + Upload + </Button> + </div> + <div className="w-25" hidden={hideAllLanguages}> + <LanguageSelector + options={availableLanguages} + value={null} + disabled={!showTable} + onChange={(lang) => { + if (lang) { + setPending((pd) => + pd + .map((v) => ({ ...v, language: lang })) + .map((v) => ({ ...v, ...validate(v) })) + ); + } + }} + ></LanguageSelector> + </div> + </div> + ); + + return ( + <BaseModal + size={showTable ? "xl" : "lg"} + title="Upload Subtitles" + footer={footer} + {...props} + > + <Container fluid className="flex-column"> + <Form> + <Form.Group> + <FileForm + disabled={showTable} + emptyText="Select..." + multiple + value={fileList} + onChange={setFiles} + ></FileForm> + </Form.Group> + </Form> + <div hidden={!showTable}> + <SimpleTable + columns={columnsWithAction} + data={pending} + responsive={false} + update={modify} + ></SimpleTable> + </div> + </Container> + </BaseModal> + ); +} |