From f9982a5fb3c3146b8ad2b5407da9724275db333a Mon Sep 17 00:00:00 2001 From: Auggie Date: Sat, 28 Dec 2024 18:43:20 +0000 Subject: [PATCH 1/2] feat: Initial Auto-Approval concept --- server/lib/autoapproval.ts | 10 + .../ApprovalRuleList/RuleModal/index.tsx | 177 +++++++++++++++ src/components/ApprovalRuleList/index.tsx | 203 ++++++++++++++++++ src/components/Layout/Sidebar/index.tsx | 9 + src/pages/approval/index.tsx | 8 + 5 files changed, 407 insertions(+) create mode 100644 server/lib/autoapproval.ts create mode 100644 src/components/ApprovalRuleList/RuleModal/index.tsx create mode 100644 src/components/ApprovalRuleList/index.tsx create mode 100644 src/pages/approval/index.tsx diff --git a/server/lib/autoapproval.ts b/server/lib/autoapproval.ts new file mode 100644 index 000000000..dfa4aba14 --- /dev/null +++ b/server/lib/autoapproval.ts @@ -0,0 +1,10 @@ +export interface AutoApprovalRule { + name: string; + conditions: AutoApprovalCondition[]; +} + +export interface AutoApprovalCondition { + implementation: string; + comparisonType: string; + value; +} diff --git a/src/components/ApprovalRuleList/RuleModal/index.tsx b/src/components/ApprovalRuleList/RuleModal/index.tsx new file mode 100644 index 000000000..4dfb361ac --- /dev/null +++ b/src/components/ApprovalRuleList/RuleModal/index.tsx @@ -0,0 +1,177 @@ +import Button from '@app/components/Common/Button'; +import Modal from '@app/components/Common/Modal'; +import Table from '@app/components/Common/Table'; +import { Transition } from '@headlessui/react'; +import { PlusIcon } from '@heroicons/react/24/solid'; +import type { AutoApprovalRule } from '@server/lib/autoapproval'; +import { useState } from 'react'; +import Select from 'react-select'; + +type OptionType = { value: number; label: string; exists: boolean }; + +interface RuleModalProps { + approvalRule: AutoApprovalRule | null; + onClose: () => void; + onSave: () => void; +} + +const GenreCondition = (comparison = 'is', values) => { + const genreOptions = [ + { label: 'Action', value: 0 }, + { label: 'Comedy', value: 1 }, + { label: 'Documentary', value: 2 }, + { label: 'Romance', value: 3 }, + { label: 'Drama', value: 4 }, + ]; + return ( +
+ + + + + + options={genreOptions} + isMulti + className="react-select-container rounded-r-only" + classNamePrefix="react-select" + defaultValue={genreOptions.filter((genre) => + typeof values == 'number' + ? genre.value == values + : values.includes(genre.value) + )} + /> + +
+ ); +}; + +const ReleaseYearCondition = (comparison = 'equals') => { + const options = [ + { value: 'equals', text: '=' }, + { value: 'greaterthan', text: '>' }, + { value: 'lessthan', text: '<' }, + ]; + const optionsContent = options.map((option) => ( + + )); + return ( + + ); +}; + +const ConditionItem = ( + defaultImplementation: string, + comparison = '', + values: any +) => { + const [implementation, setImplementation] = useState(defaultImplementation); + return ( + + + + + + { + { + genre: GenreCondition(comparison, values), + 'release-year': ReleaseYearCondition(comparison), + }[implementation] + } + + + ); +}; + +const RuleModal = ({ onClose, approvalRule }: RuleModalProps) => { + const conditionsList = approvalRule?.conditions.map((condition) => + ConditionItem( + condition.implementation, + condition.comparisonType, + condition.value + ) + ); + return ( + + +
+

This is a modal

+
+ + + + Condition + + + + + {conditionsList} + + +
+ +
+
+ + +
+
+
+ ); +}; + +export default RuleModal; diff --git a/src/components/ApprovalRuleList/index.tsx b/src/components/ApprovalRuleList/index.tsx new file mode 100644 index 000000000..1f0a8b483 --- /dev/null +++ b/src/components/ApprovalRuleList/index.tsx @@ -0,0 +1,203 @@ +import RuleModal from '@app/components/ApprovalRuleList/RuleModal'; +import Badge from '@app/components/Common/Badge'; +import Button from '@app/components/Common/Button'; +import Header from '@app/components/Common/Header'; +import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid'; +import type { AutoApprovalRule } from '@server/lib/autoapproval'; +import { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +export const messages = defineMessages('components.ApprovalRuleList',{ + autoapprovalrules: 'Auto Approval Rules', + addRule: 'Add Approval Rule', +}); + +interface ApprovalRuleInstanceProps { + name: string; + currentRule: AutoApprovalRule; + onEdit: () => void; +} + +const ApprovalRuleInstance = ({ + name, + currentRule, + onEdit, +}: ApprovalRuleInstanceProps) => { + const comparisonNames = new Map([ + ['equals', '='], + ['greaterthan', '>'], + ['lessthan', '<'], + ['is', 'is'], + ['isnot', 'is not'], + ['contains', 'contains'], + ['does not contain'], + ]); + const valueNames: string[] = ['Action', 'Comedy', 'Documentary', 'Romance']; + const conditionBadges = currentRule.conditions.map((condition) => ( + + {condition.implementation} {comparisonNames.get(condition.comparisonType)}{' '} + {condition.value > 1000 ? condition.value : valueNames[condition.value]} + + )); + + return ( +
  • +
    +
    +
    +

    + + {name} + +

    +
    +

    + {conditionBadges} +

    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
  • + ); +}; + +const AutoApprovalList = () => { + const intl = useIntl(); + const movieRuleData = [ + { + name: 'Test Rule', + currentRule: { + name: 'Test Rule', + conditions: [ + { implementation: 'genre', comparisonType: 'is', value: 0 }, + { + implementation: 'release-year', + comparisonType: 'lessthan', + value: 2020, + }, + { implementation: 'genre', comparisonType: 'isnot', value: 2 }, + { + implementation: 'genre', + comparisonType: 'contains', + value: [1, 4], + }, + ], + }, + }, + ]; + const [editRuleModal, setEditRuleModal] = useState<{ + open: boolean; + approvalRule: AutoApprovalRule | null; + }>({ + open: false, + approvalRule: null, + }); + return ( +
    +
    +
    +
    Auto Approval Rules
    +
    + {editRuleModal.open && ( + + setEditRuleModal({ open: false, approvalRule: null }) + } + onSave={() => { + setEditRuleModal({ open: false, approvalRule: null }); + }} + /> + )} +

    Movie Auto-approval rules

    +
    +
      + {movieRuleData.map((rule) => ( + + setEditRuleModal({ + open: true, + approvalRule: rule.currentRule, + }) + } + currentRule={{ + name: rule.name, + conditions: rule.currentRule.conditions, + }} + /> + ))} +
    • +
      + +
      +
    • +
    +
    +

    Series Auto-approval rules

    +
    +
      +
    • +
      + +
      +
    • +
    +
    +
    +
    + ); +}; + +export default AutoApprovalList; diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index a947e2626..9985f18cb 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -29,6 +29,7 @@ export const menuMessages = defineMessages('components.Layout.Sidebar', { blacklist: 'Blacklist', issues: 'Issues', users: 'Users', + autoapproval: 'Auto Approval', settings: 'Settings', }); @@ -104,6 +105,14 @@ const SidebarLinks: SidebarLinkProps[] = [ requiredPermission: Permission.MANAGE_USERS, dataTestId: 'sidebar-menu-users', }, + { + href: '/approval', + messagesKey: 'autoapproval', + svgIcon: , + activeRegExp: /^\/approval/, + requiredPermission: Permission.MANAGE_USERS, + dataTestId: 'sidebar-menu-auto-approval', + }, { href: '/settings', messagesKey: 'settings', diff --git a/src/pages/approval/index.tsx b/src/pages/approval/index.tsx new file mode 100644 index 000000000..52ade3557 --- /dev/null +++ b/src/pages/approval/index.tsx @@ -0,0 +1,8 @@ +import ApprovalRuleList from '@app/components/ApprovalRuleList'; +import type { NextPage } from 'next'; + +const ApprovalRulePage: NextPage = () => { + return ; +}; + +export default ApprovalRulePage; From b2351d5aa1cd3a337454cb37ea94626b7722fe3f Mon Sep 17 00:00:00 2001 From: Auggie Date: Sat, 28 Dec 2024 22:44:14 +0000 Subject: [PATCH 2/2] feat: Move auto-approval config page into Settings --- src/components/Layout/Sidebar/index.tsx | 8 -------- .../ApprovalRuleList/RuleModal/index.tsx | 0 .../SettingsAutoApproval}/index.tsx | 6 +++--- src/components/Settings/SettingsLayout.tsx | 6 ++++++ src/i18n/locale/en.json | 1 + src/pages/approval/index.tsx | 8 -------- src/pages/settings/autoapproval.tsx | 16 ++++++++++++++++ 7 files changed, 26 insertions(+), 19 deletions(-) rename src/components/{ => Settings/SettingsAutoApproval}/ApprovalRuleList/RuleModal/index.tsx (100%) rename src/components/{ApprovalRuleList => Settings/SettingsAutoApproval}/index.tsx (97%) delete mode 100644 src/pages/approval/index.tsx create mode 100644 src/pages/settings/autoapproval.tsx diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index 9985f18cb..a26f3a77d 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -105,14 +105,6 @@ const SidebarLinks: SidebarLinkProps[] = [ requiredPermission: Permission.MANAGE_USERS, dataTestId: 'sidebar-menu-users', }, - { - href: '/approval', - messagesKey: 'autoapproval', - svgIcon: , - activeRegExp: /^\/approval/, - requiredPermission: Permission.MANAGE_USERS, - dataTestId: 'sidebar-menu-auto-approval', - }, { href: '/settings', messagesKey: 'settings', diff --git a/src/components/ApprovalRuleList/RuleModal/index.tsx b/src/components/Settings/SettingsAutoApproval/ApprovalRuleList/RuleModal/index.tsx similarity index 100% rename from src/components/ApprovalRuleList/RuleModal/index.tsx rename to src/components/Settings/SettingsAutoApproval/ApprovalRuleList/RuleModal/index.tsx diff --git a/src/components/ApprovalRuleList/index.tsx b/src/components/Settings/SettingsAutoApproval/index.tsx similarity index 97% rename from src/components/ApprovalRuleList/index.tsx rename to src/components/Settings/SettingsAutoApproval/index.tsx index 1f0a8b483..07d8b724e 100644 --- a/src/components/ApprovalRuleList/index.tsx +++ b/src/components/Settings/SettingsAutoApproval/index.tsx @@ -1,4 +1,4 @@ -import RuleModal from '@app/components/ApprovalRuleList/RuleModal'; +import RuleModal from '@app/components/Settings/SettingsAutoApproval/ApprovalRuleList/RuleModal'; import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import Header from '@app/components/Common/Header'; @@ -82,7 +82,7 @@ const ApprovalRuleInstance = ({ ); }; -const AutoApprovalList = () => { +const SettingsAutoApproval = () => { const intl = useIntl(); const movieRuleData = [ { @@ -200,4 +200,4 @@ const AutoApprovalList = () => { ); }; -export default AutoApprovalList; +export default SettingsAutoApproval; diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index 6336bad01..82bf45247 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -14,6 +14,7 @@ const messages = defineMessages('components.Settings', { menuJellyfinSettings: '{mediaServerName}', menuServices: 'Services', menuNotifications: 'Notifications', + menuAutoApproval: 'Auto Approval', menuLogs: 'Logs', menuJobs: 'Jobs & Cache', menuAbout: 'About', @@ -58,6 +59,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => { route: '/settings/notifications/email', regex: /^\/settings\/notifications/, }, + { + text: intl.formatMessage(messages.menuAutoApproval), + route: '/settings/autoapproval', + regex: /^\/settings\/autoapproval/, + }, { text: intl.formatMessage(messages.menuLogs), route: '/settings/logs', diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 3fce7abd7..ad60d235a 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1064,6 +1064,7 @@ "components.Settings.menuJobs": "Jobs & Cache", "components.Settings.menuLogs": "Logs", "components.Settings.menuNotifications": "Notifications", + "components.Settings.menuAutoApproval": "Auto Approval", "components.Settings.menuPlexSettings": "Plex", "components.Settings.menuServices": "Services", "components.Settings.menuUsers": "Users", diff --git a/src/pages/approval/index.tsx b/src/pages/approval/index.tsx deleted file mode 100644 index 52ade3557..000000000 --- a/src/pages/approval/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import ApprovalRuleList from '@app/components/ApprovalRuleList'; -import type { NextPage } from 'next'; - -const ApprovalRulePage: NextPage = () => { - return ; -}; - -export default ApprovalRulePage; diff --git a/src/pages/settings/autoapproval.tsx b/src/pages/settings/autoapproval.tsx new file mode 100644 index 000000000..00d522f49 --- /dev/null +++ b/src/pages/settings/autoapproval.tsx @@ -0,0 +1,16 @@ +import SettingsLayout from '@app/components/Settings/SettingsLayout'; +import SettingsAutoApproval from '@app/components/Settings/SettingsAutoApproval'; +import useRouteGuard from '@app/hooks/useRouteGuard'; +import {Permission} from '@app/hooks/useUser'; +import type { NextPage } from 'next'; + +const SettingsAutoApprovalPage: NextPage = () => { + useRouteGuard(Permission.ADMIN); + return ( + + + + ) +} + +export default SettingsAutoApprovalPage;