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

[MI-2570] Add modal to list records using filters and add slash command #27

Draft
wants to merge 2 commits into
base: MI-2553
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions server/plugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,13 @@ func (p *Plugin) handleCreate(args *model.CommandArgs, parameters []string, clie

func (p *Plugin) handleRecords(args *model.CommandArgs, parameters []string, client Client, isSysAdmin bool) string {
if len(parameters) == 0 {
return "Invalid create command. Available commands are 'share' and 'view'."
return "Invalid record command. Available commands are 'share', 'list' and 'view'."
}

command := parameters[0]
parameters = parameters[1:]
switch command {
case constants.SubCommandSearchAndShare:
case constants.SubCommandSearchAndShare, constants.SubCommandList:
return ""
case constants.SubCommandView:
return p.handleViewRecords(args, parameters, client)
Expand Down Expand Up @@ -487,6 +487,9 @@ func getAutocompleteData() *model.AutocompleteData {
changeRequestRecord.AddTextArgument("Record number", "[record_number]", "")
records.AddCommand(viewRecords)

listRecords := model.NewAutocompleteData(constants.SubCommandList, "", "List ServiceNow records")
records.AddCommand(listRecords)

serviceNow.AddCommand(records)

create := model.NewAutocompleteData(constants.CommandCreate, "[command]", fmt.Sprintf("Available commands: %s, %s", constants.SubCommandIncident, constants.SubCommandRequest))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type RecordTypePanelProps = {
requiredFieldValidationErr?: boolean;
recordType: RecordType | null;
setRecordType: (value: RecordType) => void;
setResetRecordPanelStates: (reset: boolean) => void;
setResetRecordPanelStates?: (reset: boolean) => void;
showFooter?: boolean;
placeholder?: string;
recordTypeOptions: DropdownOptionType[];
Expand Down Expand Up @@ -56,7 +56,9 @@ const RecordTypePanel = forwardRef<HTMLDivElement, RecordTypePanelProps>(({
// Handle change in record type
const handleRecordTypeChange = (newValue: RecordType) => {
setRecordType(newValue);
setResetRecordPanelStates(true);
if (setResetRecordPanelStates) {
setResetRecordPanelStates(true);
}
};

return (
Expand Down
208 changes: 208 additions & 0 deletions webapp/src/containers/listRecords/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import {AutoSuggest, CustomModal as Modal, ModalFooter, ModalHeader} from '@brightscout/mattermost-ui-library';
import React, {useCallback, useEffect, useState} from 'react';
import {useDispatch} from 'react-redux';

import usePluginApi from 'src/hooks/usePluginApi';
import {resetGlobalModalState} from 'src/reducers/globalModal';
import {isFilterRecordsModalOpen} from 'src/selectors';
import RecordTypePanel from '../addOrEditSubscriptions/subComponents/recordTypePanel';
import Constants from 'src/plugin_constants';

import Utils from 'src/utils';

import './styles.scss';

// TODO: mock data, will change later
const AssignmentGroupOptions: FieldsFilterData[] = [
{
sys_id: 'mockId 1',
name: 'group 1',
},
{
sys_id: 'mockId 2',
name: 'group 2',
},
{
sys_id: 'mockId 3',
name: 'group 3',
},
];

const ServiceOptions: FieldsFilterData[] = [
{
sys_id: 'mockId 1',
name: 'service 1',
},
{
sys_id: 'mockId 2',
name: 'service 2',
},
{
sys_id: 'mockId 3',
name: 'service 3',
},
];

const ListRecords = () => {
const [showModal, setShowModal] = useState(false);
const [assignmentGroupOptions, setAssignmentGroupOptions] = useState<FieldsFilterData[]>(AssignmentGroupOptions);
const [serviceOptions, setServiceOptions] = useState<FieldsFilterData[]>(ServiceOptions);
const [assignmentGroupSuggestions, setAssignmentGroupSuggestions] = useState<Record<string, string>[]>([]);
const [serviceSuggestions, setServiceSuggestions] = useState<Record<string, string>[]>([]);
const [assignmentGroupAutoSuggestValue, setassignmentGroupAutoSuggestValue] = useState('');
const [serviceAutoSuggestValue, setServiceAutoSuggestValue] = useState('');
const [recordType, setRecordType] = useState<RecordType | null>(null);
const [assignmentGroup, setAssignmentGroup] = useState<Record<string, string> | null>(null);
const [service, setService] = useState<Record<string, string> | null>(null);

const {pluginState} = usePluginApi();
const dispatch = useDispatch();

const open = isFilterRecordsModalOpen(pluginState);

// Reset the field states
const resetFieldStates = useCallback(() => {
setRecordType(null);
setAssignmentGroupOptions([]);
setServiceOptions([]);
setAssignmentGroupSuggestions([]);
setServiceSuggestions([]);
setassignmentGroupAutoSuggestValue('');
setServiceAutoSuggestValue('');
setAssignmentGroup(null);
setService(null);
}, []);

// Hide the modal and reset the states
const hideModal = useCallback(() => {
dispatch(resetGlobalModalState());
setShowModal(false);
setTimeout(() => {
resetFieldStates();
});
}, []);

const showFilteredRecords = () => {
// TODO: for testing remove after integration
// eslint-disable-next-line no-console
console.log(recordType, assignmentGroup, service);
hideModal();
};

const getSuggestions = ({searchFor}: {searchFor?: string}) => {
if (searchFor) {
// TODO: Make Api call later
}
};

const mapRequestsToSuggestions = (data: FieldsFilterData[]): Array<Record<string, string>> => data.map((d) => ({
id: d.sys_id,
name: d.name,
}));

const debouncedGetSuggestions = useCallback(Utils.debounce(getSuggestions, Constants.DebounceFunctionTimeLimit), [getSuggestions]);

const handleAssignmentGroupInputChange = (currentValue: string) => {
setassignmentGroupAutoSuggestValue(currentValue);
if (currentValue) {
if (currentValue.length >= Constants.DefaultCharThresholdToShowSuggestions) {
debouncedGetSuggestions({searchFor: currentValue});
}
}
};

const handleServiceInputChange = (currentValue: string) => {
setServiceAutoSuggestValue(currentValue);
if (currentValue) {
if (currentValue.length >= Constants.DefaultCharThresholdToShowSuggestions) {
debouncedGetSuggestions({searchFor: currentValue});
}
}
};

const handleAssignmentGroupSelection = (suggestion: Record<string, string> | null) => {
setassignmentGroupAutoSuggestValue(suggestion?.name || '');
setAssignmentGroup(suggestion);
};

const handleServiceSelection = (suggestion: Record<string, string> | null) => {
setServiceAutoSuggestValue(suggestion?.name || '');
setService(suggestion);
};

useEffect(() => {
setAssignmentGroupSuggestions(mapRequestsToSuggestions(assignmentGroupOptions));
}, [assignmentGroupOptions]);

useEffect(() => {
setServiceSuggestions(mapRequestsToSuggestions(serviceOptions));
}, [serviceOptions]);

useEffect(() => {
if (open && pluginState.connectedReducer.connected) {
setShowModal(true);
} else {
dispatch(resetGlobalModalState());
}
}, [open]);

return (
<Modal
show={showModal}
onHide={hideModal}
className='servicenow-rhs-modal'
>
<>
<ModalHeader
title='List records'
onHide={hideModal}
showCloseIconInHeader={true}
/>
<RecordTypePanel
className='margin-bottom-25'
recordType={recordType}
setRecordType={setRecordType}
recordTypeOptions={Constants.recordTypeOptions}
/>
<div
className={
`servicenow-list-records-modal__auto-suggest
${(assignmentGroup || serviceOptions) && 'servicenow-list-records-modal__suggestion-chosen'}
`}
>
<AutoSuggest
placeholder='Search Assignment Groups'
inputValue={assignmentGroupAutoSuggestValue}
onInputValueChange={handleAssignmentGroupInputChange}
onChangeSelectedSuggestion={handleAssignmentGroupSelection}
suggestionConfig={{
suggestions: assignmentGroupSuggestions,
renderValue: (suggestion) => suggestion.name,
}}
charThresholdToShowSuggestions={Constants.DefaultCharThresholdToShowSuggestions}
/>
<AutoSuggest
placeholder='Search Services'
inputValue={serviceAutoSuggestValue}
onInputValueChange={handleServiceInputChange}
onChangeSelectedSuggestion={handleServiceSelection}
suggestionConfig={{
suggestions: serviceSuggestions,
renderValue: (suggestion) => suggestion.name,
}}
charThresholdToShowSuggestions={Constants.DefaultCharThresholdToShowSuggestions}
/>
</div>
<ModalFooter
onConfirm={showFilteredRecords}
confirmBtnText='Submit'
confirmDisabled={!recordType}
onHide={hideModal}
cancelBtnText='Cancel'
/>
</>
</Modal>
);
};

export default ListRecords;
14 changes: 14 additions & 0 deletions webapp/src/containers/listRecords/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.servicenow-list-records-modal {
&__auto-suggest {
.auto-suggest {
left: 12px;
width: 573px;
}
}

&__suggestion-chosen {
.auto-suggest__selected-option-container {
padding-left: 15px !important;
}
}
}
8 changes: 8 additions & 0 deletions webapp/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ export default class Hooks {
};
}

if (commandTrimmed?.startsWith('/servicenow records list')) {
this.store.dispatch(setGlobalModalState({modalId: ModalIds.LIST_RECORDS}) as Action);
return {
message,
args: contextArgs,
};
}

return Promise.resolve({
message,
args: contextArgs,
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import NotificationPost from 'src/containers/custom_post/notificationPost';
import ShareRecordPost from 'src/containers/custom_post/shareRecordPost';
import CreateRequest from 'src/containers/createRequest';
import CreateIncidentPostMenuAction from 'src/containers/createIncident/createIncidentMenu';
import ListRecords from 'src/containers/listRecords';
import ShareRecords from 'src/containers/shareRecords';
import UpdateState from 'src/containers/updateState';

Expand Down Expand Up @@ -46,6 +47,7 @@ export default class Plugin {
registry.registerRootComponent(ShareRecords);
registry.registerRootComponent(UpdateState);
registry.registerRootComponent(CreateRequest);
registry.registerRootComponent(ListRecords);
registry.registerRootComponent(App);
const {id, toggleRHSPlugin} = registry.registerRightHandSidebarComponent(Rhs, Constants.RightSidebarHeader);
registry.registerChannelHeaderButtonAction(<ServiceNowIcon className='servicenow-icon'/>, () => store.dispatch(toggleRHSPlugin), null, Constants.ChannelHeaderTooltipText);
Expand Down
1 change: 1 addition & 0 deletions webapp/src/plugin_constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export enum ModalIds {
UPDATE_STATE = 'updateState',
CREATE_INCIDENT = 'createIncident',
CREATE_REQUEST = 'createRequest',
LIST_RECORDS = 'listRecords',
}

export enum SubscriptionEvents {
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const isCreateIncidentModalOpen = (state: PluginState): boolean => state.
export const isCreateRequestModalOpen = (state: PluginState): boolean => state.globalModalReducer.modalId === ModalIds.CREATE_REQUEST;

export const getApiRequestCompletionState = (state: PluginState): ApiRequestCompletionState => state.apiRequestCompletionReducer;

export const isFilterRecordsModalOpen = (state: PluginState): boolean => state.globalModalReducer.modalId === ModalIds.LIST_RECORDS;
5 changes: 5 additions & 0 deletions webapp/src/types/common/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,8 @@ type FormatTextOptions = {
type MessageHtmlToComponentOptions = {
mentionHighlight: boolean;
}

type FieldsFilterData = {
sys_id: string;
name: string;
}