Co-authored-by: Qstick <qstick@gmail.com>pull/1771/head
parent
cb520b2264
commit
1706728230
@ -0,0 +1,48 @@
|
|||||||
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
|
||||||
|
export interface Error {
|
||||||
|
responseJSON: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionDeleteState {
|
||||||
|
isDeleting: boolean;
|
||||||
|
deleteError: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionSaveState {
|
||||||
|
isSaving: boolean;
|
||||||
|
saveError: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagedAppSectionState {
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionSchemaState<T> {
|
||||||
|
isSchemaFetching: boolean;
|
||||||
|
isSchemaPopulated: boolean;
|
||||||
|
schemaError: Error;
|
||||||
|
schema: {
|
||||||
|
items: T[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionItemState<T> {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
item: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppSectionState<T> {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
items: T[];
|
||||||
|
sortKey: string;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppSectionState;
|
@ -0,0 +1,43 @@
|
|||||||
|
import IndexerAppState from './IndexerAppState';
|
||||||
|
import SettingsAppState from './SettingsAppState';
|
||||||
|
import TagsAppState from './TagsAppState';
|
||||||
|
|
||||||
|
interface FilterBuilderPropOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterBuilderProp<T> {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
valueType?: string;
|
||||||
|
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropertyFilter {
|
||||||
|
key: string;
|
||||||
|
value: boolean | string | number | string[] | number[];
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
filers: PropertyFilter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomFilter {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
filers: PropertyFilter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
indexers: IndexerAppState;
|
||||||
|
settings: SettingsAppState;
|
||||||
|
tags: TagsAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppState;
|
@ -0,0 +1,12 @@
|
|||||||
|
import Indexer from 'typings/Indexer';
|
||||||
|
import AppSectionState, {
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState,
|
||||||
|
} from './AppSectionState';
|
||||||
|
|
||||||
|
interface IndexerAppState
|
||||||
|
extends AppSectionState<Indexer>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export default IndexerAppState;
|
@ -0,0 +1,33 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import Application from 'typings/Application';
|
||||||
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
|
import Notification from 'typings/Notification';
|
||||||
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
|
|
||||||
|
export interface ApplicationAppState
|
||||||
|
extends AppSectionState<Application>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface DownloadClientAppState
|
||||||
|
extends AppSectionState<DownloadClient>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NotificationAppState
|
||||||
|
extends AppSectionState<Notification>,
|
||||||
|
AppSectionDeleteState {}
|
||||||
|
|
||||||
|
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||||
|
|
||||||
|
interface SettingsAppState {
|
||||||
|
applications: ApplicationAppState;
|
||||||
|
downloadClients: DownloadClientAppState;
|
||||||
|
notifications: NotificationAppState;
|
||||||
|
uiSettings: UiSettingsAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsAppState;
|
@ -0,0 +1,12 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import AppSectionState, {
|
||||||
|
AppSectionDeleteState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
|
||||||
|
export interface Tag extends ModelBase {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||||
|
|
||||||
|
export default TagsAppState;
|
@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'input': string;
|
||||||
|
'selectCell': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageApplicationsEditModalContent from './ManageApplicationsEditModalContent';
|
||||||
|
|
||||||
|
interface ManageApplicationsEditModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
applicationIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageApplicationsEditModal(props: ManageApplicationsEditModalProps) {
|
||||||
|
const { isOpen, applicationIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageApplicationsEditModalContent
|
||||||
|
applicationIds={applicationIds}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageApplicationsEditModal;
|
@ -0,0 +1,16 @@
|
|||||||
|
.modalFooter {
|
||||||
|
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointExtraSmall) {
|
||||||
|
.modalFooter {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'modalFooter': string;
|
||||||
|
'selected': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,108 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import { ApplicationSyncLevel } from 'typings/Application';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ManageApplicationsEditModalContent.css';
|
||||||
|
|
||||||
|
interface SavePayload {
|
||||||
|
syncLevel?: ApplicationSyncLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageApplicationsEditModalContentProps {
|
||||||
|
applicationIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
const syncLevelOptions = [
|
||||||
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
|
{ key: ApplicationSyncLevel.Disabled, value: translate('Disabled') },
|
||||||
|
{ key: ApplicationSyncLevel.AddOnly, value: translate('AddOnly') },
|
||||||
|
{ key: ApplicationSyncLevel.FullSync, value: translate('FullSync') },
|
||||||
|
];
|
||||||
|
|
||||||
|
function ManageApplicationsEditModalContent(
|
||||||
|
props: ManageApplicationsEditModalContentProps
|
||||||
|
) {
|
||||||
|
const { applicationIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
const [syncLevel, setSyncLevel] = useState(NO_CHANGE);
|
||||||
|
|
||||||
|
const save = useCallback(() => {
|
||||||
|
let hasChanges = false;
|
||||||
|
const payload: SavePayload = {};
|
||||||
|
|
||||||
|
if (syncLevel !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.syncLevel = syncLevel as ApplicationSyncLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
onSavePress(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [syncLevel, onSavePress, onModalClose]);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ name, value }: { name: string; value: string }) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'syncLevel':
|
||||||
|
setSyncLevel(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`EditApplicationsModalContent Unknown Input: '${name}'`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCount = applicationIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('EditSelectedApplications')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('SyncLevel')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="syncLevel"
|
||||||
|
value={syncLevel}
|
||||||
|
values={syncLevelOptions}
|
||||||
|
helpText={`${translate('SyncLevelAddRemove')}<br>${translate(
|
||||||
|
'SyncLevelFull'
|
||||||
|
)}`}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div className={styles.selected}>
|
||||||
|
{translate('CountApplicationsSelected', [selectedCount])}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageApplicationsEditModalContent;
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageApplicationsModalContent from './ManageApplicationsModalContent';
|
||||||
|
|
||||||
|
interface ManageApplicationsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageApplicationsModal(props: ManageApplicationsModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageApplicationsModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageApplicationsModal;
|
@ -0,0 +1,16 @@
|
|||||||
|
.leftButtons,
|
||||||
|
.rightButtons {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 50%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightButtons {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'deleteButton': string;
|
||||||
|
'leftButtons': string;
|
||||||
|
'rightButtons': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,282 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { ApplicationAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
bulkDeleteApplications,
|
||||||
|
bulkEditApplications,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import ManageApplicationsEditModal from './Edit/ManageApplicationsEditModal';
|
||||||
|
import ManageApplicationsModalRow from './ManageApplicationsModalRow';
|
||||||
|
import TagsModal from './Tags/TagsModal';
|
||||||
|
import styles from './ManageApplicationsModalContent.css';
|
||||||
|
|
||||||
|
// TODO: This feels janky to do, but not sure of a better way currently
|
||||||
|
type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
|
typeof ManageApplicationsModalRow
|
||||||
|
>['onSelectedChange'];
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: translate('Name'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'implementation',
|
||||||
|
label: translate('Implementation'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'syncLevel',
|
||||||
|
label: translate('SyncLevel'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
label: translate('Tags'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ManageApplicationsModalContentProps {
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageApplicationsModalContent(
|
||||||
|
props: ManageApplicationsModalContentProps
|
||||||
|
) {
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
isDeleting,
|
||||||
|
isSaving,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
}: ApplicationAppState = useSelector(
|
||||||
|
createClientSideCollectionSelector('settings.applications')
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
|
||||||
|
const [isSavingTags, setIsSavingTags] = useState(false);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds: number[] = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onDeletePress = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}, [setIsDeleteModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteModalClose = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}, [setIsDeleteModalOpen]);
|
||||||
|
|
||||||
|
const onEditPress = useCallback(() => {
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onEditModalClose = useCallback(() => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onConfirmDelete = useCallback(() => {
|
||||||
|
dispatch(bulkDeleteApplications({ ids: selectedIds }));
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}, [selectedIds, dispatch]);
|
||||||
|
|
||||||
|
const onSavePress = useCallback(
|
||||||
|
(payload: object) => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
bulkEditApplications({
|
||||||
|
ids: selectedIds,
|
||||||
|
...payload,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTagsPress = useCallback(() => {
|
||||||
|
setIsTagsModalOpen(true);
|
||||||
|
}, [setIsTagsModalOpen]);
|
||||||
|
|
||||||
|
const onTagsModalClose = useCallback(() => {
|
||||||
|
setIsTagsModalOpen(false);
|
||||||
|
}, [setIsTagsModalOpen]);
|
||||||
|
|
||||||
|
const onApplyTagsPress = useCallback(
|
||||||
|
(tags: number[], applyTags: string) => {
|
||||||
|
setIsSavingTags(true);
|
||||||
|
setIsTagsModalOpen(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
bulkEditApplications({
|
||||||
|
ids: selectedIds,
|
||||||
|
tags,
|
||||||
|
applyTags,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectAllChange = useCallback(
|
||||||
|
({ value }: SelectStateInputProps) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
|
||||||
|
({ id, value, shiftKey = false }) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorMessage = getErrorMessage(
|
||||||
|
error,
|
||||||
|
'Unable to load download clients.'
|
||||||
|
);
|
||||||
|
const anySelected = selectedCount > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('ManageApplications')}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length && (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoApplicationsFound')}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
|
<Table
|
||||||
|
columns={COLUMNS}
|
||||||
|
horizontalScroll={true}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<ManageApplicationsModalRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
{...item}
|
||||||
|
columns={COLUMNS}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<div className={styles.leftButtons}>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onDeletePress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onEditPress}
|
||||||
|
>
|
||||||
|
{translate('Edit')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving && isSavingTags}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onTagsPress}
|
||||||
|
>
|
||||||
|
{translate('SetTags')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<ManageApplicationsEditModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
onModalClose={onEditModalClose}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
applicationIds={selectedIds}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagsModal
|
||||||
|
isOpen={isTagsModalOpen}
|
||||||
|
ids={selectedIds}
|
||||||
|
onApplyTagsPress={onApplyTagsPress}
|
||||||
|
onModalClose={onTagsModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteSelectedApplications')}
|
||||||
|
message={translate('DeleteSelectedApplicationsMessageText', [
|
||||||
|
selectedIds.length,
|
||||||
|
])}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={onConfirmDelete}
|
||||||
|
onCancel={onDeleteModalClose}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageApplicationsModalContent;
|
@ -0,0 +1,8 @@
|
|||||||
|
.name,
|
||||||
|
.syncLevel,
|
||||||
|
.tags,
|
||||||
|
.implementation {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'implementation': string;
|
||||||
|
'name': string;
|
||||||
|
'syncLevel': string;
|
||||||
|
'tags': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,82 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { ApplicationSyncLevel } from 'typings/Application';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ManageApplicationsModalRow.css';
|
||||||
|
|
||||||
|
interface ManageApplicationsModalRowProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
syncLevel: string;
|
||||||
|
implementation: string;
|
||||||
|
tags: number[];
|
||||||
|
columns: Column[];
|
||||||
|
isSelected?: boolean;
|
||||||
|
onSelectedChange(result: SelectStateInputProps): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageApplicationsModalRow(props: ManageApplicationsModalRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isSelected,
|
||||||
|
name,
|
||||||
|
syncLevel,
|
||||||
|
implementation,
|
||||||
|
tags,
|
||||||
|
onSelectedChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const onSelectedChangeWrapper = useCallback(
|
||||||
|
(result: SelectStateInputProps) => {
|
||||||
|
onSelectedChange({
|
||||||
|
...result,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onSelectedChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChangeWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.implementation}>
|
||||||
|
{implementation}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.syncLevel}>
|
||||||
|
{syncLevel === ApplicationSyncLevel.AddOnly && (
|
||||||
|
<Label kind={kinds.WARNING}>{translate('AddRemoveOnly')}</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{syncLevel === ApplicationSyncLevel.FullSync && (
|
||||||
|
<Label kind={kinds.SUCCESS}>{translate('FullSync')}</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{syncLevel === ApplicationSyncLevel.Disabled && (
|
||||||
|
<Label kind={kinds.DISABLED} outline={true}>
|
||||||
|
{translate('Disabled')}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.tags}>
|
||||||
|
<TagListConnector tags={tags} />
|
||||||
|
</TableRowCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageApplicationsModalRow;
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import TagsModalContent from './TagsModalContent';
|
||||||
|
|
||||||
|
interface TagsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
ids: number[];
|
||||||
|
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsModal(props: TagsModalProps) {
|
||||||
|
const { isOpen, onModalClose, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<TagsModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsModal;
|
@ -0,0 +1,12 @@
|
|||||||
|
.renameIcon {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
'renameIcon': string;
|
||||||
|
'result': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,183 @@
|
|||||||
|
import { uniq } from 'lodash';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { ApplicationAppState } from 'App/State/SettingsAppState';
|
||||||
|
import { Tag } from 'App/State/TagsAppState';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
|
import Application from 'typings/Application';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TagsModalContent.css';
|
||||||
|
|
||||||
|
interface TagsModalContentProps {
|
||||||
|
ids: number[];
|
||||||
|
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsModalContent(props: TagsModalContentProps) {
|
||||||
|
const { ids, onModalClose, onApplyTagsPress } = props;
|
||||||
|
|
||||||
|
const allApplications: ApplicationAppState = useSelector(
|
||||||
|
(state: AppState) => state.settings.applications
|
||||||
|
);
|
||||||
|
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||||
|
|
||||||
|
const [tags, setTags] = useState<number[]>([]);
|
||||||
|
const [applyTags, setApplyTags] = useState('add');
|
||||||
|
|
||||||
|
const applicationsTags = useMemo(() => {
|
||||||
|
const tags = ids.reduce((acc: number[], id) => {
|
||||||
|
const s = allApplications.items.find((s: Application) => s.id === id);
|
||||||
|
|
||||||
|
if (s) {
|
||||||
|
acc.push(...s.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return uniq(tags);
|
||||||
|
}, [ids, allApplications]);
|
||||||
|
|
||||||
|
const onTagsChange = useCallback(
|
||||||
|
({ value }: { value: number[] }) => {
|
||||||
|
setTags(value);
|
||||||
|
},
|
||||||
|
[setTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onApplyTagsChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
setApplyTags(value);
|
||||||
|
},
|
||||||
|
[setApplyTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onApplyPress = useCallback(() => {
|
||||||
|
onApplyTagsPress(tags, applyTags);
|
||||||
|
}, [tags, applyTags, onApplyTagsPress]);
|
||||||
|
|
||||||
|
const applyTagsOptions = [
|
||||||
|
{ key: 'add', value: translate('Add') },
|
||||||
|
{ key: 'remove', value: translate('Remove') },
|
||||||
|
{ key: 'replace', value: translate('Replace') },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
value={tags}
|
||||||
|
onChange={onTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="applyTags"
|
||||||
|
value={applyTags}
|
||||||
|
values={applyTagsOptions}
|
||||||
|
helpTexts={[
|
||||||
|
translate('ApplyTagsHelpTexts1'),
|
||||||
|
translate('ApplyTagsHelpTexts2'),
|
||||||
|
translate('ApplyTagsHelpTexts3'),
|
||||||
|
translate('ApplyTagsHelpTexts4'),
|
||||||
|
]}
|
||||||
|
onChange={onApplyTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Result')}</FormLabel>
|
||||||
|
|
||||||
|
<div className={styles.result}>
|
||||||
|
{applicationsTags.map((id) => {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTag =
|
||||||
|
(applyTags === 'remove' && tags.indexOf(id) > -1) ||
|
||||||
|
(applyTags === 'replace' && tags.indexOf(id) === -1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={tag.id}
|
||||||
|
title={
|
||||||
|
removeTag
|
||||||
|
? translate('RemovingTag')
|
||||||
|
: translate('ExistingTag')
|
||||||
|
}
|
||||||
|
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{(applyTags === 'add' || applyTags === 'replace') &&
|
||||||
|
tags.map((id) => {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationsTags.indexOf(id) > -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={tag.id}
|
||||||
|
title={translate('AddingTag')}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
|
{translate('Apply')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsModalContent;
|
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageDownloadClientsEditModalContent from './ManageDownloadClientsEditModalContent';
|
||||||
|
|
||||||
|
interface ManageDownloadClientsEditModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
downloadClientIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageDownloadClientsEditModal(
|
||||||
|
props: ManageDownloadClientsEditModalProps
|
||||||
|
) {
|
||||||
|
const { isOpen, downloadClientIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageDownloadClientsEditModalContent
|
||||||
|
downloadClientIds={downloadClientIds}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsEditModal;
|
@ -0,0 +1,16 @@
|
|||||||
|
.modalFooter {
|
||||||
|
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointExtraSmall) {
|
||||||
|
.modalFooter {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'modalFooter': string;
|
||||||
|
'selected': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ManageDownloadClientsEditModalContent.css';
|
||||||
|
|
||||||
|
interface SavePayload {
|
||||||
|
enable?: boolean;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageDownloadClientsEditModalContentProps {
|
||||||
|
downloadClientIds: number[];
|
||||||
|
onSavePress(payload: object): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
const enableOptions = [
|
||||||
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
|
{ key: 'enabled', value: translate('Enabled') },
|
||||||
|
{ key: 'disabled', value: translate('Disabled') },
|
||||||
|
];
|
||||||
|
|
||||||
|
function ManageDownloadClientsEditModalContent(
|
||||||
|
props: ManageDownloadClientsEditModalContentProps
|
||||||
|
) {
|
||||||
|
const { downloadClientIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
|
const [enable, setEnable] = useState(NO_CHANGE);
|
||||||
|
const [priority, setPriority] = useState<null | string | number>(null);
|
||||||
|
|
||||||
|
const save = useCallback(() => {
|
||||||
|
let hasChanges = false;
|
||||||
|
const payload: SavePayload = {};
|
||||||
|
|
||||||
|
if (enable !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.enable = enable === 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority !== null) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.priority = priority as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
onSavePress(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [enable, priority, onSavePress, onModalClose]);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ name, value }: { name: string; value: string }) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'enable':
|
||||||
|
setEnable(value);
|
||||||
|
break;
|
||||||
|
case 'priority':
|
||||||
|
setPriority(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
`EditDownloadClientsModalContent Unknown Input: '${name}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCount = downloadClientIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('EditSelectedDownloadClients')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Enabled')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="enable"
|
||||||
|
value={enable}
|
||||||
|
values={enableOptions}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ClientPriority')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="priority"
|
||||||
|
value={priority}
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
helpText={translate('DownloadClientPriorityHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div className={styles.selected}>
|
||||||
|
{translate('CountDownloadClientsSelected', [selectedCount])}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsEditModalContent;
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ManageDownloadClientsModalContent from './ManageDownloadClientsModalContent';
|
||||||
|
|
||||||
|
interface ManageDownloadClientsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageDownloadClientsModal(props: ManageDownloadClientsModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ManageDownloadClientsModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsModal;
|
@ -0,0 +1,16 @@
|
|||||||
|
.leftButtons,
|
||||||
|
.rightButtons {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 50%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightButtons {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'deleteButton': string;
|
||||||
|
'leftButtons': string;
|
||||||
|
'rightButtons': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,240 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
bulkDeleteDownloadClients,
|
||||||
|
bulkEditDownloadClients,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal';
|
||||||
|
import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow';
|
||||||
|
import styles from './ManageDownloadClientsModalContent.css';
|
||||||
|
|
||||||
|
// TODO: This feels janky to do, but not sure of a better way currently
|
||||||
|
type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
|
typeof ManageDownloadClientsModalRow
|
||||||
|
>['onSelectedChange'];
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: translate('Name'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'implementation',
|
||||||
|
label: translate('Implementation'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enable',
|
||||||
|
label: translate('Enabled'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'priority',
|
||||||
|
label: translate('ClientPriority'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ManageDownloadClientsModalContentProps {
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageDownloadClientsModalContent(
|
||||||
|
props: ManageDownloadClientsModalContentProps
|
||||||
|
) {
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
isDeleting,
|
||||||
|
isSaving,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
}: DownloadClientAppState = useSelector(
|
||||||
|
createClientSideCollectionSelector('settings.downloadClients')
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds: number[] = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
const onDeletePress = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}, [setIsDeleteModalOpen]);
|
||||||
|
|
||||||
|
const onDeleteModalClose = useCallback(() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}, [setIsDeleteModalOpen]);
|
||||||
|
|
||||||
|
const onEditPress = useCallback(() => {
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onEditModalClose = useCallback(() => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
}, [setIsEditModalOpen]);
|
||||||
|
|
||||||
|
const onConfirmDelete = useCallback(() => {
|
||||||
|
dispatch(bulkDeleteDownloadClients({ ids: selectedIds }));
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}, [selectedIds, dispatch]);
|
||||||
|
|
||||||
|
const onSavePress = useCallback(
|
||||||
|
(payload: object) => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
bulkEditDownloadClients({
|
||||||
|
ids: selectedIds,
|
||||||
|
...payload,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectAllChange = useCallback(
|
||||||
|
({ value }: SelectStateInputProps) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
|
||||||
|
({ id, value, shiftKey = false }) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorMessage = getErrorMessage(
|
||||||
|
error,
|
||||||
|
'Unable to load download clients.'
|
||||||
|
);
|
||||||
|
const anySelected = selectedCount > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('ManageDownloadClients')}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length && (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
|
<Table
|
||||||
|
columns={COLUMNS}
|
||||||
|
horizontalScroll={true}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectAllChange={onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<ManageDownloadClientsModalRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
{...item}
|
||||||
|
columns={COLUMNS}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<div className={styles.leftButtons}>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onDeletePress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onEditPress}
|
||||||
|
>
|
||||||
|
{translate('Edit')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<ManageDownloadClientsEditModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
onModalClose={onEditModalClose}
|
||||||
|
onSavePress={onSavePress}
|
||||||
|
downloadClientIds={selectedIds}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteSelectedDownloadClients')}
|
||||||
|
message={translate('DeleteSelectedDownloadClientsMessageText', [
|
||||||
|
selectedIds.length,
|
||||||
|
])}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={onConfirmDelete}
|
||||||
|
onCancel={onDeleteModalClose}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsModalContent;
|
@ -0,0 +1,8 @@
|
|||||||
|
.name,
|
||||||
|
.enable,
|
||||||
|
.priority,
|
||||||
|
.implementation {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'enable': string;
|
||||||
|
'implementation': string;
|
||||||
|
'name': string;
|
||||||
|
'priority': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ManageDownloadClientsModalRow.css';
|
||||||
|
|
||||||
|
interface ManageDownloadClientsModalRowProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
priority: number;
|
||||||
|
implementation: string;
|
||||||
|
columns: Column[];
|
||||||
|
isSelected?: boolean;
|
||||||
|
onSelectedChange(result: SelectStateInputProps): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageDownloadClientsModalRow(
|
||||||
|
props: ManageDownloadClientsModalRowProps
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isSelected,
|
||||||
|
name,
|
||||||
|
enable,
|
||||||
|
priority,
|
||||||
|
implementation,
|
||||||
|
onSelectedChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const onSelectedChangeWrapper = useCallback(
|
||||||
|
(result: SelectStateInputProps) => {
|
||||||
|
onSelectedChange({
|
||||||
|
...result,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onSelectedChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChangeWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.implementation}>
|
||||||
|
{implementation}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.enable}>
|
||||||
|
<Label kind={enable ? kinds.SUCCESS : kinds.DISABLED} outline={!enable}>
|
||||||
|
{enable ? translate('Yes') : translate('No')}
|
||||||
|
</Label>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDownloadClientsModalRow;
|
@ -0,0 +1,54 @@
|
|||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import { set, updateItem } from '../baseActions';
|
||||||
|
|
||||||
|
function createBulkEditItemHandler(section, url) {
|
||||||
|
return function(getState, payload, dispatch) {
|
||||||
|
|
||||||
|
dispatch(set({ section, isSaving: true }));
|
||||||
|
|
||||||
|
const ajaxOptions = {
|
||||||
|
url: `${url}`,
|
||||||
|
method: 'PUT',
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
dataType: 'json'
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = createAjaxRequest(ajaxOptions).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
...data.map((provider) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
...propsToUpdate
|
||||||
|
} = provider;
|
||||||
|
|
||||||
|
return updateItem({
|
||||||
|
id: provider.id,
|
||||||
|
section,
|
||||||
|
...propsToUpdate
|
||||||
|
});
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createBulkEditItemHandler;
|
@ -0,0 +1,48 @@
|
|||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import { removeItem, set } from '../baseActions';
|
||||||
|
|
||||||
|
function createBulkRemoveItemHandler(section, url) {
|
||||||
|
return function(getState, payload, dispatch) {
|
||||||
|
const {
|
||||||
|
ids
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
dispatch(set({ section, isDeleting: true }));
|
||||||
|
|
||||||
|
const ajaxOptions = {
|
||||||
|
url: `${url}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
dataType: 'json'
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = createAjaxRequest(ajaxOptions).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
...ids.map((id) => {
|
||||||
|
return removeItem({ section, id });
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createBulkRemoveItemHandler;
|
@ -0,0 +1,19 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export enum ApplicationSyncLevel {
|
||||||
|
Disabled = 'disabled',
|
||||||
|
AddOnly = 'addOnly',
|
||||||
|
FullSync = 'fullSync',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Application extends ModelBase {
|
||||||
|
name: string;
|
||||||
|
syncLevel: ApplicationSyncLevel;
|
||||||
|
implementationName: string;
|
||||||
|
implementation: string;
|
||||||
|
configContract: string;
|
||||||
|
infoLink: string;
|
||||||
|
tags: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Application;
|
@ -0,0 +1,26 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export interface Field {
|
||||||
|
order: number;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
value: boolean | number | string;
|
||||||
|
type: string;
|
||||||
|
advanced: boolean;
|
||||||
|
privacy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadClient extends ModelBase {
|
||||||
|
enable: boolean;
|
||||||
|
protocol: string;
|
||||||
|
priority: number;
|
||||||
|
name: string;
|
||||||
|
fields: Field[];
|
||||||
|
implementationName: string;
|
||||||
|
implementation: string;
|
||||||
|
configContract: string;
|
||||||
|
infoLink: string;
|
||||||
|
tags: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadClient;
|
@ -0,0 +1,27 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export interface Field {
|
||||||
|
order: number;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
value: boolean | number | string;
|
||||||
|
type: string;
|
||||||
|
advanced: boolean;
|
||||||
|
privacy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Indexer extends ModelBase {
|
||||||
|
enable: boolean;
|
||||||
|
appProfileId: number;
|
||||||
|
protocol: string;
|
||||||
|
priority: number;
|
||||||
|
name: string;
|
||||||
|
fields: Field[];
|
||||||
|
implementationName: string;
|
||||||
|
implementation: string;
|
||||||
|
configContract: string;
|
||||||
|
infoLink: string;
|
||||||
|
tags: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Indexer;
|
@ -0,0 +1,24 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export interface Field {
|
||||||
|
order: number;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
value: boolean | number | string;
|
||||||
|
type: string;
|
||||||
|
advanced: boolean;
|
||||||
|
privacy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Notification extends ModelBase {
|
||||||
|
enable: boolean;
|
||||||
|
name: string;
|
||||||
|
fields: Field[];
|
||||||
|
implementationName: string;
|
||||||
|
implementation: string;
|
||||||
|
configContract: string;
|
||||||
|
infoLink: string;
|
||||||
|
tags: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification;
|
@ -0,0 +1,6 @@
|
|||||||
|
export interface UiSettings {
|
||||||
|
showRelativeDates: boolean;
|
||||||
|
shortDateFormat: string;
|
||||||
|
longDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export interface SelectStateInputProps {
|
||||||
|
id: number;
|
||||||
|
value: boolean;
|
||||||
|
shiftKey: boolean;
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Applications;
|
||||||
|
|
||||||
|
namespace Prowlarr.Api.V1.Applications
|
||||||
|
{
|
||||||
|
public class ApplicationBulkResource : ProviderBulkResource<ApplicationBulkResource>
|
||||||
|
{
|
||||||
|
public ApplicationSyncLevel? SyncLevel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationBulkResourceMapper : ProviderBulkResourceMapper<ApplicationBulkResource, ApplicationDefinition>
|
||||||
|
{
|
||||||
|
public override List<ApplicationDefinition> UpdateModel(ApplicationBulkResource resource, List<ApplicationDefinition> existingDefinitions)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return new List<ApplicationDefinition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
existingDefinitions.ForEach(existing =>
|
||||||
|
{
|
||||||
|
existing.SyncLevel = resource.SyncLevel ?? existing.SyncLevel;
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingDefinitions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
|
||||||
|
namespace Prowlarr.Api.V1.DownloadClient
|
||||||
|
{
|
||||||
|
public class DownloadClientBulkResource : ProviderBulkResource<DownloadClientBulkResource>
|
||||||
|
{
|
||||||
|
public bool? Enable { get; set; }
|
||||||
|
public int? Priority { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DownloadClientBulkResourceMapper : ProviderBulkResourceMapper<DownloadClientBulkResource, DownloadClientDefinition>
|
||||||
|
{
|
||||||
|
public override List<DownloadClientDefinition> UpdateModel(DownloadClientBulkResource resource, List<DownloadClientDefinition> existingDefinitions)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return new List<DownloadClientDefinition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
existingDefinitions.ForEach(existing =>
|
||||||
|
{
|
||||||
|
existing.Enable = resource.Enable ?? existing.Enable;
|
||||||
|
existing.Priority = resource.Priority ?? existing.Priority;
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingDefinitions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using NzbDrone.Core.IndexerProxies;
|
||||||
|
|
||||||
|
namespace Prowlarr.Api.V1.IndexerProxies
|
||||||
|
{
|
||||||
|
public class IndexerProxyBulkResource : ProviderBulkResource<IndexerProxyBulkResource>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexerProxyBulkResourceMapper : ProviderBulkResourceMapper<IndexerProxyBulkResource, IndexerProxyDefinition>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.IndexerProxies;
|
using NzbDrone.Core.IndexerProxies;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.IndexerProxies
|
namespace Prowlarr.Api.V1.IndexerProxies
|
||||||
{
|
{
|
||||||
[V1ApiController]
|
[V1ApiController]
|
||||||
public class IndexerProxyController : ProviderControllerBase<IndexerProxyResource, IIndexerProxy, IndexerProxyDefinition>
|
public class IndexerProxyController : ProviderControllerBase<IndexerProxyResource, IndexerProxyBulkResource, IIndexerProxy, IndexerProxyDefinition>
|
||||||
{
|
{
|
||||||
public static readonly IndexerProxyResourceMapper ResourceMapper = new IndexerProxyResourceMapper();
|
public static readonly IndexerProxyResourceMapper ResourceMapper = new ();
|
||||||
|
public static readonly IndexerProxyBulkResourceMapper BulkResourceMapper = new ();
|
||||||
|
|
||||||
public IndexerProxyController(IndexerProxyFactory notificationFactory)
|
public IndexerProxyController(IndexerProxyFactory notificationFactory)
|
||||||
: base(notificationFactory, "indexerProxy", ResourceMapper)
|
: base(notificationFactory, "indexerProxy", ResourceMapper, BulkResourceMapper)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public override ActionResult<IndexerProxyResource> UpdateProvider([FromBody] IndexerProxyBulkResource providerResource)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public override object DeleteProviders([FromBody] IndexerProxyBulkResource resource)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
|
||||||
|
namespace Prowlarr.Api.V1.Indexers
|
||||||
|
{
|
||||||
|
public class IndexerBulkResource : ProviderBulkResource<IndexerBulkResource>
|
||||||
|
{
|
||||||
|
public bool? Enable { get; set; }
|
||||||
|
public int? AppProfileId { get; set; }
|
||||||
|
public int? Priority { get; set; }
|
||||||
|
public int? MinimumSeeders { get; set; }
|
||||||
|
public double? SeedRatio { get; set; }
|
||||||
|
public int? SeedTime { get; set; }
|
||||||
|
public int? PackSeedTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexerBulkResourceMapper : ProviderBulkResourceMapper<IndexerBulkResource, IndexerDefinition>
|
||||||
|
{
|
||||||
|
public override List<IndexerDefinition> UpdateModel(IndexerBulkResource resource, List<IndexerDefinition> existingDefinitions)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return new List<IndexerDefinition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
existingDefinitions.ForEach(existing =>
|
||||||
|
{
|
||||||
|
existing.Enable = resource.Enable ?? existing.Enable;
|
||||||
|
existing.AppProfileId = resource.AppProfileId ?? existing.AppProfileId;
|
||||||
|
existing.Priority = resource.Priority ?? existing.Priority;
|
||||||
|
|
||||||
|
if (existing.Protocol == DownloadProtocol.Torrent)
|
||||||
|
{
|
||||||
|
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.AppMinimumSeeders = resource.MinimumSeeders ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.AppMinimumSeeders;
|
||||||
|
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio = resource.SeedRatio ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio;
|
||||||
|
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime = resource.SeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime;
|
||||||
|
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime = resource.PackSeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingDefinitions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,88 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using NzbDrone.Core.Indexers;
|
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
|
||||||
using Prowlarr.Http;
|
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Indexers
|
|
||||||
{
|
|
||||||
[V1ApiController("indexer/editor")]
|
|
||||||
public class IndexerEditorController : Controller
|
|
||||||
{
|
|
||||||
private readonly IIndexerFactory _indexerFactory;
|
|
||||||
private readonly IManageCommandQueue _commandQueueManager;
|
|
||||||
private readonly IndexerResourceMapper _resourceMapper;
|
|
||||||
|
|
||||||
public IndexerEditorController(IIndexerFactory indexerFactory, IManageCommandQueue commandQueueManager, IndexerResourceMapper resourceMapper)
|
|
||||||
{
|
|
||||||
_indexerFactory = indexerFactory;
|
|
||||||
_commandQueueManager = commandQueueManager;
|
|
||||||
_resourceMapper = resourceMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut]
|
|
||||||
[Consumes("application/json")]
|
|
||||||
public IActionResult SaveAll(IndexerEditorResource resource)
|
|
||||||
{
|
|
||||||
var indexersToUpdate = _indexerFactory.AllProviders(false).Select(x => (IndexerDefinition)x.Definition).Where(d => resource.IndexerIds.Contains(d.Id));
|
|
||||||
|
|
||||||
foreach (var indexer in indexersToUpdate)
|
|
||||||
{
|
|
||||||
if (resource.Enable.HasValue)
|
|
||||||
{
|
|
||||||
indexer.Enable = resource.Enable.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resource.AppProfileId.HasValue)
|
|
||||||
{
|
|
||||||
indexer.AppProfileId = resource.AppProfileId.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resource.Priority.HasValue)
|
|
||||||
{
|
|
||||||
indexer.Priority = resource.Priority.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resource.Tags != null)
|
|
||||||
{
|
|
||||||
var newTags = resource.Tags;
|
|
||||||
var applyTags = resource.ApplyTags;
|
|
||||||
|
|
||||||
switch (applyTags)
|
|
||||||
{
|
|
||||||
case ApplyTags.Add:
|
|
||||||
newTags.ForEach(t => indexer.Tags.Add(t));
|
|
||||||
break;
|
|
||||||
case ApplyTags.Remove:
|
|
||||||
newTags.ForEach(t => indexer.Tags.Remove(t));
|
|
||||||
break;
|
|
||||||
case ApplyTags.Replace:
|
|
||||||
indexer.Tags = new HashSet<int>(newTags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_indexerFactory.Update(indexersToUpdate);
|
|
||||||
|
|
||||||
var indexers = _indexerFactory.All();
|
|
||||||
|
|
||||||
foreach (var definition in indexers)
|
|
||||||
{
|
|
||||||
_indexerFactory.SetProviderCharacteristics(definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Accepted(_resourceMapper.ToResource(indexers));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete]
|
|
||||||
[Consumes("application/json")]
|
|
||||||
public object DeleteIndexers([FromBody] IndexerEditorResource resource)
|
|
||||||
{
|
|
||||||
_indexerFactory.Delete(resource.IndexerIds);
|
|
||||||
|
|
||||||
return new { };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Indexers
|
|
||||||
{
|
|
||||||
public class IndexerEditorResource
|
|
||||||
{
|
|
||||||
public List<int> IndexerIds { get; set; }
|
|
||||||
public bool? Enable { get; set; }
|
|
||||||
public int? AppProfileId { get; set; }
|
|
||||||
public int? Priority { get; set; }
|
|
||||||
public List<int> Tags { get; set; }
|
|
||||||
public ApplyTags ApplyTags { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ApplyTags
|
|
||||||
{
|
|
||||||
Add,
|
|
||||||
Remove,
|
|
||||||
Replace
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,12 @@
|
|||||||
|
using NzbDrone.Core.Notifications;
|
||||||
|
|
||||||
|
namespace Prowlarr.Api.V1.Notifications
|
||||||
|
{
|
||||||
|
public class NotificationBulkResource : ProviderBulkResource<NotificationBulkResource>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationBulkResourceMapper : ProviderBulkResourceMapper<NotificationBulkResource, NotificationDefinition>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.Notifications;
|
using NzbDrone.Core.Notifications;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Notifications
|
namespace Prowlarr.Api.V1.Notifications
|
||||||
{
|
{
|
||||||
[V1ApiController]
|
[V1ApiController]
|
||||||
public class NotificationController : ProviderControllerBase<NotificationResource, INotification, NotificationDefinition>
|
public class NotificationController : ProviderControllerBase<NotificationResource, NotificationBulkResource, INotification, NotificationDefinition>
|
||||||
{
|
{
|
||||||
public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper();
|
public static readonly NotificationResourceMapper ResourceMapper = new ();
|
||||||
|
public static readonly NotificationBulkResourceMapper BulkResourceMapper = new ();
|
||||||
|
|
||||||
public NotificationController(NotificationFactory notificationFactory)
|
public NotificationController(NotificationFactory notificationFactory)
|
||||||
: base(notificationFactory, "notification", ResourceMapper)
|
: base(notificationFactory, "notification", ResourceMapper, BulkResourceMapper)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public override ActionResult<NotificationResource> UpdateProvider([FromBody] NotificationBulkResource providerResource)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
public override object DeleteProviders([FromBody] NotificationBulkResource resource)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace Prowlarr.Api.V1
|
||||||
|
{
|
||||||
|
public class ProviderBulkResource<T>
|
||||||
|
{
|
||||||
|
public List<int> Ids { get; set; }
|
||||||
|
public List<int> Tags { get; set; }
|
||||||
|
public ApplyTags ApplyTags { get; set; }
|
||||||
|
|
||||||
|
public ProviderBulkResource()
|
||||||
|
{
|
||||||
|
Ids = new List<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ApplyTags
|
||||||
|
{
|
||||||
|
Add,
|
||||||
|
Remove,
|
||||||
|
Replace
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProviderBulkResourceMapper<TProviderBulkResource, TProviderDefinition>
|
||||||
|
where TProviderBulkResource : ProviderBulkResource<TProviderBulkResource>, new()
|
||||||
|
where TProviderDefinition : ProviderDefinition, new()
|
||||||
|
{
|
||||||
|
public virtual List<TProviderDefinition> UpdateModel(TProviderBulkResource resource, List<TProviderDefinition> existingDefinitions)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return new List<TProviderDefinition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingDefinitions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue