Skip to content

Commit

Permalink
Feature/execute custom action (#442)
Browse files Browse the repository at this point in the history
* fix(snapp): console log warning about missing key

* fix(client-utils): getsegments

do not fail on root/content/example(1).pdf

* fix(snapp):  console errors on start and search

* feature(snapp): custom actions for crud

closes #382

* fix(snapp): term can be undefined

* refactor(snapp): do not mutate object

* chore(snapp): execute action clear monaco

* chore: rename delete actions parameter

Co-Authored-By: Aniko Litvanyi <herflis33@gmail.com>

* style: lint fix

* chore: do not request actions multiple times
  • Loading branch information
zoltanbedi committed Nov 11, 2019
1 parent 38eca9e commit 497aa44
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class CommandPaletteComponent extends React.Component<
public static contextType: React.Context<Repository> = RepositoryContext

private handleKeyUp(ev: KeyboardEvent) {
if (ev.key.toLowerCase() === 'p' && ev.ctrlKey) {
if (ev.key && ev.key.toLowerCase() === 'p' && ev.ctrlKey) {
ev.stopImmediatePropagation()
ev.preventDefault()
if (ev.shiftKey) {
Expand Down
141 changes: 90 additions & 51 deletions apps/sensenet/src/components/dialogs/execute-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import Typography from '@material-ui/core/Typography'
import React, { useEffect, useState } from 'react'
import MonacoEditor from 'react-monaco-editor'
import { useInjector, useLogger, useRepository } from '@sensenet/hooks-react'
import { PathHelper } from '@sensenet/client-utils'
import { useLocalization, useTheme } from '../../hooks'
import { CustomActionCommandProvider } from '../../services/CommandProviders/CustomActionCommandProvider'
import { createCustomActionModel } from '../../services/MonacoModels/create-custom-action-model'
import { getMonacoModelUri } from '../edit/TextEditor'

const postBodyCache = new Map<string, string>()
const EDITOR_INITIAL_VALUE = `{
}`

export const ExecuteActionDialog: React.FunctionComponent = () => {
const theme = useTheme()
Expand All @@ -26,7 +30,7 @@ export const ExecuteActionDialog: React.FunctionComponent = () => {
const [actionValue, setActionValue] = useState(customActionService.onExecuteAction.getValue())

const [isVisible, setIsVisible] = useState(false)
const [postBody, setPostBody] = useState('{}')
const [postBody, setPostBody] = useState(EDITOR_INITIAL_VALUE)
const [uri, setUri] = useState<import('monaco-editor').Uri>()

const [isExecuting, setIsExecuting] = useState(false)
Expand All @@ -41,7 +45,7 @@ export const ExecuteActionDialog: React.FunctionComponent = () => {
if (stored) {
setPostBody(stored)
} else {
setPostBody('{}')
setPostBody(EDITOR_INITIAL_VALUE)
}
setError('')
}, [uri])
Expand All @@ -63,14 +67,89 @@ export const ExecuteActionDialog: React.FunctionComponent = () => {
return () => observables.forEach(o => o.dispose())
}, [customActionService.onExecuteAction, repo])

const getActionResult = async () => {
setIsExecuting(true)
setError('')
try {
switch (actionValue.action.Name) {
case 'Load':
return await repo.load({ idOrPath: actionValue.content.Id, oDataOptions: { select: 'all' } })
case 'LoadCollection':
return await repo.loadCollection({ path: actionValue.content.Path })
case 'Create': {
const parsedBody = JSON.parse(postBody) as { contentType: string; content: object }
return await repo.post({
contentType: parsedBody.contentType,
parentPath: actionValue.content.IsFolder
? actionValue.content.Path
: PathHelper.getParentPath(actionValue.content.Path),
content: parsedBody.content,
})
}
case 'Remove': {
const { permanent } = JSON.parse(postBody)
return await repo.delete({
idOrPath: actionValue.content.Id,
permanent: permanent == null ? false : permanent,
})
}
case 'Update':
return await repo.patch({ idOrPath: actionValue.content.Id, content: JSON.parse(postBody).content })
default:
return await repo.executeAction({
idOrPath: actionValue.content.Id,
body: JSON.parse(postBody),
method: actionValue.method,
name: actionValue.action.Name,
})
}
} catch (e) {
setError(e.message)
logger.error({
message: `There was an error executing custom action '${actionValue.action.DisplayName ||
actionValue.action.Name}'`,
data: {
isDismissed: true,
relatedRepository: repo.configuration.repositoryUrl,
relatedContent: actionValue.content,
details: { actionValue, error },
},
})
} finally {
setIsExecuting(false)
}
}

const onClick = async () => {
const result = await getActionResult()

result &&
customActionService.onActionExecuted.setValue({
action: actionValue.action,
content: actionValue.content,
response: result,
})

result &&
logger.information({
message: `Action executed: '${actionValue.action.DisplayName || actionValue.action.Name}'`,
data: {
relatedContent: actionValue.content,
relatedRepository: repo.configuration.repositoryUrl,
details: { actionValue, result },
},
})
setPostBody(EDITOR_INITIAL_VALUE)
setIsVisible(!result)
}

const onClose = () => {
setPostBody(EDITOR_INITIAL_VALUE)
setIsVisible(false)
}

return (
<Dialog
open={isVisible}
onClose={() => setIsVisible(false)}
fullWidth={true}
onKeyUp={ev => {
ev.key === 'Escape' && setIsVisible(false)
}}>
<Dialog open={isVisible} onClose={onClose} fullWidth={true}>
<DialogTitle>
{localization.title
.replace('{0}', (actionValue && (actionValue.action.DisplayName || actionValue.action.Name)) || '')
Expand Down Expand Up @@ -121,7 +200,7 @@ export const ExecuteActionDialog: React.FunctionComponent = () => {
<div style={{ flex: 1, marginLeft: '1.5em' }}>
{error ? <Typography color="error">{error}</Typography> : null}
</div>
<Button onClick={() => setIsVisible(false)}>{localization.cancelButton}</Button>
<Button onClick={onClose}>{localization.cancelButton}</Button>
<Button
autoFocus={
!(
Expand All @@ -131,47 +210,7 @@ export const ExecuteActionDialog: React.FunctionComponent = () => {
actionValue.metadata.parameters.length > 0
)
}
onClick={async () => {
setIsExecuting(true)
setError('')
try {
const result = await repo.executeAction({
idOrPath: actionValue.content.Id,
body: JSON.parse(postBody),
method: actionValue.method,
name: actionValue.action.Name,
})
customActionService.onActionExecuted.setValue({
action: actionValue.action,
content: actionValue.content,
response: result,
})

logger.information({
message: `Action executed: '${actionValue.action.DisplayName || actionValue.action.Name}'`,
data: {
relatedContent: actionValue.content,
relatedRepository: repo.configuration.repositoryUrl,
details: { actionValue, result },
},
})
setIsVisible(false)
} catch (e) {
setError(e.message)
logger.error({
message: `There was an error executing custom action '${actionValue.action.DisplayName ||
actionValue.action.Name}'`,
data: {
isDismissed: true,
relatedRepository: repo.configuration.repositoryUrl,
relatedContent: actionValue.content,
details: { actionValue, error },
},
})
} finally {
setIsExecuting(false)
}
}}>
onClick={onClick}>
{localization.executeButton}
</Button>
</DialogActions>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MetadataAction } from '@sensenet/client-core'
import { MetadataAction, ODataResponse, Repository } from '@sensenet/client-core'
import { Injectable, ObservableValue } from '@sensenet/client-utils'
import { ActionModel, GenericContent } from '@sensenet/default-content-types'
import { CommandProvider, SearchOptions } from '../CommandProviderManager'
Expand All @@ -17,43 +17,64 @@ export class CustomActionCommandProvider implements CommandProvider {
public onActionExecuted = new ObservableValue<{ content: GenericContent; action: ActionModel; response: any }>()

public shouldExec(options: SearchOptions) {
return this.selectionService.activeContent.getValue() && options.term.length > 2 && options.term.startsWith('>')
return this.selectionService.activeContent.getValue() &&
options.term &&
options.term.length > 2 &&
options.term.startsWith('>')
? true
: false
}
public async getItems(options: SearchOptions) {
const content = this.selectionService.activeContent.getValue()
const localization = this.localization.currentValues.getValue().commandPalette.customAction
const filteredTerm = options.term.substr(1).toLowerCase()
if (!content) {
return []

private contentWithActionsAndMetadata: ODataResponse<GenericContent> | undefined

private async getActions(id: number, repository: Repository) {
if (this.contentWithActionsAndMetadata && id === this.contentWithActionsAndMetadata.d.Id) {
return this.contentWithActionsAndMetadata
}
const result = await options.repository.load<GenericContent>({
idOrPath: content.Id,
const result = await repository.load<GenericContent>({
idOrPath: id,
oDataOptions: {
metadata: 'full',
expand: ['Actions'],
select: ['Actions'],
},
})
const actions = (result.d.Actions as ActionModel[]) || []
this.contentWithActionsAndMetadata = result
return result
}

return actions
public async getItems(options: SearchOptions) {
const content = this.selectionService.activeContent.getValue()
const localization = this.localization.currentValues.getValue().commandPalette.customAction
const filteredTerm = options.term.substr(1).toLowerCase()
if (!content) {
return []
}

const { d: contentWithActions } = await this.getActions(content.Id, options.repository)

return (contentWithActions.Actions as ActionModel[])
.filter(
a =>
(a.Name.toLowerCase().includes(filteredTerm) && a.IsODataAction) ||
(a.DisplayName.toLowerCase().includes(filteredTerm) && a.IsODataAction),
)
.map(a => {
const actionMetadata =
result.d.__metadata &&
result.d.__metadata.actions &&
result.d.__metadata.actions.find(action => action.name === a.Name)
contentWithActions.__metadata &&
contentWithActions.__metadata.actions &&
contentWithActions.__metadata.actions.find(action => action.name === a.Name)

const functionMetadata =
result.d.__metadata &&
result.d.__metadata.functions &&
result.d.__metadata.functions.find(fn => fn.name === a.Name)
contentWithActions.__metadata &&
contentWithActions.__metadata.functions &&
contentWithActions.__metadata.functions.find(fn => fn.name === a.Name)

// merge custom parameters to function metadata
const customActionMetadata = functionMetadata && {
...functionMetadata,
...this.addParametersForCustomActions(a),
}

return {
primaryText: localization.executePrimaryText
Expand All @@ -67,7 +88,7 @@ export class CustomActionCommandProvider implements CommandProvider {
this.onExecuteAction.setValue({
action: a,
content,
metadata: actionMetadata || functionMetadata,
metadata: actionMetadata || customActionMetadata,
method: actionMetadata ? 'POST' : 'GET',
}),
}
Expand All @@ -78,4 +99,22 @@ export class CustomActionCommandProvider implements CommandProvider {
private readonly selectionService: SelectionService,
private readonly localization: LocalizationService,
) {}

private addParametersForCustomActions(action: ActionModel) {
switch (action.Name) {
case 'Create':
return {
parameters: [
{ name: 'contentType', type: 'string', required: true },
{ name: 'content', type: 'object', required: true },
],
}
case 'Update':
return { parameters: [{ name: 'content', type: 'object', required: true }] }
case 'Remove':
return { parameters: [{ name: 'permanent', type: 'boolean', required: false }] }
default:
return
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CommandPaletteItem } from '../../hooks'
@Injectable({ lifetime: 'singleton' })
export class HistoryCommandProvider implements CommandProvider {
public shouldExec({ term }: SearchOptions) {
return term.length === 0
return term != null && term.length === 0
}
public async getItems(): Promise<CommandPaletteItem[]> {
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CommandPaletteItem } from '../../hooks'
@Injectable({ lifetime: 'singleton' })
export class InFolderSearchCommandProvider implements CommandProvider {
public shouldExec({ term }: SearchOptions): boolean {
return term[0] === '/'
return term != null && term[0] === '/'
}

public async getItems(options: SearchOptions): Promise<CommandPaletteItem[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export class NavigationCommandProvider implements CommandProvider {
private localizationValues: ReturnType<LocalizationService['currentValues']['getValue']>['navigationCommandProvider']

public shouldExec(options: SearchOptions) {
const termLowerCase = options.term.toLocaleLowerCase()
const termLowerCase = options.term && options.term.toLocaleLowerCase()
return (
options.term.length > 0 &&
options.term != null &&
this.getRoutes(options).find(
r =>
r.primaryText.toLocaleLowerCase().includes(termLowerCase) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export class QueryCommandProvider implements CommandProvider {
private readonly localization: LocalizationService,
) {}

public shouldExec(options: SearchOptions): boolean {
return options.term[0] === '+'
public shouldExec({ term }: SearchOptions): boolean {
return term != null && term[0] === '+'
}

public async getItems(options: SearchOptions): Promise<CommandPaletteItem[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const getJsonType = (type: string) => {
if (lowerType.includes('bool')) {
return 'boolean'
}
if (lowerType.includes('object')) {
return 'object'
}
return 'string'
}

Expand Down
Loading

0 comments on commit 497aa44

Please sign in to comment.