diff --git a/CHANGELOG.md b/CHANGELOG.md index 067adfe72..80d2797aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v1.14.3](https://github.com/nyaruka/floweditor/compare/v1.14.1...v1.14.3) + +> 20 September 2021 + +- Let components control how new objects are created [`#1013`](https://github.com/nyaruka/floweditor/pull/1013) +- Use topic rather than subject for open ticket events in simulator [`#1012`](https://github.com/nyaruka/floweditor/pull/1012) +- Bump axios from 0.21.1 to 0.21.2 [`#1011`](https://github.com/nyaruka/floweditor/pull/1011) +- Change topic to be an object rather than string [`521994f`](https://github.com/nyaruka/floweditor/commit/521994f536a45618c7adf7cfd2b5cc6adeed2d9e) + +#### [v1.14.1](https://github.com/nyaruka/floweditor/compare/v1.14.0...v1.14.1) + +> 15 September 2021 + +- Topic and assignee [`#1010`](https://github.com/nyaruka/floweditor/pull/1010) +- Include latest translations from transifex [`#1006`](https://github.com/nyaruka/floweditor/pull/1006) +- Add topic and assignee to open ticket action [`f3f6274`](https://github.com/nyaruka/floweditor/commit/f3f6274e0169de45d81e302be899b7010b289f5e) +- Update snapshots [`07a9bef`](https://github.com/nyaruka/floweditor/commit/07a9befef7699da7d41f235ddee31e44bb475899) +- Update snapshots [`95de60a`](https://github.com/nyaruka/floweditor/commit/95de60a6b89860baf573858a5e02995d4e3c2230) + #### [v1.14.0](https://github.com/nyaruka/floweditor/compare/v1.13.19...v1.14.0) > 14 July 2021 diff --git a/lambda/topics.js b/lambda/topics.js new file mode 100644 index 000000000..41f8e1c3c --- /dev/null +++ b/lambda/topics.js @@ -0,0 +1,29 @@ +import { v4 as generateUUID } from 'uuid'; + +import { respond } from './utils/index.js'; +const topics = { + next: null, + previous: null, + results: [ + { + uuid: '6f38eba0-d673-4a35-82df-21bae2b6d466', + name: 'General', + created_on: '2021-09-01T01:06:39.178493Z' + } + ] +}; + +exports.handler = (request, context, callback) => { + if (request.httpMethod === 'POST') { + const body = JSON.parse(request.body); + respond(callback, { + uuid: generateUUID(), + name: body.name, + query: null, + status: 'ready', + count: 0 + }); + } else { + respond(callback, topics); + } +}; diff --git a/lambda/users.js b/lambda/users.js new file mode 100644 index 000000000..cf70c6a64 --- /dev/null +++ b/lambda/users.js @@ -0,0 +1,45 @@ +import { v4 as generateUUID } from 'uuid'; + +import { respond } from './utils/index.js'; +const users = { + next: null, + previous: null, + results: [ + { + email: 'agent.user@gmail.com', + first_name: 'Agent', + last_name: 'User', + role: 'agent', + created_on: '2021-06-10T21:44:30.971221Z' + }, + { + email: 'viewr.user@gmail.com', + first_name: 'Viewer', + last_name: 'User', + role: 'viewer', + created_on: '2020-11-09T23:02:10.095493Z' + }, + { + email: 'admin.user@gmail.com', + first_name: 'Admin', + last_name: 'User', + role: 'administrator', + created_on: '2020-08-18T19:07:08.984182Z' + } + ] +}; + +exports.handler = (request, context, callback) => { + if (request.httpMethod === 'POST') { + const body = JSON.parse(request.body); + respond(callback, { + uuid: generateUUID(), + name: body.name, + query: null, + status: 'ready', + count: 0 + }); + } else { + respond(callback, users); + } +}; diff --git a/package.json b/package.json index 29863b1ca..44386adfd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@nyaruka/flow-editor", "license": "AGPL-3.0", "repository": "git://github.com/nyaruka/floweditor.git", - "version": "1.14.0", + "version": "1.14.3", "description": "'Standalone flow editing tool designed for use within the RapidPro suite of messaging tools but can be adopted for use outside of that ecosystem.'", "browser": "umd/flow-editor.min.js", "unpkg": "umd/flow-editor.min.js", @@ -70,7 +70,7 @@ "@babel/core": "^7.4.4", "@babel/preset-env": "^7.4.4", "@babel/preset-react": "7.0.0", - "@nyaruka/temba-components": "0.11.13", + "@nyaruka/temba-components": "0.16.1", "@testing-library/jest-dom": "4.0.0", "@testing-library/react": "8.0.1", "@types/common-tags": "^1.8.0", @@ -96,7 +96,7 @@ "array-move": "2.1.0", "auto-bind": "2.1.0", "auto-changelog": "1.13.0", - "axios": "0.21.1", + "axios": "0.21.2", "camelcase": "^5.3.1", "classnames": "2.2.6", "common-tags": "1.8.0", diff --git a/src/components/__snapshots__/index.test.ts.snap b/src/components/__snapshots__/index.test.ts.snap index bc4d30462..b33cff215 100644 --- a/src/components/__snapshots__/index.test.ts.snap +++ b/src/components/__snapshots__/index.test.ts.snap @@ -23,6 +23,8 @@ Array [ "simulateStart": "", "templates": "/assets/templates.json", "ticketers": "/assets/ticketers.json", + "topics": "/assets/topics.json", + "users": "/assets/users.json", }, "a4f64f1b-85bc-477e-b706-de313a022979", undefined, diff --git a/src/components/flow/actions/openticket/OpenTicket.tsx b/src/components/flow/actions/openticket/OpenTicket.tsx index 03b51c209..343fdbc8a 100644 --- a/src/components/flow/actions/openticket/OpenTicket.tsx +++ b/src/components/flow/actions/openticket/OpenTicket.tsx @@ -3,13 +3,13 @@ import { OpenTicket } from 'flowTypes'; import { fakePropType } from 'config/ConfigProvider'; const OpenTicketComp: React.SFC = ( - { ticketer, subject }, + { ticketer, subject, topic }, context: any ): JSX.Element => { const showTicketer = ticketer.name.indexOf(context.config.brand) === -1; return (
-
{subject}
+
{subject ? subject : topic ? topic.name : null}
{showTicketer ? (
Using {ticketer.name} diff --git a/src/components/flow/routers/ticket/TicketRouterForm.test.tsx b/src/components/flow/routers/ticket/TicketRouterForm.test.tsx index 08bff9ecc..4e0295108 100644 --- a/src/components/flow/routers/ticket/TicketRouterForm.test.tsx +++ b/src/components/flow/routers/ticket/TicketRouterForm.test.tsx @@ -9,6 +9,7 @@ import { render, fireEvent, fireChangeText, fireTembaSelect } from 'test/utils'; mock(utils, 'createUUID', utils.seededUUIDs()); +// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion const ticketForm = getRouterFormProps({ node: createOpenTicketNode('Need help', 'Where are my cookies'), ui: { type: Types.split_by_ticket } @@ -30,17 +31,28 @@ describe(TicketRouterForm.name, () => { expect(baseElement).toMatchSnapshot(); const okButton = getByText('Ok'); - const subject = getByTestId('Subject'); const resultName = getByTestId('Result Name'); - // our ticketer, subject, body and result name are required - fireChangeText(subject, ''); + // our ticketer, body and result name are required fireChangeText(resultName, ''); fireEvent.click(okButton); expect(ticketForm.updateRouter).not.toBeCalled(); - // set our subject and result name - fireChangeText(subject, 'Need help'); + // we need a topic + fireTembaSelect(getByTestId('temba_select_assignee'), { + email: 'agent.user@gmail.com', + first_name: 'Agent', + last_name: 'User', + role: 'agent', + created_on: '2021-06-10T21:44:30.971221Z' + }); + + // we need a topic + fireTembaSelect(getByTestId('temba_select_topic'), { + name: 'General', + uuid: '6f38eba0-d673-4a35-82df-21bae2b6d466' + }); + fireChangeText(resultName, 'My Ticket Result'); fireEvent.click(okButton); diff --git a/src/components/flow/routers/ticket/TicketRouterForm.tsx b/src/components/flow/routers/ticket/TicketRouterForm.tsx index 32fa00c44..00f26d79d 100644 --- a/src/components/flow/routers/ticket/TicketRouterForm.tsx +++ b/src/components/flow/routers/ticket/TicketRouterForm.tsx @@ -19,8 +19,13 @@ import { Asset } from 'store/flowContext'; import styles from './TicketRouterForm.module.scss'; import i18n from 'config/i18n'; import TextInputElement from 'components/form/textinput/TextInputElement'; +import TembaSelect from 'temba/TembaSelect'; +import { fakePropType } from 'config/ConfigProvider'; +import { Topic, User } from 'flowTypes'; export interface TicketRouterFormState extends FormState { + assignee: FormEntry; + topic: FormEntry; ticketer: FormEntry; subject: StringEntry; body: StringEntry; @@ -31,6 +36,10 @@ export default class TicketRouterForm extends React.Component< RouterFormProps, TicketRouterFormState > { + public static contextTypes = { + config: fakePropType + }; + constructor(props: RouterFormProps) { super(props); @@ -45,6 +54,8 @@ export default class TicketRouterForm extends React.Component< } private handleUpdate( keys: { + assignee?: User; + topic?: Topic; ticketer?: Asset; subject?: string; body?: string; @@ -54,6 +65,18 @@ export default class TicketRouterForm extends React.Component< ): boolean { const updates: Partial = {}; + if (keys.hasOwnProperty('assignee')) { + updates.assignee = validate(i18n.t('forms.assignee', 'Assignee'), keys.assignee, [ + shouldRequireIf(submitting) + ]); + } + + if (keys.hasOwnProperty('topic')) { + updates.topic = validate(i18n.t('forms.topic', 'Topic'), keys.topic, [ + shouldRequireIf(submitting) + ]); + } + if (keys.hasOwnProperty('ticketer')) { updates.ticketer = validate(i18n.t('forms.ticketer', 'Ticketer'), keys.ticketer, [ shouldRequireIf(submitting) @@ -61,9 +84,7 @@ export default class TicketRouterForm extends React.Component< } if (keys.hasOwnProperty('subject')) { - updates.subject = validate(i18n.t('forms.subject', 'Subject'), keys.subject, [ - shouldRequireIf(submitting) - ]); + updates.subject = validate(i18n.t('forms.subject', 'Subject'), keys.subject, []); } if (keys.hasOwnProperty('body')) { @@ -89,6 +110,14 @@ export default class TicketRouterForm extends React.Component< this.handleUpdate({ ticketer: selected[0] }); } + private handleAssigneeUpdate(assignee: User): void { + this.handleUpdate({ assignee }); + } + + private handleTopicUpdate(topic: Topic): void { + this.handleUpdate({ topic }); + } + private handleSubjectUpdate(subject: string, name: string, submitting = false): boolean { return this.handleUpdate({ subject }, submitting); } @@ -165,14 +194,37 @@ export default class TicketRouterForm extends React.Component< '' )} -
- +
+
+ +
+ +
+ { + if (!user.first_name && !user.last_name) { + return user.email || ''; + } + return `${user.first_name} ${user.last_name}`; + }} + /> +
- +
+
+
+
+
@@ -193,19 +218,44 @@ exports[`TicketRouterForm updates should save changes 1`] = `
- +
+
+
+
+
@@ -293,13 +343,27 @@ Array [ "node": Object { "actions": Array [ Object { + "assignee": Object { + "value": Object { + "created_on": "2021-06-10T21:44:30.971221Z", + "email": "agent.user@gmail.com", + "first_name": "Agent", + "last_name": "User", + "role": "agent", + }, + }, "body": "Where are my cookies", "result_name": "My Ticket Result", - "subject": "Need help", "ticketer": Object { "name": "Email (bob@acme.com)", "uuid": "1165a73a-2ee0-4891-895e-768645194862", }, + "topic": Object { + "value": Object { + "name": "General", + "uuid": "6f38eba0-d673-4a35-82df-21bae2b6d466", + }, + }, "type": "open_ticket", "uuid": "b1f332f3-bdd3-4891-aec5-1843a712dbf1", }, diff --git a/src/components/flow/routers/ticket/helpers.ts b/src/components/flow/routers/ticket/helpers.ts index cbff66fb6..9ada50bfe 100644 --- a/src/components/flow/routers/ticket/helpers.ts +++ b/src/components/flow/routers/ticket/helpers.ts @@ -27,16 +27,22 @@ export const nodeToState = ( let subject = { value: '@run.flow.name' }; let body = { value: '@results' }; let resultName = { value: 'Result' }; + let assignee: FormEntry = { value: null }; + let topic: FormEntry = { value: null }; if (getType(settings.originalNode) === Types.split_by_ticket) { const action = getOriginalAction(settings) as OpenTicket; ticketer = { value: action.ticketer }; subject = { value: action.subject }; body = { value: action.body }; + topic = { value: action.topic }; + assignee = { value: action.assignee }; resultName = { value: action.result_name }; } const state: TicketRouterFormState = { + assignee, + topic, ticketer, subject, body, @@ -64,8 +70,9 @@ export const stateToNode = ( uuid: state.ticketer.value.uuid, name: state.ticketer.value.name }, - subject: state.subject.value, body: state.body.value, + topic: state.topic.value, + assignee: state.assignee.value, result_name: state.resultName.value }; diff --git a/src/components/form/assetselector/AssetSelector.tsx b/src/components/form/assetselector/AssetSelector.tsx index fa7a8bac7..f137c4e01 100644 --- a/src/components/form/assetselector/AssetSelector.tsx +++ b/src/components/form/assetselector/AssetSelector.tsx @@ -151,6 +151,10 @@ export default class AssetSelector extends React.Component { testEventRender({ type: 'ticket_opened', ticketer: { uuid: '15892014-144c-4721-a611-c80b38481055', name: 'Email Support' }, - ticket: { subject: 'Need help', body: 'Where are my cookies?' }, + ticket: { + topic: { uuid: 'ee1e36a8-dd42-4464-b6b2-37418a26db1f', name: 'Support' }, + body: 'Where are my cookies?' + }, result_name: 'Ticket', ...commonEventProps }); diff --git a/src/components/simulator/LogEvent.tsx b/src/components/simulator/LogEvent.tsx index a14463674..42f14d46c 100644 --- a/src/components/simulator/LogEvent.tsx +++ b/src/components/simulator/LogEvent.tsx @@ -3,7 +3,7 @@ import { MediaPlayer } from 'components/mediaplayer/MediaPlayer'; import Modal from 'components/modal/Modal'; import styles from 'components/simulator/LogEvent.module.scss'; import { Types } from 'config/interfaces'; -import { Flow, Group, Label } from 'flowTypes'; +import { Flow, Group, Label, Topic } from 'flowTypes'; import * as React from 'react'; import { createUUID, getURNPath } from 'utils'; import i18n from 'config/i18n'; @@ -71,7 +71,7 @@ export interface EventProps { service?: string; classifier?: { uuid: string; name: string }; ticketer?: { uuid: string; name: string }; - ticket?: { subject: string; body: string }; + ticket?: { topic: Topic; body: string }; } interface FlowEvent { @@ -465,8 +465,8 @@ export default class LogEvent extends React.Component return null; case 'ticket_opened': return renderInfo( - i18n.t('simulator.ticket_opened', 'Ticket opened with subject "[[subject]]"', { - subject: this.props.ticket.subject + i18n.t('simulator.ticket_opened', 'Ticket opened with topic "[[topic]]"', { + topic: this.props.ticket.topic.name }) ); case 'airtime_transferred': diff --git a/src/components/simulator/__snapshots__/LogEvent.test.tsx.snap b/src/components/simulator/__snapshots__/LogEvent.test.tsx.snap index a29d1a6a8..b7c6dada9 100644 --- a/src/components/simulator/__snapshots__/LogEvent.test.tsx.snap +++ b/src/components/simulator/__snapshots__/LogEvent.test.tsx.snap @@ -397,7 +397,7 @@ exports[`LogEvent should render ticket_opened event 1`] = ` class="info" > - Ticket opened with subject "Need help" + Ticket opened with topic "Support"
diff --git a/src/config/i18n/defaults.json b/src/config/i18n/defaults.json index 1597eab0e..0d2644db0 100644 --- a/src/config/i18n/defaults.json +++ b/src/config/i18n/defaults.json @@ -491,7 +491,7 @@ "title": "Email Details" }, "session_triggered": "Started somebody else in \"[[flow]]\"", - "ticket_opened": "Ticket opened with subject \"[[subject]]\"" + "ticket_opened": "Ticket opened with topic \"[[topic]]\"" }, "sticky": { "body": "...", @@ -501,4 +501,4 @@ "header": "Flow Translation", "label": "Translations" } -} +} \ No newline at end of file diff --git a/src/config/i18n/es/resource.json b/src/config/i18n/es/resource.json index bf1e08b9d..2b5847b1f 100644 --- a/src/config/i18n/es/resource.json +++ b/src/config/i18n/es/resource.json @@ -491,7 +491,7 @@ "title": "Detalles de Correo Electrónico" }, "session_triggered": "Empezada a otra persona en \"[[flow]]\"", - "ticket_opened": "Ticket abierto con asunto \"[[subject]]\"" + "ticket_opened": "Ticket abierto con tema \"[[topic]]\"" }, "sticky": { "body": "...", @@ -501,4 +501,4 @@ "header": "Traducción de Flujo", "label": "Traducciones" } -} +} \ No newline at end of file diff --git a/src/config/i18n/fr/resource.json b/src/config/i18n/fr/resource.json index fa6bb43cd..5a8620f4b 100644 --- a/src/config/i18n/fr/resource.json +++ b/src/config/i18n/fr/resource.json @@ -491,7 +491,7 @@ "title": "Détails de l'e-mail" }, "session_triggered": "A commencé quelqu'un d'autre dans \"[[flow]]\"", - "ticket_opened": "Billet ouvert avec le sujet \"[[subject]]\"" + "ticket_opened": "Billet ouvert avec le thème \"[[topic]]\"" }, "sticky": { "body": "...", @@ -501,4 +501,4 @@ "header": "Traduction de flux", "label": "Traductions" } -} +} \ No newline at end of file diff --git a/src/config/i18n/pt-br/resource.json b/src/config/i18n/pt-br/resource.json index 6185f091f..f37e61c31 100644 --- a/src/config/i18n/pt-br/resource.json +++ b/src/config/i18n/pt-br/resource.json @@ -494,7 +494,7 @@ "title": "Detalhes do Email" }, "session_triggered": "Iniciou outra pessoa em \"[[flow]]\"", - "ticket_opened": "Ticket aberto com assunto \"[[subject]]\"" + "ticket_opened": "Ticket aberto com tema \"[[topic]]\"" }, "sticky": { "body": "...", @@ -504,4 +504,4 @@ "header": "Tradução do fluxo", "label": "Traduções" } -} +} \ No newline at end of file diff --git a/src/flowTypes.ts b/src/flowTypes.ts index 374b14cd7..12bb6be34 100644 --- a/src/flowTypes.ts +++ b/src/flowTypes.ts @@ -35,6 +35,8 @@ export interface Endpoints { channels: string; classifiers: string; ticketers: string; + users: string; + topics: string; environment: string; languages: string; templates: string; @@ -114,6 +116,20 @@ export enum FlowIssueType { INVALID_REGEX = 'invalid_regex' } +export interface User { + email?: string; + first_name?: string; + last_name?: string; + role?: string; + created_on?: string; +} + +export interface Topic { + uuid: string; + name: string; + created_on?: string; +} + export interface FlowIssue { type: FlowIssueType; node_uuid: string; @@ -424,9 +440,11 @@ export interface CallWebhook extends Action { export interface OpenTicket extends Action { ticketer: Ticketer; - subject: string; + subject?: string; + topic?: Topic; body: string; result_name: string; + assignee?: User; } export interface StartFlow extends Action { diff --git a/src/store/validators.ts b/src/store/validators.ts index c6ebf2d9c..c5bc586a4 100644 --- a/src/store/validators.ts +++ b/src/store/validators.ts @@ -3,6 +3,7 @@ import { Asset } from 'store/flowContext'; import { FormEntry, ValidationFailure } from 'store/nodeEditor'; import { SelectOption } from 'components/form/select/SelectElement'; import i18n from 'config/i18n'; +import { Topic, User } from 'flowTypes'; export type FormInput = | string @@ -11,7 +12,9 @@ export type FormInput = | Asset | Asset[] | SelectOption - | SelectOption[]; + | SelectOption[] + | User + | Topic; export type ValidatorFunc = ( name: string, input: FormInput diff --git a/src/temba/TembaSelect.tsx b/src/temba/TembaSelect.tsx index 6c6017985..8e9ab6b7b 100644 --- a/src/temba/TembaSelect.tsx +++ b/src/temba/TembaSelect.tsx @@ -23,6 +23,7 @@ export interface TembaSelectProps { assets?: Assets; errors?: string[]; style?: TembaSelectStyle; + endpoint?: string; placeholder?: string; searchable?: boolean; @@ -33,6 +34,8 @@ export interface TembaSelectProps { getName?: (option: any) => string; + createArbitraryOption?: (input: string) => any; + nameKey?: string; valueKey?: string; @@ -45,6 +48,7 @@ export interface TembaSelectProps { queryParam?: string; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface interface TembaSelectState {} export default class TembaSelect extends React.Component { @@ -113,10 +117,15 @@ export default class TembaSelect extends React.Component { const result = (this.props.options || []).find( (option: any) => this.getValue(option) === this.getValue(op) @@ -207,7 +216,7 @@ export default class TembaSelect extends React.Component