@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
346 lines (344 loc) • 12.5 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
import EditGroupDialogUI from './EditGroupDialogUI';
import { fetchAll } from '../../services/users';
import {
addUsersToGroup,
create,
deleteUsersFromGroup,
fetchUsersFromGroup,
trash,
update
} from '../../services/groups';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { showErrorDialog } from '../../state/reducers/dialogs/error';
import { useDispatch } from 'react-redux';
import { showSystemNotification } from '../../state/actions/system';
import Typography from '@mui/material/Typography';
import { useSpreadState } from '../../hooks/useSpreadState';
import { isInvalidGroupName, validateGroupNameMinLength, validateRequiredField } from '../GroupManagement/utils';
import { excludeCommonItems, useTransferListState } from '../TransferList/utils';
import useMount from '../../hooks/useMount';
import { createPresenceTable } from '../../utils/array';
import { pluckProps, reversePluckProps } from '../../utils/object';
import useUpdateRefs from '../../hooks/useUpdateRefs';
const translations = defineMessages({
groupCreated: {
id: 'groupEditDialog.groupCreated',
defaultMessage: 'Group created successfully'
},
groupEdited: {
id: 'groupEditDialog.groupEdited',
defaultMessage: 'Group edited successfully'
},
groupDeleted: {
id: 'groupEditDialog.groupDeleted',
defaultMessage: 'Group deleted successfully'
},
membersAdded: {
id: 'groupEditDialog.membersAdded',
defaultMessage: '{count, plural, one {User added successfully} other {Users added successfully}}'
},
membersRemoved: {
id: 'groupEditDialog.membersRemoved',
defaultMessage: '{count, plural, one {User removed successfully} other {Users removed successfully}}'
}
});
export function EditGroupDialogContainer(props) {
const { onClose, onGroupSaved, onGroupDeleted, isSubmitting, onSubmittingAndOrPendingChange } = props;
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const [group, setGroup] = useSpreadState(props.group ?? { id: null, name: '', desc: '', externallyManaged: false });
const [isDirty, setIsDirty] = useState(false);
const groupNameError =
validateRequiredField(group.name, isDirty) ||
isInvalidGroupName(group.name) ||
validateGroupNameMinLength(group.name);
const isEdit = Boolean(props.group);
// This validation is different than groupName error, because for groupNameError it will return true only when form
// is dirty. For submit, it will be true even though form is not dirty (to avoid submitting a clean form).
const submitOk = Boolean(
group.name.trim() &&
!validateGroupNameMinLength(group.name) &&
!isInvalidGroupName(group.name) &&
(!isEdit || group.desc !== (props.group?.desc ?? ''))
);
const [users, setUsers] = useState();
const [usersHaveNextPage, setUsersHaveNextPage] = useState(false);
const usersRef = useRef([]);
usersRef.current = users;
const [members, setMembers] = useState();
const [membersLookup, setMembersLookup] = useState(null);
const [inProgressIds, setInProgressIds] = useState([]);
const transferListState = useTransferListState();
const { sourceItems, targetItems, setSourceItems, setTargetItems, sourceFilterKeyword, isAllChecked, getChecked } =
transferListState;
const usersFetchSize = 10;
const [usersOffset, setUsersOffset] = useState(0);
const sourceItemsAllChecked = useMemo(
() => isAllChecked(excludeCommonItems(sourceItems, targetItems)),
[isAllChecked, sourceItems, targetItems]
);
const disableAddMembers = getChecked(excludeCommonItems(sourceItems, targetItems)).length === 0;
const fnRefs = useUpdateRefs({ onSubmittingAndOrPendingChange });
const onDeleteGroup = (group) => {
trash(group.id).subscribe({
next() {
dispatch(
showSystemNotification({
message: formatMessage(translations.groupDeleted)
})
);
onGroupDeleted(group);
},
error({ response: { response } }) {
dispatch(showErrorDialog({ error: response }));
}
});
};
const onAddMembers = () => {
const { sourceItems, targetItems, getChecked, setCheckedList } = transferListState;
const users = getChecked(excludeCommonItems(sourceItems, targetItems));
const usernames = users.map((item) => item.id);
if (usernames.length) {
setInProgressIds(usernames);
addUsersToGroup(group.id, usernames).subscribe({
next() {
setCheckedList({});
setTargetItems([...targetItems, ...users]);
setMembersLookup({
...membersLookup,
...createPresenceTable(usernames, true)
});
dispatch(
showSystemNotification({
message: formatMessage(translations.membersAdded, { count: usernames.length })
})
);
setInProgressIds([]);
fetchUsers({
offset: 0,
limit: usersOffset,
...(sourceFilterKeyword && { keyword: sourceFilterKeyword })
});
},
error({ response: { response } }) {
dispatch(showErrorDialog({ error: response }));
}
});
}
};
const onRemoveMembers = () => {
const { targetItems, getChecked, setCheckedList } = transferListState;
const users = getChecked(targetItems);
const usernames = users.map((item) => item.id);
if (users.length) {
setInProgressIds(usernames);
deleteUsersFromGroup(group.id, usernames).subscribe({
next() {
setCheckedList({});
setTargetItems(excludeCommonItems(targetItems, users));
setMembersLookup(reversePluckProps(membersLookup, ...usernames));
dispatch(
showSystemNotification({
message: formatMessage(translations.membersRemoved, { count: usernames.length })
})
);
setInProgressIds([]);
fetchUsers({
limit: usersOffset,
offset: 0,
...(sourceFilterKeyword && { keyword: sourceFilterKeyword })
});
},
error({ response: { response } }) {
dispatch(showErrorDialog({ error: response }));
}
});
}
};
const onChangeValue = (property) => {
setIsDirty(true);
setGroup({ [property.key]: property.value });
};
const onSave = () => {
onSubmittingAndOrPendingChange({
isSubmitting: true
});
if (props.group) {
update(pluckProps(group, 'id', 'desc')).subscribe({
next(group) {
dispatch(
showSystemNotification({
message: formatMessage(translations.groupEdited)
})
);
setIsDirty(false);
onGroupSaved(group);
fnRefs.current.onSubmittingAndOrPendingChange({
isSubmitting: false
});
},
error({ response: { response } }) {
dispatch(showErrorDialog({ error: response }));
fnRefs.current.onSubmittingAndOrPendingChange({
isSubmitting: false
});
}
});
} else {
create({ name: group.name.trim(), desc: group.desc }).subscribe({
next(group) {
dispatch(
showSystemNotification({
message: formatMessage(translations.groupCreated)
})
);
setIsDirty(false);
onGroupSaved(group);
fnRefs.current.onSubmittingAndOrPendingChange({
isSubmitting: false
});
// Fetch users and members for created group
fetchUsers();
fetchMembers(group.id);
},
error({ response: { response } }) {
dispatch(showErrorDialog({ error: response }));
fnRefs.current.onSubmittingAndOrPendingChange({
isSubmitting: false
});
}
});
}
};
const fetchUsers = (options) => {
fetchAll({
limit: usersFetchSize,
...options
}).subscribe((_users) => {
setUsersHaveNextPage(_users.total > _users.length);
setUsers(_users);
setUsersOffset(options?.limit ?? usersFetchSize);
});
};
const fetchMembers = (groupId) => {
fetchUsersFromGroup(groupId).subscribe((members) => {
setMembers(members);
setMembersLookup(createPresenceTable(members, true, (member) => member.username));
});
};
const fetchMoreUsers = (options) => {
fetchAll({
limit: usersFetchSize,
offset: usersOffset,
...options
}).subscribe((_users) => {
const newUsersLength = usersRef.current.length + _users.length;
setUsersHaveNextPage(_users.total > newUsersLength);
setUsers([...usersRef.current, ..._users]);
setUsersOffset(usersOffset + usersFetchSize);
});
};
const onFilterUsers = (keyword) => {
fetchUsers({
keyword,
offset: 0
});
};
// region effects
useMount(() => {
if (props.group) {
fetchUsers();
fetchMembers(props.group.id);
}
});
useEffect(() => {
users && setSourceItems(users.map((user) => ({ id: user.username, title: user.username, subtitle: user.email })));
}, [users, setSourceItems]);
useEffect(() => {
members &&
setTargetItems(
members.map((user) => ({
id: user.username,
title: user.username,
subtitle: user.email
}))
);
}, [members, setTargetItems]);
useEffect(() => {
if (props.group?.id !== group?.id) {
setGroup(props.group);
}
}, [group?.id, props.group, setGroup]);
useEffect(() => {
onSubmittingAndOrPendingChange({ hasPendingChanges: isDirty });
}, [isDirty, onSubmittingAndOrPendingChange]);
// endregion
return React.createElement(EditGroupDialogUI, {
title: React.createElement(
Typography,
{ variant: 'h6', component: 'h2' },
isEdit
? group.externallyManaged
? React.createElement(FormattedMessage, {
id: 'groupEditDialog.viewExternallyManagedGroup',
defaultMessage: 'View Group (managed externally)'
})
: React.createElement(FormattedMessage, { id: 'groupEditDialog.editGroup', defaultMessage: 'Edit Group' })
: React.createElement(FormattedMessage, { id: 'groupEditDialog.createGroup', defaultMessage: 'Create Group' })
),
onCloseButtonClick: (e) => onClose(e, null),
group: group,
groupNameError: groupNameError,
isEdit: isEdit,
users: users,
members: members,
membersLookup: membersLookup,
onDeleteGroup: onDeleteGroup,
onChangeValue: onChangeValue,
submitOk: submitOk,
onSave: onSave,
onAddMembers: onAddMembers,
onRemoveMembers: onRemoveMembers,
inProgressIds: inProgressIds,
isDirty: isDirty,
transferListState: transferListState,
sourceItemsAllChecked: sourceItemsAllChecked,
onFilterUsers: onFilterUsers,
onFetchMoreUsers: fetchMoreUsers,
hasMoreUsers: usersHaveNextPage,
disableAddMembers: disableAddMembers,
isSubmitting: isSubmitting
});
}
export default EditGroupDialogContainer;