Viewing File: /home/markqprx/iniasli.pro/client/auth/ui/permission-selector.tsx

import {useControlledState} from '@react-stately/utils';
import React, {Fragment, useState} from 'react';
import {useController} from 'react-hook-form';
import {mergeProps} from '@react-aria/utils';
import clsx from 'clsx';
import {produce} from 'immer';
import {Permission, PermissionRestriction} from '../permission';
import {useValueLists} from '../../http/value-lists';
import {ucFirst} from '../../utils/string/uc-first';
import {Accordion, AccordionItem} from '../../ui/accordion/accordion';
import {List, ListItem} from '../../ui/list/list';
import {Switch} from '../../ui/forms/toggle/switch';
import {TextField} from '../../ui/forms/input-field/text-field/text-field';
import {DoneAllIcon} from '../../icons/material/DoneAll';
import {Trans} from '../../i18n/trans';

interface PermissionSelectorProps {
  value?: Permission[];
  onChange?: (value: Permission[]) => void;
  valueListKey?: 'permissions' | 'workspacePermissions';
}
export const PermissionSelector = React.forwardRef<
  HTMLDivElement,
  PermissionSelectorProps
>(({valueListKey = 'permissions', ...props}, ref) => {
  const {data} = useValueLists([valueListKey]);
  const permissions = data?.permissions || data?.workspacePermissions;

  const [value, setValue] = useControlledState(props.value, [], props.onChange);
  const [showAdvanced, setShowAdvanced] = useState(false);

  if (!permissions) return null;

  const groupedPermissions = buildPermissionList(
    permissions,
    value,
    showAdvanced
  );

  const onRestrictionChange = (newPermission: Permission) => {
    const newValue = [...value];
    const index = newValue.findIndex(p => p.id === newPermission.id);
    if (index > -1) {
      newValue.splice(index, 1, newPermission);
    }
    setValue(newValue);
  };

  return (
    <Fragment>
      <Accordion variant="outline" ref={ref}>
        {groupedPermissions.map(({groupName, items, anyChecked}) => (
          <AccordionItem
            label={<Trans message={prettyName(groupName)} />}
            key={groupName}
            startIcon={anyChecked ? <DoneAllIcon size="sm" /> : undefined}
          >
            <List>
              {items.map(permission => {
                const index = value.findIndex(v => v.id === permission.id);
                const isChecked = index > -1;

                return (
                  <div key={permission.id}>
                    <ListItem
                      onSelected={() => {
                        if (isChecked) {
                          const newValue = [...value];
                          newValue.splice(index, 1);
                          setValue(newValue);
                        } else {
                          setValue([...value, permission]);
                        }
                      }}
                      endSection={
                        <Switch
                          tabIndex={-1}
                          checked={isChecked}
                          onChange={() => {}}
                        />
                      }
                      description={<Trans message={permission.description} />}
                    >
                      <Trans
                        message={permission.display_name || permission.name}
                      />
                    </ListItem>
                    {isChecked && (
                      <Restrictions
                        permission={permission}
                        onChange={onRestrictionChange}
                      />
                    )}
                  </div>
                );
              })}
            </List>
          </AccordionItem>
        ))}
      </Accordion>
      <Switch
        className="mt-30"
        checked={showAdvanced}
        onChange={e => {
          setShowAdvanced(e.target.checked);
        }}
      >
        <Trans message="Show advanced permissions" />
      </Switch>
    </Fragment>
  );
});

interface RestrictionsProps {
  permission: Permission;
  onChange?: (newPermission: Permission) => void;
}
function Restrictions({permission, onChange}: RestrictionsProps) {
  if (!permission?.restrictions?.length) return null;

  const setRestrictionValue = (
    name: string,
    value: PermissionRestriction['value']
  ) => {
    const nextState = produce(permission, draftState => {
      const restriction = draftState.restrictions.find(r => r.name === name);
      if (restriction) {
        restriction.value = value;
      }
    });
    onChange?.(nextState);
  };

  return (
    <div className="px-40 py-20">
      {permission.restrictions.map((restriction, index) => {
        const isLast = index === permission.restrictions.length - 1;

        const name = <Trans message={prettyName(restriction.name)} />;
        const description = restriction.description ? (
          <Trans message={restriction.description} />
        ) : undefined;

        if (restriction.type === 'bool') {
          return (
            <Switch
              description={description}
              key={restriction.name}
              className={clsx(!isLast && 'mb-30')}
              checked={Boolean(restriction.value)}
              onChange={e => {
                setRestrictionValue(restriction.name, e.target.checked);
              }}
            >
              {name}
            </Switch>
          );
        }

        return (
          <TextField
            size="sm"
            label={name}
            description={description}
            type="number"
            key={restriction.name}
            className={clsx(!isLast && 'mb-30')}
            value={(restriction.value as string) || ''}
            onChange={e => {
              setRestrictionValue(
                restriction.name,
                e.target.value === '' ? undefined : parseInt(e.target.value)
              );
            }}
          />
        );
      })}
    </div>
  );
}

export type FormChipFieldProps = PermissionSelectorProps & {
  name: string;
};
export function FormPermissionSelector(props: FormChipFieldProps) {
  const {
    field: {onChange, value = [], ref},
  } = useController({
    name: props.name,
  });

  const formProps: Partial<PermissionSelectorProps> = {
    onChange,
    value,
  };

  return <PermissionSelector ref={ref} {...mergeProps(formProps, props)} />;
}

export const prettyName = (name: string) => {
  return ucFirst(name.replace('_', ' '));
};

interface PermissionGroup {
  groupName: string;
  anyChecked: boolean;
  items: Permission[];
}

// merge "restrictions" from selected value into all permissions to make
// it easier to bind restriction values to form inputs
export function buildPermissionList(
  allPermissions: Permission[],
  selectedPermissions: Permission[],
  showAdvanced: boolean
) {
  const groupedPermissions: PermissionGroup[] = [];

  allPermissions.forEach(permission => {
    const index = selectedPermissions.findIndex(p => p.id === permission.id);
    if (!showAdvanced && permission.advanced) return;

    let group: PermissionGroup | undefined = groupedPermissions.find(
      g => g.groupName === permission.group
    );
    if (!group) {
      group = {groupName: permission.group, anyChecked: false, items: []};
      groupedPermissions.push(group);
    }

    if (index > -1) {
      const mergedPermission = {
        ...permission,
        restrictions: mergeRestrictions(
          permission.restrictions,
          selectedPermissions[index].restrictions
        ),
      };
      group.anyChecked = true;
      group.items.push(mergedPermission);
    } else {
      group.items.push(permission);
    }
  });

  return groupedPermissions;
}

function mergeRestrictions(
  allRestrictions: PermissionRestriction[],
  selectedRestrictions: PermissionRestriction[]
): PermissionRestriction[] {
  return allRestrictions?.map(restriction => {
    const selected = selectedRestrictions.find(
      r => r.name === restriction.name
    );
    if (selected) {
      return {...restriction, value: selected.value};
    } else {
      return restriction;
    }
  });
}
Back to Directory File Manager