Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add configurable auto-approval #1190

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Initial Auto-Approval concept
  • Loading branch information
augustuen committed Dec 29, 2024
commit f9982a5fb3c3146b8ad2b5407da9724275db333a
10 changes: 10 additions & 0 deletions server/lib/autoapproval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface AutoApprovalRule {
name: string;
conditions: AutoApprovalCondition[];
}

export interface AutoApprovalCondition {
implementation: string;
comparisonType: string;
value;
}
177 changes: 177 additions & 0 deletions src/components/ApprovalRuleList/RuleModal/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<Table.TD>
<select
id="condition-type"
name="condition-type"
className=""
defaultValue={comparison}
>
<option value="is">is</option>
<option value="isnot">is not</option>
<option value="contains">contains</option>
<option value="containsnot">does not contain</option>
</select>
</Table.TD>
<Table.TD>
<Select<OptionType, true>
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)
)}
/>
</Table.TD>
</div>
);
};

const ReleaseYearCondition = (comparison = 'equals') => {
const options = [
{ value: 'equals', text: '=' },
{ value: 'greaterthan', text: '>' },
{ value: 'lessthan', text: '<' },
];
const optionsContent = options.map((option) => (
<option key={`condition-release-`} value={option.value}>
{option.text}
</option>
));
return (
<select
key="mykey"
id="comparison-type"
name="comparison-type"
className="rounded-r-only"
defaultValue={comparison}
>
{optionsContent}
</select>
);
};

const ConditionItem = (
defaultImplementation: string,
comparison = '',
values: any
) => {
const [implementation, setImplementation] = useState(defaultImplementation);
return (
<tr
key="approval-rule-condition-0"
data-testid="approval-condition-list-row"
>
<Table.TD>
<select
id="implementation"
name="implementation"
value={implementation}
onChange={(e) => setImplementation(e.target.value)}
>
<option value="genre">Genre</option>
<option value="release-year">Release Year</option>
</select>
</Table.TD>
<Table.TD>
{
{
genre: GenreCondition(comparison, values),
'release-year': ReleaseYearCondition(comparison),
}[implementation]
}
</Table.TD>
</tr>
);
};

const RuleModal = ({ onClose, approvalRule }: RuleModalProps) => {
const conditionsList = approvalRule?.conditions.map((condition) =>
ConditionItem(
condition.implementation,
condition.comparisonType,
condition.value
)
);
return (
<Transition
as="div"
appear
show
enter="transition-opacity ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Modal
onCancel={onClose}
okButtonType="primary"
okText="Add"
title="Add Auto Approval Rule"
>
<div className="mb-6">
<h2>This is a modal</h2>
</div>
<Table>
<thead>
<tr>
<Table.TH>Condition</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{conditionsList}
<tr className="rounded-lg border-2 border-dashed border-gray-400 shadow">
<Table.TD
colSpan={3}
className="rounded-r-only border-2 border-dashed border-gray-400"
>
<div className="flex w-screen flex-col items-center sm:space-y-0 lg:w-full">
<Button
buttonType="ghost"
className="lg:justify-y-center flex justify-center"
>
<PlusIcon />
<span> Add Condition</span>
</Button>
</div>
</Table.TD>
</tr>
</Table.TBody>
</Table>
</Modal>
</Transition>
);
};

export default RuleModal;
203 changes: 203 additions & 0 deletions src/components/ApprovalRuleList/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>([
['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) => (
<Badge key={`auto-approval-badge-`} className="m-0.5">
{condition.implementation} {comparisonNames.get(condition.comparisonType)}{' '}
{condition.value > 1000 ? condition.value : valueNames[condition.value]}
</Badge>
));

return (
<li className="col-span-1 rounded-lg bg-gray-800 shadow ring-1 ring-gray-500">
<div className="flex w-full items-center justify-between space-x-6 p-6">
<div className="flex-1 truncate">
<div className="mb-2 flex items-center space-x-2">
<h3 className="truncate font-medium leading-5 text-white">
<a
href=""
className="transition duration-300 hover:text-white hover:underline"
>
{name}
</a>
</h3>
</div>
<p className="mt-1 flex flex-wrap text-sm leading-5 text-gray-300">
{conditionBadges}
</p>
</div>
</div>
<div className="border-t border-gray-500">
<div className="-mt-px flex">
<div className="flex w-0 flex-1 border-r border-gray-500">
<button
onClick={() => onEdit()}
className="focus:ring-blue relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
>
<PencilIcon className="mr-2 h-5 w-5" />
<span>Edit</span>
</button>
</div>
<div className="-ml-px flex w-0 flex-1">
<button className="focus:ring-blue relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none">
<TrashIcon className="mr-2 h-5 w-5" />
<span>Delete</span>
</button>
</div>
</div>
</div>
</li>
);
};

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 (
<div className="mt-6">
<div className="mt-10 text-white">
<div className="flex flex-col justify-between lg:flex-row lg:items-end">
<Header>Auto Approval Rules</Header>
</div>
{editRuleModal.open && (
<RuleModal
approvalRule={editRuleModal.approvalRule}
onClose={() =>
setEditRuleModal({ open: false, approvalRule: null })
}
onSave={() => {
setEditRuleModal({ open: false, approvalRule: null });
}}
/>
)}
<h3 className="heading">Movie Auto-approval rules</h3>
<div className="section">
<ul className="xl:grid-cols3 grid max-w-4xl grid-cols-1 gap-6 lg:grid-cols-2">
{movieRuleData.map((rule) => (
<ApprovalRuleInstance
key={`approval-rule-`}
name={rule.name}
onEdit={() =>
setEditRuleModal({
open: true,
approvalRule: rule.currentRule,
})
}
currentRule={{
name: rule.name,
conditions: rule.currentRule.conditions,
}}
/>
))}
<li className="col-span-1 h-32 rounded-lg border-2 border-dashed border-gray-400 shadow sm:h-44">
<div className="flex h-full w-full items-center justify-center">
<Button
buttonType="ghost"
className="mt-3 mb-3"
onClick={() =>
setEditRuleModal({
open: true,
approvalRule: {
name: 'Test rule',
conditions: [
{
implementation: 'genre',
comparisonType: 'is',
value: 4,
},
{
implementation: 'release-year',
comparisonType: 'lessthan',
value: 2,
},
],
},
})
}
>
<PlusIcon />
<span>Add Rule</span>
</Button>
</div>
</li>
</ul>
</div>
<h3 className="heading">Series Auto-approval rules</h3>
<div className="section">
<ul className="xl:grid-cols3 grid max-w-6xl grid-cols-1 gap-6 lg:grid-cols-2">
<li className="col-span-1 h-32 rounded-lg border-2 border-dashed border-gray-400 shadow sm:h-44">
<div className="flex h-full w-full items-center justify-center">
<Button buttonType="ghost" className="mt-3 mb-3">
<PlusIcon />
<span>Add Rule</span>
</Button>
</div>
</li>
</ul>
</div>
</div>
</div>
);
};

export default AutoApprovalList;
Loading