@nakedobjects/cicero
Version:
Single Page Application client for a Naked Objects application.
1 lines • 229 kB
Source Map (JSON)
{"version":3,"file":"nakedobjects-cicero.mjs","sources":["../../../cicero/src/helpers-components.ts","../../../cicero/src/user-messages.ts","../../../cicero/src/cicero-commands/result.ts","../../../cicero/src/cicero-commands/command-result.ts","../../../cicero/src/cicero-commands/command.ts","../../../cicero/src/cicero-commands/action.ts","../../../cicero/src/cicero-commands/back.ts","../../../cicero/src/cicero-commands/cancel.ts","../../../cicero/src/cicero-commands/clipboard.ts","../../../cicero/src/cicero-commands/edit.ts","../../../cicero/src/validate.ts","../../../cicero/src/cicero-commands/enter.ts","../../../cicero/src/cicero-commands/forward.ts","../../../cicero/src/cicero-commands/gemini.ts","../../../cicero/src/cicero-commands/goto.ts","../../../cicero/src/cicero-commands/help.ts","../../../cicero/src/cicero-commands/menu.ts","../../../cicero/src/cicero-commands/ok.ts","../../../cicero/src/cicero-commands/page.ts","../../../cicero/src/cicero-commands/reload.ts","../../../cicero/src/cicero-commands/root.ts","../../../cicero/src/cicero-commands/save.ts","../../../cicero/src/cicero-commands/selection.ts","../../../cicero/src/cicero-commands/show.ts","../../../cicero/src/cicero-commands/where.ts","../../../cicero/src/cicero-context.service.ts","../../../cicero/src/cicero-renderer.service.ts","../../../cicero/src/cicero-command-factory.service.ts","../../../cicero/src/cicero/cicero.component.ts","../../../cicero/src/cicero/cicero.component.html","../../../cicero/src/lib.module.ts","../../../cicero/src/nakedobjects-cicero.ts"],"sourcesContent":["import { ElementRef } from '@angular/core';\nimport { SubscriptionLike as ISubscription } from 'rxjs';\n\nexport function safeUnsubscribe(sub?: ISubscription) {\n if (sub) {\n sub.unsubscribe();\n }\n}\n\nfunction isFocusable(nativeElement: unknown): nativeElement is { focus: () => void } {\n return !!(nativeElement && nativeElement instanceof Object && 'focus' in nativeElement);\n}\n\nfunction safeFocus(nativeElement?: unknown) {\n if (isFocusable(nativeElement)) {\n nativeElement.focus();\n }\n}\n\nexport function focus(element?: ElementRef) {\n setTimeout(() => safeFocus(element?.nativeElement));\n return true;\n}\n\nexport function hasMessage(obj: unknown): obj is { message: string } {\n return typeof obj === 'object' && obj !== null && 'message' in obj && typeof obj.message == 'string';\n}\n\nexport function messageFrom(e: unknown) {\n return hasMessage(e) ? e.message : 'unknown error';\n}","import { ILocalFilter } from '@nakedobjects/services';\n\nexport const mandatory = 'Mandatory';\nexport const optional = 'Optional';\nexport const choices = 'Choices';\n\nexport const tooLong = 'Too long';\nexport const notANumber = 'Not a number';\nexport const noPatternMatch = 'Invalid entry';\n\nexport const outOfRange = (_: unknown, min: unknown, max: unknown, filter: ILocalFilter) => {\n const minVal = filter ? filter.filter(min) : min;\n const maxVal = filter ? filter.filter(max) : max;\n\n return `Value is outside the range ${minVal || 'unlimited'} to ${maxVal || 'unlimited'}`;\n};\n\nexport const welcomeMessage = 'Welcome to Cicero. Type \\'help\\' and the Enter key for more information.';\nexport const basicHelp = 'Cicero is a user interface purpose-designed to work with an audio screen-reader.\\n' +\n 'The display has only two fields: a read-only output field, and a single input field.\\n' +\n 'The input field always has the focus.\\n' +\n 'Commands are typed into the input field followed by the Enter key.\\n' +\n 'When the output field updates (either instantaneously or after the server has responded)\\n' +\n 'its contents are read out automatically, so \\n' +\n 'the user never has to navigate around the screen.\\n' +\n 'Commands, such as \\'action\\', \\'field\\' and \\'save\\', may be typed in full\\n' +\n 'or abbreviated to the first two or more characters.\\n' +\n 'Commands are not case sensitive.\\n' +\n 'Some commands take one or more arguments.\\n' +\n 'There must be a space between the command word and the first argument,\\n' +\n 'and a comma between arguments.\\n' +\n 'Arguments may contain spaces if needed.\\n' +\n 'The commands available to the user vary according to the context.\\n' +\n 'The command \\'help ?\\' (note that there is a space between help and \\'?\\')\\n' +\n 'will list the commands available to the user in the current context.\\n' +\n '‘help’ followed by another command word (in full or abbreviated) will give more details about that command.\\n' +\n 'Some commands will change the context, for example using the Go command to navigate to an associated object, \\n' +\n 'in which case the new context will be read out.\\n' +\n 'Other commands - help being an example - do not change the context, but will read out information to the user.\\n' +\n 'If the user needs a reminder of the current context, the \\'Where\\' command will read the context out again.\\n' +\n 'Hitting Enter on the empty input field has the same effect.\\n' +\n 'When the user enters a command and the output has been updated, the input field will be cleared, \\n' +\n 'ready for the next command. The user may recall the previous command by hitting the up-arrow key.\\n' +\n 'The user might then edit or extend that previous command and hit Enter to run it again.\\n' +\n 'For advanced users: commands may be chained using a semi-colon between them,\\n' +\n 'however commands that do, or might, result in data updates cannot be chained.';\nexport const actionCommand = 'action';\nexport const actionHelp = 'Open the dialog for action from a menu, or from object actions.\\n' +\n 'A dialog is always opened for an action, even if it has no fields (parameters):\\n' +\n 'This is a safety mechanism, allowing the user to confirm that the action is the one intended.\\n' +\n 'Once the dialog fields have been completed, using the Enter command,\\n' +\n 'the action may then be invoked with the OK command.\\n' +\n 'The action command takes two optional arguments.\\n' +\n 'The first is the name, or partial name, of the action.\\n' +\n 'If the partial name matches more than one action, a list of matches is returned but none opened.\\n' +\n 'If no argument is provided, a full list of available action names is returned.\\n' +\n 'The partial name may have more than one clause, separated by spaces.\\n' +\n 'these may match either parts of the action name or the sub-menu name if one exists.\\n' +\n 'If the action name matches a single action, then a question-mark may be added as a second\\n' +\n 'parameter, which will generate a more detailed description of the Action.';\nexport const backCommand = 'back';\nexport const backHelp = 'Move back to the previous context.';\nexport const cancelCommand = 'cancel';\nexport const cancelHelp = 'Leave the current activity (action dialog, or object edit), incomplete.';\nexport const clipboardCommand = 'clipboard';\nexport const clipboardCopy = 'copy';\nexport const clipboardShow = 'show';\nexport const clipboardGo = 'go';\nexport const clipboardDiscard = 'discard';\nexport const clipboardHelp = 'The clipboard command is used for temporarily\\n' +\n 'holding a reference to an object, so that it may be used later\\n' +\n 'to enter into a field.\\n' +\n 'Clipboard requires one argument, which may take one of four values:\\n' +\n 'copy, show, go, or discard\\n' +\n 'each of which may be abbreviated down to one character.\\n' +\n 'Copy copies a reference to the object being viewed into the clipboard,\\n' +\n 'overwriting any existing reference.\\n' +\n 'Show displays the content of the clipboard without using it.\\n' +\n 'Go takes you directly to the object held in the clipboard.\\n' +\n 'Discard removes any existing reference from the clipboard.\\n' +\n 'The reference held in the clipboard may be used within the Enter command.';\nexport const editCommand = 'edit';\nexport const editHelp = 'Put an object into Edit mode.';\nexport const enterCommand = 'enter';\nexport const enterHelp = 'Enter a value into a field,\\n' +\n 'meaning a parameter in an action dialog,\\n' +\n 'or a property on an object being edited.\\n' +\n 'Enter requires 2 arguments.\\n' +\n 'The first argument is the partial field name, which must match a single field.\\n' +\n 'The second optional argument specifies the value, or selection, to be entered.\\n' +\n 'If a question mark is provided as the second argument, the field will not be\\n' +\n 'updated but further details will be provided about that input field.\\n' +\n 'If the word paste is used as the second argument, then, provided that the field is\\n' +\n 'a reference field, the object reference in the clipboard will be pasted into the field.\\n';\nexport const forwardCommand = 'forward';\nexport const forwardHelp = 'Move forward to next context in the history\\n' +\n '(if you have previously moved back).';\nexport const geminiCommand = 'gemini';\nexport const geminiHelp = 'Switch to the Gemini (graphical) user interface\\n' +\n 'preserving the current context.';\nexport const gotoCommand = 'goto';\nexport const gotoHelp = 'Go to the object referenced in a property,\\n' +\n 'or to a collection within an object,\\n' +\n 'or to an object within an open list or collection.\\n' +\n 'Goto takes one argument. In the context of an object\\n' +\n 'that is the name or partial name of the property or collection.\\n' +\n 'In the context of an open list or collection, it is the\\n' +\n 'number of the item within the list or collection (starting at 1). ';\nexport const helpCommand = 'help';\nexport const helpHelp = 'If no argument is specified, help provides a basic explanation of how to use Cicero.\\n' +\n 'If help is followed by a question mark as an argument, this lists the commands available\\n' +\n 'in the current context. If help is followed by another command word as an argument\\n' +\n '(or an abbreviation of it), a description of the specified Command is returned.';\nexport const menuCommand = 'menu';\nexport const menuHelp = 'Open a named main menu, from any context.\\n' +\n 'Menu takes one optional argument: the name, or partial name, of the menu.\\n' +\n 'If the partial name matches more than one menu, a list of matches is returned\\n' +\n 'but no menu is opened; if no argument is provided a list of all the menus\\n' +\n 'is returned.';\nexport const okCommand = 'ok';\nexport const okHelp = 'Invoke the action currently open as a dialog.\\n' +\n 'Fields in the dialog should be completed before this.';\nexport const pageCommand = 'page';\nexport const pageFirst = 'first';\nexport const pagePrevious = 'previous';\nexport const pageNext = 'next';\nexport const pageLast = 'last';\nexport const pageHelp = 'Supports paging of returned lists.\\n' +\n 'The page command takes a single argument, which may be one of these four words:\\n' +\n 'first, previous, next, or last, \\n' +\n 'which may be abbreviated down to the first character.\\n' +\n 'Alternative, the argument may be a specific page number.';\nexport const reloadCommand = 'reload';\nexport const reloadHelp = 'Not yet implemented. Reload the data from the server for an object or a list.\\n' +\n 'Note that for a list, which was generated by an action, reload runs the action again, \\n' +\n 'thus ensuring that the list is up to date. However, reloading a list does not reload the\\n' +\n 'individual objects in that list, which may still be cached. Invoking Reload on an\\n' +\n 'individual object, however, will ensure that its fields show the latest server data.';\nexport const rootCommand = 'root';\nexport const rootHelp = 'From within an opend collection context, the root command returns\\n' +\n ' to the root object that owns the collection. Does not take any arguments.\\n';\nexport const saveCommand = 'save';\nexport const saveHelp = 'Save the updated fields on an object that is being edited,\\n' +\n 'and return from edit mode to a normal view of that object';\nexport const selectionCommand = 'selection';\nexport const selectionHelp = 'Not fully implemented. Select one or more items from a list,\\n' +\n 'prior to invoking an action on the selection.\\n' +\n 'Selection has one mandatory argument, which must be one of these words,\\n' +\n 'add, remove, all, clear, show.\\n' +\n 'The Add and Remove options must be followed by a second argument specifying\\n' +\n 'the item number, or range, to be added or removed.\\n';\nexport const showCommand = 'show';\nexport const showHelp = 'In the context of an object, shows the name and content of\\n' +\n 'one or more of the properties.\\n' +\n 'May take 1 argument: the partial field name.\\n' +\n 'If this matches more than one property, a list of matches is returned.\\n' +\n 'If no argument is provided, the full list of properties is returned.\\n' +\n 'In the context of an opened object collection, or a list,\\n' +\n 'shows one or more items from that collection or list.\\n' +\n 'If no arguments are specified, show will list all of the the items in the collection,\\n' +\n 'or the first page of items if in a list view.\\n' +\n 'Alternatively, the command may be specified with an item number, or a range such as 3- 5.';\nexport const whereCommand = 'where';\nexport const whereHelp = 'Display a reminder of the current context.\\n' +\n 'The same can also be achieved by hitting the Return key on the empty input field.';\n\n// Cicero feedback messages\nexport const commandTooShort = 'Command word must have at least 2 characters';\nexport const noCommandMatch = (a: string) => `No command begins with ${a}`;\nexport const commandsAvailable = 'Commands available in current context:\\n';\n\nexport const noArguments = 'No arguments provided';\nexport const tooFewArguments = 'Too few arguments provided';\nexport const tooManyArguments = 'Too many arguments provided';\n\nexport const commandNotAvailable = (c: string) => `The command: ${c} is not available in the current context`;\n\nexport const startHigherEnd = 'Starting item number cannot be greater than the ending item number';\n\nexport const highestItem = (n: number) => `The highest numbered item is ${n}`;\n\nexport const item = 'item';\nexport const empty = 'empty';\nexport const numberOfItems = (n: number) => `${n} items`;\nexport const on = 'on';\nexport const collection = 'Collection';\nexport const modified = 'modified';\nexport const properties = 'properties';\nexport const modifiedProperties = `Modified ${properties}`;\nexport const page = 'Page';\n\nexport const noVisible = 'No visible properties';\n\nexport const doesNotMatch = (name: string) => `${name} does not match any properties`;\n\nexport const cannotPage = 'Cannot page list';\n\nexport const alreadyOnFirst = 'List is already showing the first page';\n\nexport const alreadyOnLast = 'List is already showing the last page';\n\nexport const pageArgumentWrong = 'The argument must match: first, previous, next, last, or a single number';\n\nexport const pageNumberWrong = (max: number) => `Specified page number must be between 1 and ${max}`;\n\nexport const mayNotbeChainedMessage = (c: string, r: string) => `${c} command may not be chained${r}. Use Where command to see where execution stopped.`;\n\nexport const queryOnlyRider = ' unless the action is query-only';\n\nexport const noSuchCommand = (c: string) => `No such command: ${c}`;\n\nexport const missingArgument = (i: number) => `Required argument number ${i} is missing`;\n\nexport const wrongTypeArgument = (i: number) => `Argument number ${i} must be a number`;\n\nexport const isNotANumber = (s: string) => `${s} is not a number`;\n\nexport const tooManyDashes = 'Cannot have more than one dash in argument';\n\nexport const mustBeGreaterThanZero = 'Item number or range values must be greater than zero';\n\nexport const pleaseCompleteOrCorrect = 'Please complete or correct these fields:\\n';\n\nexport const required = 'required';\n\nexport const mustbeQuestionMark = 'Second argument may only be a question mark - to get action details';\n\nexport const noActionsAvailable = 'No actions available';\n\nexport const doesNotMatchActions = (a: string | undefined) => `${a} does not match any actions`;\n\nexport const matchingActions = 'Matching actions:\\n';\nexport const actionsMessage = 'Actions:\\n';\nexport const actionPrefix = 'Action:';\nexport const disabledPrefix = 'disabled:';\n\nexport const isDisabled = 'is disabled.';\n\nexport const noDescription = 'No description provided';\n\nexport const descriptionPrefix = 'Description for action:';\n\nexport const clipboardError = 'Clipboard command may only be followed by copy, show, go, or discard';\n\nexport const clipboardContextError = 'Clipboard copy may only be used in the context of viewing an object';\n\nexport const clipboardContents = (contents: string) => `Clipboard contains: ${contents}`;\n\nexport const clipboardEmpty = 'Clipboard is empty';\n\nexport const doesNotMatchProperties = (name: string | undefined) => `${name} does not match any properties`;\n\nexport const matchesMultiple = 'matches multiple fields:\\n';\n\nexport const doesNotMatchDialog = (name: string | undefined) => `${name} does not match any fields in the dialog`;\n\nexport const multipleFieldMatches = 'Multiple fields match';\n\nexport const isNotModifiable = 'is not modifiable';\n\nexport const invalidCase = 'Invalid case';\n\nexport const invalidRefEntry = 'Invalid entry for a reference field. Use clipboard or clip';\n\nexport const emptyClipboard = 'Cannot use Clipboard as it is empty';\nexport const incompatibleClipboard = 'Contents of Clipboard are not compatible with the field';\nexport const noMatch = (s: string) => `None of the choices matches ${s}`;\nexport const multipleMatches = 'Multiple matches:\\n';\nexport const fieldName = (name: string) => `Field name: ${name}`;\nexport const descriptionFieldPrefix = 'Description:';\nexport const typePrefix = 'Type:';\n\nexport const unModifiablePrefix = (reason: string) => `Unmodifiable: ${reason}`;\nexport const outOfItemRange = (n: number | undefined) => `${n} is out of range for displayed items`;\nexport const doesNotMatchMenu = (name: string | undefined) => `${name} does not match any menu`;\nexport const matchingMenus = 'Matching menus:';\nexport const menuTitle = (title: string) => `${title} menu`;\nexport const allMenus = 'Menus:';\nexport const noRefFieldMatch = (s: string) => `${s} does not match any reference fields or collections`;\nexport const unsaved = 'Unsaved';\nexport const editing = 'Editing';\n","export class Result {\n input: string | null = null;\n output: string | null = null;\n\n static create(input: string | null, output: string | null): Result {\n return { input: input, output: output };\n }\n}\n","import * as Ro from '@nakedobjects/restful-objects';\nimport { ContextService } from '@nakedobjects/services';\nimport { Dictionary } from 'lodash';\nimport map from 'lodash-es/map';\nimport mapValues from 'lodash-es/mapValues';\nimport { Result } from './result';\n\n// todo move this\nexport function getParametersAndCurrentValue(action: Ro.ActionMember | Ro.ActionRepresentation | Ro.InvokableActionMember, context: ContextService): Dictionary<Ro.Value> {\n\n if (action instanceof Ro.InvokableActionMember || action instanceof Ro.ActionRepresentation) {\n const parms = action.parameters();\n const cachedValues = context.getDialogCachedValues(action.actionId());\n const values = mapValues(parms, p => {\n const value = cachedValues[p.id()];\n return value === undefined ? p.default() : value;\n });\n return values;\n }\n return {};\n}\n\nexport function getFields(field: Ro.IField): Ro.IField[] {\n\n if (field instanceof Ro.Parameter) {\n const action = field.parent;\n if (action instanceof Ro.InvokableActionMember || action instanceof Ro.ActionRepresentation) {\n const parms = action.parameters();\n return map(parms, p => p as Ro.IField);\n }\n }\n\n if (field instanceof Ro.PropertyMember) {\n // todo\n return [];\n }\n\n return [];\n}\n\nexport class CommandResult extends Result {\n stopChain?: boolean;\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n changeState: () => void = () => { };\n}\n","import { Location } from '@angular/common';\nimport * as Ro from '@nakedobjects/restful-objects';\nimport {\n ClientErrorCode,\n CollectionViewState,\n ConfigService,\n ContextService,\n ErrorCategory,\n ErrorService,\n ErrorWrapper,\n InteractionMode,\n MaskService,\n PaneRouteData,\n UrlManagerService\n } from '@nakedobjects/services';\nimport { Dictionary } from 'lodash';\nimport each from 'lodash-es/each';\nimport every from 'lodash-es/every';\nimport filter from 'lodash-es/filter';\nimport findIndex from 'lodash-es/findIndex';\nimport forEach from 'lodash-es/forEach';\nimport keys from 'lodash-es/keys';\nimport map from 'lodash-es/map';\nimport some from 'lodash-es/some';\nimport * as Commandresult from './command-result';\nimport { CommandResult } from './command-result';\nimport { CiceroCommandFactoryService } from '../cicero-command-factory.service';\nimport { CiceroContextService } from '../cicero-context.service';\nimport { CiceroRendererService } from '../cicero-renderer.service';\nimport * as Usermessages from '../user-messages';\n\nexport abstract class Command {\n\n constructor(protected urlManager: UrlManagerService,\n protected location: Location,\n protected commandFactory: CiceroCommandFactoryService,\n protected context: ContextService,\n protected mask: MaskService,\n protected error: ErrorService,\n protected configService: ConfigService,\n protected ciceroContext: CiceroContextService,\n protected ciceroRenderer: CiceroRendererService,\n ) {\n \n }\n\n argString: string | null = null;\n chained = false;\n\n abstract shortCommand: string;\n abstract fullCommand: string;\n abstract helpText: string;\n protected abstract minArguments: number;\n protected abstract maxArguments: number;\n protected get keySeparator() { \n return this.configService.config.keySeparator;\n }\n\n execute(): Promise<CommandResult> {\n const result = new CommandResult();\n\n // TODO Create outgoing Vm and copy across values as needed\n if (!this.isAvailableInCurrentContext()) {\n return this.returnResult('', Usermessages.commandNotAvailable(this.fullCommand));\n }\n // TODO: This could be moved into a pre-parse method as it does not depend on context\n if (this.argString == null) {\n if (this.minArguments > 0) {\n return this.returnResult('', Usermessages.noArguments);\n }\n } else {\n const args = this.argString.split(',');\n if (args.length < this.minArguments) {\n\n return this.returnResult('', Usermessages.tooFewArguments);\n } else if (args.length > this.maxArguments) {\n return this.returnResult('', Usermessages.tooManyArguments);\n }\n }\n return this.doExecute(this.argString!, this.chained, result);\n }\n\n protected returnResult(input: string | null, output: string | null, changeState?: () => void, stopChain?: boolean): Promise<CommandResult> {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n changeState = changeState ? changeState : () => { };\n return Promise.resolve({ input: input, output: output, changeState: changeState, stopChain: !!stopChain });\n }\n\n protected abstract doExecute(args: string | null, chained: boolean, result: CommandResult): Promise<CommandResult>;\n\n abstract isAvailableInCurrentContext(): boolean;\n\n protected mayNotBeChained(rider = '') {\n return Usermessages.mayNotbeChainedMessage(this.fullCommand, rider);\n }\n\n checkMatch(matchText: string): void {\n if (this.fullCommand.indexOf(matchText) !== 0) {\n throw new Error(Usermessages.noSuchCommand(matchText));\n }\n }\n\n // argNo starts from 0.\n // If argument does not parse correctly, message will be passed to UI and command aborted.\n protected argumentAsString(argString: string | null, argNo: number, optional = false, toLower = true): string | undefined {\n if (!argString) { return undefined; }\n if (!optional && argString.split(',').length < argNo + 1) {\n throw new Error(Usermessages.tooFewArguments);\n }\n const args = argString.split(',');\n if (args.length < argNo + 1) {\n if (optional) {\n return undefined;\n } else {\n throw new Error(Usermessages.missingArgument(argNo + 1));\n }\n }\n return toLower ? args[argNo].trim().toLowerCase() : args[argNo].trim(); // which may be \"\" if argString ends in a ','\n }\n\n // argNo starts from 0.\n protected argumentAsNumber(args: string | null, argNo: number, optional = false): number | null {\n const arg = this.argumentAsString(args, argNo, optional);\n if (!arg && optional) { return null; }\n const number = parseInt(arg!, 10);\n if (isNaN(number)) {\n throw new Error(Usermessages.wrongTypeArgument(argNo + 1));\n }\n return number;\n }\n\n protected parseInt(input: string): number {\n const number = parseInt(input, 10);\n if (isNaN(number)) {\n throw new Error(Usermessages.isNotANumber(input));\n }\n return number;\n }\n\n // Parses '17, 3-5, -9, 6-' into two numbers\n protected parseRange(arg?: string): { start: number | null; end: number | null } {\n if (!arg) {\n arg = '1-';\n }\n const clauses = arg.split('-');\n const range: { start: number | null; end: number | null } = { start: null, end: null };\n switch (clauses.length) {\n case 1: {\n const firstValue = clauses[0];\n range.start = firstValue ? this.parseInt(firstValue) : null;\n range.end = range.start;\n break;\n }\n case 2: {\n const firstValue = clauses[0];\n const secondValue = clauses[1];\n range.start = firstValue ? this.parseInt(firstValue) : null;\n range.end = secondValue ? this.parseInt(secondValue) : null;\n break;\n }\n default:\n throw new Error(Usermessages.tooManyDashes);\n }\n if ((range.start != null && range.start < 1) || (range.end != null && range.end < 1)) {\n throw new Error(Usermessages.mustBeGreaterThanZero);\n }\n return range;\n }\n\n protected getContextDescription(): string | null {\n // todo\n return null;\n }\n\n protected routeData(): PaneRouteData {\n return this.urlManager.getRouteData().pane1;\n }\n\n // Helpers delegating to RouteData\n protected isHome(): boolean {\n return this.urlManager.isHome();\n }\n\n protected isObject(): boolean {\n return this.urlManager.isObject();\n }\n\n protected getObject(): Promise<Ro.DomainObjectRepresentation> {\n const oid = Ro.ObjectIdWrapper.fromObjectId(this.routeData().objectId!, this.keySeparator);\n // TODO: Consider view model & transient modes?\n\n return this.context.getObject(1, oid, this.routeData().interactionMode!).then((obj: Ro.DomainObjectRepresentation) => {\n if (this.routeData().interactionMode === InteractionMode.Edit) {\n return this.context.getObjectForEdit(1, obj);\n } else {\n return obj; // To wrap a known object as a promise\n }\n });\n }\n\n protected isList(): boolean {\n return this.urlManager.isList();\n }\n\n protected getList(): Promise<Ro.ListRepresentation> {\n const routeData = this.routeData();\n // TODO: Currently covers only the list-from-menu; need to cover list from object action\n return this.context.getListFromMenu(routeData, routeData.page, routeData.pageSize);\n }\n\n protected isMenu(): boolean {\n return !!this.routeData().menuId;\n }\n\n protected getMenu(): Promise<Ro.MenuRepresentation> {\n return this.context.getMenu(this.routeData().menuId!);\n }\n\n protected isDialog(): boolean {\n return !!this.routeData().dialogId;\n }\n\n protected isMultiChoiceField(field: Ro.IField) {\n const entryType = field.entryType();\n return entryType === Ro.EntryType.MultipleChoices || entryType === Ro.EntryType.MultipleConditionalChoices;\n }\n\n protected getActionForCurrentDialog(): Promise<Ro.InvokableActionMember | Ro.ActionRepresentation> {\n const dialogId = this.routeData().dialogId;\n if (this.isObject()) {\n return this.getObject().then((obj: Ro.DomainObjectRepresentation) => this.context.getInvokableAction(obj.actionMember(dialogId!)));\n } else if (this.isMenu()) {\n return this.getMenu().then((menu: Ro.MenuRepresentation) => this.context.getInvokableAction(menu.actionMember(dialogId!))); // i.e. return a promise\n }\n return Promise.reject(new ErrorWrapper(ErrorCategory.ClientError, ClientErrorCode.NotImplemented, 'List actions not implemented yet'));\n }\n\n // Tests that at least one collection is open (should only be one).\n // TODO: assumes that closing collection removes it from routeData NOT sets it to Summary\n protected isCollection(): boolean {\n return this.isObject() && some(this.routeData().collections);\n }\n\n protected closeAnyOpenCollections() {\n const open = this.ciceroRenderer.openCollectionIds(this.routeData());\n forEach(open, id => this.urlManager.setCollectionMemberState(id, CollectionViewState.Summary));\n }\n\n protected isTable(): boolean {\n return false; // TODO\n }\n\n protected isEdit(): boolean {\n return this.routeData().interactionMode === InteractionMode.Edit;\n }\n\n protected isForm(): boolean {\n return this.routeData().interactionMode === InteractionMode.Form;\n }\n\n protected isTransient(): boolean {\n return this.routeData().interactionMode === InteractionMode.Transient;\n }\n\n protected matchingProperties(obj: Ro.DomainObjectRepresentation, match?: string): Ro.PropertyMember[] {\n let props = map(obj.propertyMembers(), prop => prop);\n if (match) {\n props = this.matchFriendlyNameAndOrMenuPath(props, match);\n }\n return props;\n }\n\n protected matchingCollections(obj: Ro.DomainObjectRepresentation, match?: string): Ro.CollectionMember[] {\n const allColls = map(obj.collectionMembers(), action => action);\n if (match) {\n return this.matchFriendlyNameAndOrMenuPath<Ro.CollectionMember>(allColls, match);\n } else {\n return allColls;\n }\n }\n\n protected matchingParameters(action: Ro.InvokableActionMember, match: string): Ro.Parameter[] {\n let params = map(action.parameters(), p => p);\n if (match) {\n params = this.matchFriendlyNameAndOrMenuPath(params, match);\n }\n return params;\n }\n\n protected matchFriendlyNameAndOrMenuPath<T extends Ro.IHasExtensions>(\n reps: T[],\n match: string | undefined): T[] {\n const clauses = match ? match.split(' ') : [];\n // An exact match has preference over any partial match\n const exactMatches = filter(reps,\n (rep) => {\n const path = rep.extensions().menuPath();\n const name = rep.extensions().friendlyName().toLowerCase();\n return match === name ||\n (!!path && match === path.toLowerCase() + ' ' + name) ||\n every(clauses, clause => name === clause || (!!path && path.toLowerCase() === clause));\n });\n if (exactMatches.length > 0) { return exactMatches; }\n return filter(reps,\n rep => {\n const path = rep.extensions().menuPath();\n const name = rep.extensions().friendlyName().toLowerCase();\n return every(clauses, clause => name.indexOf(clause) >= 0 || (!!path && path.toLowerCase().indexOf(clause) >= 0));\n });\n }\n\n protected findMatchingChoicesForRef(choices: Dictionary<Ro.Value> | null, titleMatch: string): Ro.Value[] {\n return choices ? filter(choices, v => v.toString().toLowerCase().indexOf(titleMatch.toLowerCase()) >= 0) : [];\n }\n\n protected findMatchingChoicesForScalar(choices: Dictionary<Ro.Value> | null, titleMatch: string): Ro.Value[] {\n if (choices == null) {\n return [];\n }\n\n const labels = keys(choices);\n const matchingLabels = filter(labels, l => l.toString().toLowerCase().indexOf(titleMatch.toLowerCase()) >= 0);\n const result = new Array<Ro.Value>();\n switch (matchingLabels.length) {\n case 0:\n break; // leave result empty\n case 1:\n // Push the VALUE for the key\n // For simple scalars they are the same, but not for Enums\n result.push(choices[matchingLabels[0]]);\n break;\n default:\n // Push the matching KEYs, wrapped as (pseudo) Values for display in message to user\n // For simple scalars the values would also be OK, but not for Enums\n forEach(matchingLabels, label => result.push(new Ro.Value(label)));\n break;\n }\n return result;\n }\n\n protected handleErrorResponse(err: Ro.ErrorMap, getFriendlyName: (id: string) => string) {\n if (err.invalidReason()) {\n return this.returnResult('', err.invalidReason());\n }\n let msg = Usermessages.pleaseCompleteOrCorrect;\n each(err.valuesMap(),\n (errorValue, fieldId) => {\n msg += this.fieldValidationMessage(errorValue, () => getFriendlyName(fieldId!));\n });\n return this.returnResult('', msg);\n }\n\n protected handleErrorResponseNew(err: Ro.ErrorMap, getFriendlyName: (id: string) => string) {\n if (err.invalidReason()) {\n return this.returnResult('', err.invalidReason());\n }\n let msg = Usermessages.pleaseCompleteOrCorrect;\n each(err.valuesMap(),\n (errorValue, fieldId) => {\n msg += this.fieldValidationMessage(errorValue, () => getFriendlyName(fieldId!));\n });\n return this.returnResult('', msg);\n }\n\n protected validationMessage(reason: string | null, value: Ro.Value, fieldFriendlyName: string): string {\n if (reason) {\n const prefix = `${fieldFriendlyName}: `;\n const suffix = reason === Usermessages.mandatory ? Usermessages.required : `${value} ${reason}`;\n return `${prefix}${suffix}\\n`;\n }\n return '';\n }\n\n private fieldValidationMessage(errorValue: Ro.ErrorValue, fieldFriendlyName: () => string): string {\n const reason = errorValue.invalidReason;\n return this.validationMessage(reason, errorValue.value, fieldFriendlyName());\n }\n\n protected valueForUrl(val: Ro.Value, field: Ro.IField): Ro.Value | null {\n if (val.isNull()) { return val; }\n const fieldEntryType = field.entryType();\n\n if (fieldEntryType !== Ro.EntryType.FreeForm || field.isCollectionContributed()) {\n\n if (this.isMultiChoiceField(field) || field.isCollectionContributed()) {\n let valuesFromRouteData: Ro.Value[] | null = new Array<Ro.Value>();\n if (field instanceof Ro.Parameter) {\n const rd = Commandresult.getParametersAndCurrentValue(field.parent, this.context)[field.id()];\n if (rd) { valuesFromRouteData = rd.list(); } // TODO: what if only one?\n } else if (field instanceof Ro.PropertyMember) {\n const obj = field.parent as Ro.DomainObjectRepresentation;\n const props = this.context.getObjectCachedValues(obj.id());\n const rd = props[field.id()];\n if (rd) { valuesFromRouteData = rd.list(); } // TODO: what if only one?\n }\n let vals: Ro.Value[] = [];\n if (val.isReference() || val.isScalar()) {\n vals = new Array<Ro.Value>(val);\n } else if (val.isList()) { // Should be!\n vals = val.list()!;\n }\n valuesFromRouteData = valuesFromRouteData || [];\n\n forEach(vals, v => this.addOrRemoveValue(valuesFromRouteData!, v));\n\n if (vals[0].isScalar()) { // then all must be scalar\n const scalars = map(valuesFromRouteData, v => v.scalar());\n return new Ro.Value(scalars);\n } else { // assumed to be links\n const links: Ro.ILink[] = map(valuesFromRouteData, v => ({ href: v.link()!.href(), title: Ro.withUndefined(v.link()!.title()) }));\n return new Ro.Value(links.length > 0 ? links : null);\n }\n }\n if (val.isScalar()) {\n return val;\n }\n // reference\n return this.leanLink(val);\n }\n\n if (val.isScalar()) {\n if (val.isNull()) {\n return new Ro.Value('');\n }\n return val;\n // TODO: consider these options:\n // if (from.value instanceof Date) {\n // return new Value((from.value as Date).toISOString());\n // }\n\n // return new Value(from.value as number | string | boolean);\n }\n if (val.isReference()) {\n return this.leanLink(val);\n }\n return null;\n }\n\n private leanLink(val: Ro.Value): Ro.Value {\n return new Ro.Value({ href: val.link()!.href()!, title: val.link()!.title()! });\n }\n\n private addOrRemoveValue(valuesFromRouteData: Ro.Value[], val: Ro.Value) {\n let index: number;\n let valToAdd: Ro.Value;\n if (val.isScalar()) {\n valToAdd = val;\n index = findIndex(valuesFromRouteData, v => v.scalar() === val.scalar());\n } else { // Must be reference\n valToAdd = this.leanLink(val);\n index = findIndex(valuesFromRouteData, v => v.link()!.href() === valToAdd.link()!.href());\n }\n if (index > -1) {\n valuesFromRouteData.splice(index, 1);\n } else {\n valuesFromRouteData.push(valToAdd);\n }\n }\n\n protected setFieldValueInContext(field: Ro.Parameter, val: Ro.Value) {\n this.context.cacheFieldValue(this.routeData().dialogId!, field.id(), val);\n }\n\n protected setPropertyValueinContext(obj: Ro.DomainObjectRepresentation, property: Ro.PropertyMember, urlVal: Ro.Value) {\n this.context.cachePropertyValue(obj, property, urlVal);\n }\n}\n","import * as Ro from '@nakedobjects/restful-objects';\nimport { Location } from '@angular/common';\nimport { Dictionary } from 'lodash';\nimport forEach from 'lodash-es/forEach';\nimport map from 'lodash-es/map';\nimport reduce from 'lodash-es/reduce';\nimport { CommandResult } from './command-result';\nimport * as Usermessages from '../user-messages';\nimport { Command } from './command';\nimport { CiceroCommandFactoryService } from '../cicero-command-factory.service';\nimport { CiceroContextService } from '../cicero-context.service';\nimport { CiceroRendererService } from '../cicero-renderer.service';\nimport { UrlManagerService, ContextService, MaskService, ErrorService, ConfigService } from '@nakedobjects/services';\n\nexport class Action extends Command {\n\n constructor(urlManager: UrlManagerService,\n location: Location,\n commandFactory: CiceroCommandFactoryService,\n context: ContextService,\n mask: MaskService,\n error: ErrorService,\n configService: ConfigService,\n ciceroContext: CiceroContextService,\n ciceroRenderer: CiceroRendererService,\n ) {\n super(urlManager, location, commandFactory, context, mask, error, configService, ciceroContext, ciceroRenderer);\n }\n\n override shortCommand = 'ac';\n override fullCommand = Usermessages.actionCommand;\n\n override helpText = Usermessages.actionHelp;\n protected override minArguments = 0;\n protected override maxArguments = 2;\n\n isAvailableInCurrentContext(): boolean {\n return (this.isMenu() || this.isObject() || this.isForm()) && !this.isDialog() && !this.isEdit(); // TODO add list\n }\n\n doExecute(args: string | null, _chained: boolean, _result: CommandResult): Promise<CommandResult> {\n const match = this.argumentAsString(args, 0);\n const details = this.argumentAsString(args, 1, true);\n if (details && details !== '?') {\n return this.returnResult('', Usermessages.mustbeQuestionMark);\n }\n if (this.isObject()) {\n return this.getObject().then(obj => this.processActions(match, obj.actionMembers(), details));\n\n } else if (this.isMenu()) {\n return this.getMenu().then(menu => this.processActions(match, menu.actionMembers(), details));\n }\n // TODO: handle list - CCAs\n return Promise.reject('TODO: handle list - CCAs');\n }\n\n private processActions(match: string | undefined, actionsMap: Dictionary<Ro.ActionMember>, details: string | undefined): Promise<CommandResult> {\n let actions = map(actionsMap, action => action);\n if (actions.length === 0) {\n return this.returnResult('', Usermessages.noActionsAvailable);\n }\n if (match) {\n actions = this.matchFriendlyNameAndOrMenuPath(actions, match);\n }\n switch (actions.length) {\n case 0:\n return this.returnResult('', Usermessages.doesNotMatchActions(match));\n case 1: {\n const action = actions[0];\n if (details) {\n return this.returnResult('', this.renderActionDetails(action));\n } else if (action.disabledReason()) {\n return this.returnResult('', this.disabledAction(action));\n } else {\n return this.openActionDialog(action);\n }\n }\n default: {\n let output = match ? Usermessages.matchingActions : Usermessages.actionsMessage;\n output += this.listActions(actions);\n return this.returnResult('', output);\n }\n }\n }\n\n private disabledAction(action: Ro.ActionMember) {\n return `${Usermessages.actionPrefix} ${action.extensions().friendlyName()} ${Usermessages.isDisabled} ${action.disabledReason()}`;\n }\n\n private listActions(actions: Ro.ActionMember[]): string {\n return reduce(actions,\n (s, t) => {\n const menupath = t.extensions().menuPath() ? `${t.extensions().menuPath()} - ` : '';\n const disabled = t.disabledReason() ? ` (${Usermessages.disabledPrefix} ${t.disabledReason()})` : '';\n return s + menupath + t.extensions().friendlyName() + disabled + '\\n';\n },\n '');\n }\n\n private openActionDialog(action: Ro.ActionMember): Promise<CommandResult> {\n\n return this.context.getInvokableAction(action).\n then(invokable => {\n\n this.context.clearDialogCachedValues();\n this.urlManager.setDialog(action.actionId());\n forEach(invokable.parameters(), p => this.setFieldValueInContext(p, p.default()));\n\n return this.returnResult('', '');\n });\n }\n\n private renderActionDetails(action: Ro.ActionMember) {\n return `${Usermessages.descriptionPrefix} ${action.extensions().friendlyName()}\\n${action.extensions().description() || Usermessages.noDescription}`;\n }\n}\n","import { Command } from './command';\nimport { CiceroCommandFactoryService } from '../cicero-command-factory.service';\nimport { CiceroContextService } from '../cicero-context.service';\nimport { CiceroRendererService } from '../cicero-renderer.service';\nimport { UrlManagerService, ContextService, MaskService, ErrorService, ConfigService } from '@nakedobjects/services';\nimport { CommandResult } from './command-result';\nimport * as Usermessages from '../user-messages';\nimport { Location } from '@angular/common';\n\nexport class Back extends Command {\n\n constructor(urlManager: UrlManagerService,\n location: Location,\n commandFactory: CiceroCommandFactoryService,\n context: ContextService,\n mask: MaskService,\n error: ErrorService,\n configService: ConfigService,\n ciceroContext: CiceroContextService,\n ciceroRenderer: CiceroRendererService,\n ) {\n super(urlManager, location, commandFactory, context, mask, error, configService, ciceroContext, ciceroRenderer);\n }\n\n override shortCommand = 'ba';\n override fullCommand = Usermessages.backCommand;\n override helpText = Usermessages.backHelp;\n protected override minArguments = 0;\n protected override maxArguments = 0;\n\n isAvailableInCurrentContext(): boolean {\n return true;\n }\n\n doExecute(_args: string | null, _chained: boolean): Promise<CommandResult> {\n return this.returnResult('', '', () => this.location.back());\n }\n}\n","import { InteractionMode } from '@nakedobjects/services';\nimport { Command } from './command';\nimport { CiceroCommandFactoryService } from '../cicero-command-factory.service';\nimport { CiceroContextService } from '../cicero-context.service';\nimport { CiceroRendererService } from '../cicero-renderer.service';\nimport { UrlManagerService, ContextService, MaskService, ErrorService, ConfigService } from '@nakedobjects/services';\nimport { CommandResult } from './command-result';\nimport * as Msg from '../user-messages';\nimport { Location } from '@angular/common';\n\nexport class Cancel extends Command {\n\n constructor(urlManager: UrlManagerService,\n location: Location,\n commandFactory: CiceroCommandFactoryService,\n context: ContextService,\n mask: MaskService,\n error: ErrorService,\n configService: ConfigService,\n ciceroContext: CiceroContextService,\n ciceroRenderer: CiceroRendererService,\n ) {\n super(urlManager, location, commandFactory, context, mask, error, configService, ciceroContext, ciceroRenderer);\n }\n\n override shortCommand = 'ca';\n override fullCommand = Msg.cancelCommand;\n override helpText = Msg.cancelHelp;\n protected override minArguments = 0;\n protected override maxArguments = 0;\n\n isAvailableInCurrentContext(): boolean {\n return this.isDialog() || this.isEdit();\n }\n\n doExecute(_args: string | null, _chained: boolean): Promise<CommandResult> {\n if (this.isEdit()) {\n return this.returnResult('', '', () => this.urlManager.setInteractionMode(InteractionMode.View));\n }\n\n if (this.isDialog()) {\n return this.returnResult('', '', () => this.urlManager.closeDialogReplaceHistory(this.routeData().dialogId!));\n }\n\n return this.returnResult('', 'some sort of error'); // todo\n }\n}\n","import * as Models from '@nakedobjects/restful-objects';\nimport { Command } from './command';\nimport { CiceroCommandFactoryService } from '../cicero-command-factory.service';\nimport { CiceroContextService } from '../cicero-context.service';\nimport { CiceroRendererService } from '../cicero-renderer.service';\nimport { UrlManagerService, ContextService, MaskService, ErrorService, ConfigService } from '@nakedobjects/services';\nimport { CommandResult } from './command-result';\nimport * as Usermessages from '../user-messages';\nimport { Location } from '@angular/common';\n\nexport class Clipboard extends Command {\n\n constructor(urlManager: UrlManagerService,\n location: Location,\n commandFactory: CiceroCommandFactoryService,\n context: ContextService,\n mask: MaskService,\n error: ErrorService,\n configService: ConfigService,\n ciceroContext: CiceroContextService,\n ciceroRenderer: CiceroRendererService,\n ) {\n super(urlManager, location, commandFactory, context, mask, error, configService, ciceroContext, ciceroRenderer);\n }\n\n override shortCommand = 'cl';\n override fullCommand = Usermessages.clipboardCommand;\n override helpText = Usermessages.clipboardHelp;\n\n protected override minArguments = 1;\n protected override maxArguments = 1;\n\n isAvailableInCurrentContext(): boolean {\n return true;\n }\n\n doExecute(args: string | null, _chained: boolean): Promise<CommandResult> {\n const sub = this.argumentAsString(args, 0);\n\n if