From f9982a5fb3c3146b8ad2b5407da9724275db333a Mon Sep 17 00:00:00 2001 From: Auggie Date: Sat, 28 Dec 2024 18:43:20 +0000 Subject: [PATCH] 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;