From db4d711cba85fe70aa71dd7034e32d10c7243564 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 29 Jul 2022 16:15:57 +0530 Subject: [PATCH 1/8] feat: add subject to email template --- server/db/models/email_templates.go | 2 + .../providers/cassandradb/email_template.go | 14 ++-- server/db/providers/cassandradb/provider.go | 6 ++ server/graph/generated/generated.go | 67 +++++++++++++++++++ server/graph/model/models_gen.go | 3 + server/graph/schema.graphqls | 3 + server/resolvers/add_email_template.go | 5 ++ server/resolvers/update_email_template.go | 8 +++ server/test/add_email_template_test.go | 15 ++++- server/test/update_email_template_test.go | 2 + 10 files changed, 117 insertions(+), 8 deletions(-) diff --git a/server/db/models/email_templates.go b/server/db/models/email_templates.go index 23ac7fa..8c6de30 100644 --- a/server/db/models/email_templates.go +++ b/server/db/models/email_templates.go @@ -12,6 +12,7 @@ type EmailTemplate struct { Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"` EventName string `gorm:"unique" json:"event_name" bson:"event_name" cql:"event_name"` + Subject string `gorm:"type:text" json:"subject" bson:"subject" cql:"subject"` Template string `gorm:"type:text" json:"template" bson:"template" cql:"template"` CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` @@ -26,6 +27,7 @@ func (e *EmailTemplate) AsAPIEmailTemplate() *model.EmailTemplate { return &model.EmailTemplate{ ID: id, EventName: e.EventName, + Subject: e.Subject, Template: e.Template, CreatedAt: refs.NewInt64Ref(e.CreatedAt), UpdatedAt: refs.NewInt64Ref(e.UpdatedAt), diff --git a/server/db/providers/cassandradb/email_template.go b/server/db/providers/cassandradb/email_template.go index 4fa9109..ea18fce 100644 --- a/server/db/providers/cassandradb/email_template.go +++ b/server/db/providers/cassandradb/email_template.go @@ -29,7 +29,7 @@ func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.Em return nil, fmt.Errorf("Email template with %s event_name already exists", emailTemplate.EventName) } - insertQuery := fmt.Sprintf("INSERT INTO %s (id, event_name, template, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.EmailTemplate, emailTemplate.ID, emailTemplate.EventName, emailTemplate.Template, emailTemplate.CreatedAt, emailTemplate.UpdatedAt) + insertQuery := fmt.Sprintf("INSERT INTO %s (id, event_name, subject, template, created_at, updated_at) VALUES ('%s', '%s', '%s','%s', %d, %d)", KeySpace+"."+models.Collections.EmailTemplate, emailTemplate.ID, emailTemplate.EventName, emailTemplate.Subject, emailTemplate.Template, emailTemplate.CreatedAt, emailTemplate.UpdatedAt) err := p.db.Query(insertQuery).Exec() if err != nil { return nil, err @@ -103,14 +103,14 @@ func (p *provider) ListEmailTemplate(ctx context.Context, pagination model.Pagin // there is no offset in cassandra // so we fetch till limit + offset // and return the results from offset to limit - query := fmt.Sprintf("SELECT id, event_name, template, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.EmailTemplate, pagination.Limit+pagination.Offset) + query := fmt.Sprintf("SELECT id, event_name, subject, template, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.EmailTemplate, pagination.Limit+pagination.Offset) scanner := p.db.Query(query).Iter().Scanner() counter := int64(0) for scanner.Next() { if counter >= pagination.Offset { var emailTemplate models.EmailTemplate - err := scanner.Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) + err := scanner.Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) if err != nil { return nil, err } @@ -128,8 +128,8 @@ func (p *provider) ListEmailTemplate(ctx context.Context, pagination model.Pagin // GetEmailTemplateByID to get EmailTemplate by id func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID string) (*model.EmailTemplate, error) { var emailTemplate models.EmailTemplate - query := fmt.Sprintf(`SELECT id, event_name, template, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1`, KeySpace+"."+models.Collections.EmailTemplate, emailTemplateID) - err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) + query := fmt.Sprintf(`SELECT id, event_name, subject, template, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1`, KeySpace+"."+models.Collections.EmailTemplate, emailTemplateID) + err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) if err != nil { return nil, err } @@ -139,8 +139,8 @@ func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID str // GetEmailTemplateByEventName to get EmailTemplate by event_name func (p *provider) GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) { var emailTemplate models.EmailTemplate - query := fmt.Sprintf(`SELECT id, event_name, template, created_at, updated_at FROM %s WHERE event_name = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.EmailTemplate, eventName) - err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) + query := fmt.Sprintf(`SELECT id, event_name, subject, template, created_at, updated_at FROM %s WHERE event_name = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.EmailTemplate, eventName) + err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) if err != nil { return nil, err } diff --git a/server/db/providers/cassandradb/provider.go b/server/db/providers/cassandradb/provider.go index e5a0469..08b2a26 100644 --- a/server/db/providers/cassandradb/provider.go +++ b/server/db/providers/cassandradb/provider.go @@ -214,6 +214,12 @@ func NewProvider() (*provider, error) { if err != nil { return nil, err } + // add subject on email_templates table + emailTemplateAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD subject text;`, KeySpace, models.Collections.EmailTemplate) + err = session.Query(emailTemplateAlterQuery).Exec() + if err != nil { + return nil, err + } return &provider{ db: session, diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 6b159e4..7acb942 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -56,6 +56,7 @@ type ComplexityRoot struct { CreatedAt func(childComplexity int) int EventName func(childComplexity int) int ID func(childComplexity int) int + Subject func(childComplexity int) int Template func(childComplexity int) int UpdatedAt func(childComplexity int) int } @@ -403,6 +404,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.EmailTemplate.ID(childComplexity), true + case "EmailTemplate.subject": + if e.complexity.EmailTemplate.Subject == nil { + break + } + + return e.complexity.EmailTemplate.Subject(childComplexity), true + case "EmailTemplate.template": if e.complexity.EmailTemplate.Template == nil { break @@ -1978,6 +1986,7 @@ type EmailTemplate { id: ID! event_name: String! template: String! + subject: String! created_at: Int64 updated_at: Int64 } @@ -2193,6 +2202,7 @@ input TestEndpointRequest { input AddEmailTemplateRequest { event_name: String! + subject: String! template: String! } @@ -2200,6 +2210,7 @@ input UpdateEmailTemplateRequest { id: ID! event_name: String template: String + subject: String } input DeleteEmailTemplateRequest { @@ -3108,6 +3119,41 @@ func (ec *executionContext) _EmailTemplate_template(ctx context.Context, field g return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _EmailTemplate_subject(ctx context.Context, field graphql.CollectedField, obj *model.EmailTemplate) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EmailTemplate", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Subject, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) _EmailTemplate_created_at(ctx context.Context, field graphql.CollectedField, obj *model.EmailTemplate) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -10087,6 +10133,14 @@ func (ec *executionContext) unmarshalInputAddEmailTemplateRequest(ctx context.Co if err != nil { return it, err } + case "subject": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("subject")) + it.Subject, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } case "template": var err error @@ -10866,6 +10920,14 @@ func (ec *executionContext) unmarshalInputUpdateEmailTemplateRequest(ctx context if err != nil { return it, err } + case "subject": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("subject")) + it.Subject, err = ec.unmarshalOString2áš–string(ctx, v) + if err != nil { + return it, err + } } } @@ -11632,6 +11694,11 @@ func (ec *executionContext) _EmailTemplate(ctx context.Context, sel ast.Selectio if out.Values[i] == graphql.Null { invalids++ } + case "subject": + out.Values[i] = ec._EmailTemplate_subject(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "created_at": out.Values[i] = ec._EmailTemplate_created_at(ctx, field, obj) case "updated_at": diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 70762dc..6d7f319 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -4,6 +4,7 @@ package model type AddEmailTemplateRequest struct { EventName string `json:"event_name"` + Subject string `json:"subject"` Template string `json:"template"` } @@ -43,6 +44,7 @@ type EmailTemplate struct { ID string `json:"id"` EventName string `json:"event_name"` Template string `json:"template"` + Subject string `json:"subject"` CreatedAt *int64 `json:"created_at"` UpdatedAt *int64 `json:"updated_at"` } @@ -240,6 +242,7 @@ type UpdateEmailTemplateRequest struct { ID string `json:"id"` EventName *string `json:"event_name"` Template *string `json:"template"` + Subject *string `json:"subject"` } type UpdateEnvInput struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index ca0e7df..e163379 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -189,6 +189,7 @@ type EmailTemplate { id: ID! event_name: String! template: String! + subject: String! created_at: Int64 updated_at: Int64 } @@ -404,6 +405,7 @@ input TestEndpointRequest { input AddEmailTemplateRequest { event_name: String! + subject: String! template: String! } @@ -411,6 +413,7 @@ input UpdateEmailTemplateRequest { id: ID! event_name: String template: String + subject: String } input DeleteEmailTemplateRequest { diff --git a/server/resolvers/add_email_template.go b/server/resolvers/add_email_template.go index 1cba02f..092d017 100644 --- a/server/resolvers/add_email_template.go +++ b/server/resolvers/add_email_template.go @@ -34,6 +34,10 @@ func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplate return nil, fmt.Errorf("invalid event name %s", params.EventName) } + if strings.TrimSpace(params.Subject) == "" { + return nil, fmt.Errorf("empty subject not allowed") + } + if strings.TrimSpace(params.Template) == "" { return nil, fmt.Errorf("empty template not allowed") } @@ -41,6 +45,7 @@ func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplate _, err = db.Provider.AddEmailTemplate(ctx, models.EmailTemplate{ EventName: params.EventName, Template: params.Template, + Subject: params.Subject, }) if err != nil { log.Debug("Failed to add email template: ", err) diff --git a/server/resolvers/update_email_template.go b/server/resolvers/update_email_template.go index 95362c0..5eab5aa 100644 --- a/server/resolvers/update_email_template.go +++ b/server/resolvers/update_email_template.go @@ -51,6 +51,14 @@ func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTe emailTemplateDetails.EventName = refs.StringValue(params.EventName) } + if params.Subject != nil && emailTemplateDetails.Subject != refs.StringValue(params.Subject) { + if strings.TrimSpace(refs.StringValue(params.Subject)) == "" { + log.Debug("empty subject not allowed") + return nil, fmt.Errorf("empty subject not allowed") + } + emailTemplateDetails.Subject = refs.StringValue(params.Subject) + } + if params.Template != nil && emailTemplateDetails.Template != refs.StringValue(params.Template) { if strings.TrimSpace(refs.StringValue(params.Template)) == "" { log.Debug("empty template not allowed") diff --git a/server/test/add_email_template_test.go b/server/test/add_email_template_test.go index 743ed12..40cf94e 100644 --- a/server/test/add_email_template_test.go +++ b/server/test/add_email_template_test.go @@ -31,10 +31,21 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) { assert.Nil(t, emailTemplate) }) + t.Run("should not add email template for empty template", func(t *testing.T) { + emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ + EventName: s.TestInfo.TestEmailTemplateEventTypes[0], + Template: " test ", + Subject: " ", + }) + assert.Error(t, err) + assert.Nil(t, emailTemplate) + }) + t.Run("should not add email template for empty template", func(t *testing.T) { emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ EventName: s.TestInfo.TestEmailTemplateEventTypes[0], Template: " ", + Subject: "test", }) assert.Error(t, err) assert.Nil(t, emailTemplate) @@ -43,7 +54,8 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) { t.Run("should add email template for "+eventType, func(t *testing.T) { emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ EventName: eventType, - Template: `Test email`, + Template: "Test email", + Subject: "Test email", }) assert.NoError(t, err) assert.NotNil(t, emailTemplate) @@ -52,6 +64,7 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) { et, err := db.Provider.GetEmailTemplateByEventName(ctx, eventType) assert.NoError(t, err) assert.Equal(t, et.EventName, eventType) + assert.Equal(t, "Test email", et.Subject) }) } }) diff --git a/server/test/update_email_template_test.go b/server/test/update_email_template_test.go index 1f23ff4..4f2a999 100644 --- a/server/test/update_email_template_test.go +++ b/server/test/update_email_template_test.go @@ -31,6 +31,7 @@ func updateEmailTemplateTest(t *testing.T, s TestSetup) { res, err := resolvers.UpdateEmailTemplateResolver(ctx, model.UpdateEmailTemplateRequest{ ID: emailTemplate.ID, Template: refs.NewStringRef("Updated test template"), + Subject: refs.NewStringRef("Updated subject"), }) assert.NoError(t, err) @@ -42,5 +43,6 @@ func updateEmailTemplateTest(t *testing.T, s TestSetup) { assert.NotNil(t, updatedEmailTemplate) assert.Equal(t, emailTemplate.ID, updatedEmailTemplate.ID) assert.Equal(t, updatedEmailTemplate.Template, "Updated test template") + assert.Equal(t, updatedEmailTemplate.Subject, "Updated subject") }) } From f2fb800323eeda9b643b514426c2f4ca21e7468c Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Sat, 30 Jul 2022 16:05:35 +0530 Subject: [PATCH 2/8] feat: dashboard add email-template page --- dashboard/package-lock.json | 12 +- dashboard/src/components/Menu.tsx | 2 + .../components/UpdateEmailTemplateModal.tsx | 327 +++++++++++++++++ .../src/components/UpdateWebhookModal.tsx | 17 +- dashboard/src/constants.ts | 18 +- dashboard/src/graphql/mutation/index.ts | 24 ++ dashboard/src/graphql/queries/index.ts | 20 ++ dashboard/src/pages/EmailTemplates.tsx | 335 ++++++++++++++++++ dashboard/src/pages/Webhooks.tsx | 7 +- dashboard/src/routes/index.tsx | 2 + 10 files changed, 741 insertions(+), 23 deletions(-) create mode 100644 dashboard/src/components/UpdateEmailTemplateModal.tsx create mode 100644 dashboard/src/pages/EmailTemplates.tsx diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c04cac0..41d31f9 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -2529,8 +2529,7 @@ "@chakra-ui/css-reset": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", - "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", - "requires": {} + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==" }, "@chakra-ui/descendant": { "version": "2.1.1", @@ -3134,8 +3133,7 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "requires": {} + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" }, "@popperjs/core": { "version": "2.11.0", @@ -3845,8 +3843,7 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", - "requires": {} + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" }, "react-is": { "version": "16.13.1", @@ -4032,8 +4029,7 @@ "use-callback-ref": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", - "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", - "requires": {} + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==" }, "use-sidecar": { "version": "1.0.5", diff --git a/dashboard/src/components/Menu.tsx b/dashboard/src/components/Menu.tsx index e424a0f..7e59043 100644 --- a/dashboard/src/components/Menu.tsx +++ b/dashboard/src/components/Menu.tsx @@ -31,6 +31,7 @@ import { FiUsers, FiChevronDown, FiLink, + FiFileText, } from 'react-icons/fi'; import { BiCustomize } from 'react-icons/bi'; import { AiOutlineKey } from 'react-icons/ai'; @@ -113,6 +114,7 @@ const LinkItems: Array = [ }, { name: 'Users', icon: FiUsers, route: '/users' }, { name: 'Webhooks', icon: FiLink, route: '/webhooks' }, + { name: 'Email Templates', icon: FiFileText, route: '/email-templates' }, ]; interface SidebarProps extends BoxProps { diff --git a/dashboard/src/components/UpdateEmailTemplateModal.tsx b/dashboard/src/components/UpdateEmailTemplateModal.tsx new file mode 100644 index 0000000..0d7ff05 --- /dev/null +++ b/dashboard/src/components/UpdateEmailTemplateModal.tsx @@ -0,0 +1,327 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Center, + Flex, + Input, + InputGroup, + MenuItem, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Select, + useDisclosure, + useToast, +} from '@chakra-ui/react'; +import { FaPlus } from 'react-icons/fa'; +import { useClient } from 'urql'; +import { + UpdateModalViews, + EmailTemplateInputDataFields, + emailTemplateEventNames, +} from '../constants'; +import { capitalizeFirstLetter } from '../utils'; +import { AddEmailTemplate, EditEmailTemplate } from '../graphql/mutation'; + +interface selectedEmailTemplateDataTypes { + [EmailTemplateInputDataFields.ID]: string; + [EmailTemplateInputDataFields.EVENT_NAME]: string; + [EmailTemplateInputDataFields.SUBJECT]: string; + [EmailTemplateInputDataFields.CREATED_AT]: number; + [EmailTemplateInputDataFields.TEMPLATE]: string; +} + +interface UpdateEmailTemplateInputPropTypes { + view: UpdateModalViews; + selectedTemplate?: selectedEmailTemplateDataTypes; + fetchEmailTemplatesData: Function; +} + +interface emailTemplateDataType { + [EmailTemplateInputDataFields.EVENT_NAME]: string; + [EmailTemplateInputDataFields.SUBJECT]: string; + [EmailTemplateInputDataFields.TEMPLATE]: string; +} + +interface validatorDataType { + [EmailTemplateInputDataFields.SUBJECT]: boolean; + [EmailTemplateInputDataFields.TEMPLATE]: boolean; +} + +const initTemplateData: emailTemplateDataType = { + [EmailTemplateInputDataFields.EVENT_NAME]: + emailTemplateEventNames.BASIC_AUTH_SIGNUP, + [EmailTemplateInputDataFields.SUBJECT]: '', + [EmailTemplateInputDataFields.TEMPLATE]: '', +}; + +const initTemplateValidatorData: validatorDataType = { + [EmailTemplateInputDataFields.SUBJECT]: true, + [EmailTemplateInputDataFields.TEMPLATE]: true, +}; + +const UpdateEmailTemplate = ({ + view, + selectedTemplate, + fetchEmailTemplatesData, +}: UpdateEmailTemplateInputPropTypes) => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [loading, setLoading] = useState(false); + const [templateData, setTemplateData] = useState({ + ...initTemplateData, + }); + const [validator, setValidator] = useState({ + ...initTemplateValidatorData, + }); + const inputChangehandler = (inputType: string, value: any) => { + if (inputType !== EmailTemplateInputDataFields.EVENT_NAME) { + setValidator({ + ...validator, + [inputType]: value?.trim().length, + }); + } + setTemplateData({ ...templateData, [inputType]: value }); + }; + + const validateData = () => { + return ( + !loading && + templateData[EmailTemplateInputDataFields.EVENT_NAME].length > 0 && + templateData[EmailTemplateInputDataFields.TEMPLATE].length > 0 && + templateData[EmailTemplateInputDataFields.SUBJECT].length > 0 && + validator[EmailTemplateInputDataFields.TEMPLATE] && + validator[EmailTemplateInputDataFields.SUBJECT] + ); + }; + + const saveData = async () => { + if (!validateData()) return; + setLoading(true); + const params = { + [EmailTemplateInputDataFields.EVENT_NAME]: + templateData[EmailTemplateInputDataFields.EVENT_NAME], + [EmailTemplateInputDataFields.SUBJECT]: + templateData[EmailTemplateInputDataFields.SUBJECT], + [EmailTemplateInputDataFields.TEMPLATE]: + templateData[EmailTemplateInputDataFields.TEMPLATE], + }; + let res: any = {}; + if ( + view === UpdateModalViews.Edit && + selectedTemplate?.[EmailTemplateInputDataFields.ID] + ) { + res = await client + .mutation(EditEmailTemplate, { + params: { + ...params, + id: selectedTemplate[EmailTemplateInputDataFields.ID], + }, + }) + .toPromise(); + } else { + res = await client.mutation(AddEmailTemplate, { params }).toPromise(); + } + setLoading(false); + if (res.error) { + toast({ + title: capitalizeFirstLetter(res.error.message), + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else if ( + res.data?._add_email_template || + res.data?._update_email_template + ) { + toast({ + title: capitalizeFirstLetter( + res.data?._add_email_template?.message || + res.data?._update_email_template?.message + ), + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + setTemplateData({ + ...initTemplateData, + }); + setValidator({ ...initTemplateValidatorData }); + fetchEmailTemplatesData(); + } + view === UpdateModalViews.ADD && onClose(); + }; + const resetData = () => { + if (selectedTemplate) { + setTemplateData(selectedTemplate); + } else { + setTemplateData({ ...initTemplateData }); + } + }; + useEffect(() => { + if ( + isOpen && + view === UpdateModalViews.Edit && + selectedTemplate && + Object.keys(selectedTemplate || {}).length + ) { + const { id, created_at, ...rest } = selectedTemplate; + setTemplateData(rest); + } + }, [isOpen]); + return ( + <> + {view === UpdateModalViews.ADD ? ( + + ) : ( + Edit + )} + + + + + {view === UpdateModalViews.ADD + ? 'Add New Email Template' + : 'Edit Email Template'} + + + + + + Event Name + + + + + + Subject + + + + inputChangehandler( + EmailTemplateInputDataFields.SUBJECT, + e.currentTarget.value + ) + } + /> + + + + + Template Body + + + + + + inputChangehandler( + EmailTemplateInputDataFields.TEMPLATE, + e.currentTarget.value + ) + } + /> + + + + + + + + + + + + + ); +}; + +export default UpdateEmailTemplate; diff --git a/dashboard/src/components/UpdateWebhookModal.tsx b/dashboard/src/components/UpdateWebhookModal.tsx index 60590f2..d399c62 100644 --- a/dashboard/src/components/UpdateWebhookModal.tsx +++ b/dashboard/src/components/UpdateWebhookModal.tsx @@ -27,12 +27,11 @@ import { ArrayInputOperations, WebhookInputDataFields, WebhookInputHeaderFields, - UpdateWebhookModalViews, + UpdateModalViews, webhookVerifiedStatus, } from '../constants'; import { capitalizeFirstLetter, validateURI } from '../utils'; import { AddWebhook, EditWebhook, TestEndpoint } from '../graphql/mutation'; -import { rest } from 'lodash'; import { BiCheckCircle, BiError, BiErrorCircle } from 'react-icons/bi'; interface headersDataType { @@ -54,7 +53,7 @@ interface selecetdWebhookDataTypes { } interface UpdateWebhookModalInputPropTypes { - view: UpdateWebhookModalViews; + view: UpdateModalViews; selectedWebhook?: selecetdWebhookDataTypes; fetchWebookData: Function; } @@ -254,7 +253,7 @@ const UpdateWebhookModal = ({ const params = getParams(); let res: any = {}; if ( - view === UpdateWebhookModalViews.Edit && + view === UpdateModalViews.Edit && selectedWebhook?.[WebhookInputDataFields.ID] ) { res = await client @@ -292,12 +291,12 @@ const UpdateWebhookModal = ({ setValidator({ ...initWebhookValidatorData }); fetchWebookData(); } - view === UpdateWebhookModalViews.ADD && onClose(); + view === UpdateModalViews.ADD && onClose(); }; useEffect(() => { if ( isOpen && - view === UpdateWebhookModalViews.Edit && + view === UpdateModalViews.Edit && selectedWebhook && Object.keys(selectedWebhook || {}).length ) { @@ -347,7 +346,7 @@ const UpdateWebhookModal = ({ }; return ( <> - {view === UpdateWebhookModalViews.ADD ? ( + {view === UpdateModalViews.ADD ? ( + + + + + ); +}; + +export default DeleteEmailTemplateModal; diff --git a/dashboard/src/pages/EmailTemplates.tsx b/dashboard/src/pages/EmailTemplates.tsx index 6b2990f..9058003 100644 --- a/dashboard/src/pages/EmailTemplates.tsx +++ b/dashboard/src/pages/EmailTemplates.tsx @@ -42,6 +42,7 @@ import { } from '../constants'; import { EmailTemplatesQuery, WebhooksDataQuery } from '../graphql/queries'; import dayjs from 'dayjs'; +import DeleteEmailTemplateModal from '../components/DeleteEmailTemplateModal'; interface paginationPropTypes { limit: number; @@ -174,6 +175,17 @@ const EmailTemplates = () => { selectedTemplate={templateData} fetchEmailTemplatesData={fetchEmailTemplatesData} /> + From b4ef196bfbe8d5652bc8a95ad4ce0a91c35df18e Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Mon, 1 Aug 2022 14:07:06 +0530 Subject: [PATCH 7/8] fix: update email template variables --- .../components/UpdateEmailTemplateModal.tsx | 48 +++++++++++++------ dashboard/src/constants.ts | 24 ++++++++-- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/dashboard/src/components/UpdateEmailTemplateModal.tsx b/dashboard/src/components/UpdateEmailTemplateModal.tsx index 53895c2..5deee15 100644 --- a/dashboard/src/components/UpdateEmailTemplateModal.tsx +++ b/dashboard/src/components/UpdateEmailTemplateModal.tsx @@ -28,6 +28,7 @@ import { UpdateModalViews, EmailTemplateInputDataFields, emailTemplateEventNames, + emailTemplateVariables, } from '../constants'; import { capitalizeFirstLetter } from '../utils'; import { AddEmailTemplate, EditEmailTemplate } from '../graphql/mutation'; @@ -46,6 +47,11 @@ interface UpdateEmailTemplateInputPropTypes { fetchEmailTemplatesData: Function; } +interface templateVariableDataTypes { + text: string; + value: string; +} + interface emailTemplateDataType { [EmailTemplateInputDataFields.EVENT_NAME]: string; [EmailTemplateInputDataFields.SUBJECT]: string; @@ -77,6 +83,9 @@ const UpdateEmailTemplate = ({ const [editorState, setEditorState] = React.useState( EditorState.createEmpty() ); + const [templateVariables, setTemplateVariables] = useState< + templateVariableDataTypes[] + >([]); const [templateData, setTemplateData] = useState({ ...initTemplateData, }); @@ -191,6 +200,30 @@ const UpdateEmailTemplate = ({ setEditorState(EditorState.createWithContent(stateFromHTML(template))); } }, [isOpen]); + useEffect(() => { + const updatedTemplateVariables = Object.entries( + emailTemplateVariables + ).reduce((acc, varData): any => { + if ( + (templateData[EmailTemplateInputDataFields.EVENT_NAME] !== + emailTemplateEventNames.VERIFY_OTP && + varData[1] === emailTemplateVariables.otp) || + (templateData[EmailTemplateInputDataFields.EVENT_NAME] === + emailTemplateEventNames.VERIFY_OTP && + varData[1] === emailTemplateVariables.verification_url) + ) { + return acc; + } + return [ + ...acc, + { + text: varData[0], + value: varData[1], + }, + ]; + }, []); + setTemplateVariables(updatedTemplateVariables); + }, [templateData[EmailTemplateInputDataFields.EVENT_NAME]]); return ( <> {view === UpdateModalViews.ADD ? ( @@ -307,20 +340,7 @@ const UpdateEmailTemplate = ({ mention={{ separator: ' ', trigger: '{', - suggestions: [ - { - text: 'user_name', - value: '{user.name}}', - }, - { - text: 'user_email', - value: '{user.email}}', - }, - { - text: 'org_name', - value: '{org.name}}', - }, - ], + suggestions: templateVariables, }} /> diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 2ae573d..53ca46b 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -206,7 +206,25 @@ export enum webhookVerifiedStatus { } export const emailTemplateVariables = { - user_name: '{{user.name}}', - user_email: '{{user.email}}', - organization_name: '{{org.name}}', + 'user.id': '{user.id}}', + 'user.email': '{user.email}}', + 'user.given_name': '{user.given_name}}', + 'user.family_name': '{user.family_name}}', + 'user.signup_methods': '{user.signup_methods}}', + 'user.email_verified': '{user.email_verified}}', + 'user.picture': '{user.picture}}', + 'user.roles': '{user.roles}}', + 'user.middle_name': '{user.middle_name}}', + 'user.nickname': '{user.nickname}}', + 'user.preferred_username': '{user.preferred_username}}', + 'user.gender': '{user.gender}}', + 'user.birthdate': '{user.birthdate}}', + 'user.phone_number': '{user.phone_number}}', + 'user.phone_number_verified': '{user.phone_number_verified}}', + 'user.created_at': '{user.created_at}}', + 'user.updated_at': '{user.updated_at}}', + 'organization.name': '{organization.name}}', + 'organization.logo': '{organization.logo}}', + verification_url: '{verification_url}}', + otp: '{otp}}', }; From 85630a59c1c07acbeca83c4545f789512f409491 Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Tue, 2 Aug 2022 00:56:21 +0530 Subject: [PATCH 8/8] feat: add webhook payload example --- dashboard/src/components/InputField.tsx | 1 - .../src/components/UpdateWebhookModal.tsx | 75 ++++++++++++++++++- dashboard/src/constants.ts | 27 +++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/dashboard/src/components/InputField.tsx b/dashboard/src/components/InputField.tsx index f4d373d..39d68ac 100644 --- a/dashboard/src/components/InputField.tsx +++ b/dashboard/src/components/InputField.tsx @@ -12,7 +12,6 @@ import { Select, Textarea, Switch, - Code, Text, } from '@chakra-ui/react'; import { diff --git a/dashboard/src/components/UpdateWebhookModal.tsx b/dashboard/src/components/UpdateWebhookModal.tsx index d399c62..9749ef5 100644 --- a/dashboard/src/components/UpdateWebhookModal.tsx +++ b/dashboard/src/components/UpdateWebhookModal.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react'; import { Button, Center, + Code, + Collapse, Flex, Input, InputGroup, @@ -20,7 +22,13 @@ import { useDisclosure, useToast, } from '@chakra-ui/react'; -import { FaMinusCircle, FaPlus } from 'react-icons/fa'; +import { + FaAngleDown, + FaAngleUp, + FaMinusCircle, + FaPlus, + FaRegClone, +} from 'react-icons/fa'; import { useClient } from 'urql'; import { webhookEventNames, @@ -29,8 +37,13 @@ import { WebhookInputHeaderFields, UpdateModalViews, webhookVerifiedStatus, + webhookPayloadExample, } from '../constants'; -import { capitalizeFirstLetter, validateURI } from '../utils'; +import { + capitalizeFirstLetter, + copyTextToClipboard, + validateURI, +} from '../utils'; import { AddWebhook, EditWebhook, TestEndpoint } from '../graphql/mutation'; import { BiCheckCircle, BiError, BiErrorCircle } from 'react-icons/bi'; @@ -102,6 +115,7 @@ const UpdateWebhookModal = ({ const { isOpen, onOpen, onClose } = useDisclosure(); const [loading, setLoading] = useState(false); const [verifyingEndpoint, setVerifyingEndpoint] = useState(false); + const [isShowingPayload, setIsShowingPayload] = useState(false); const [webhook, setWebhook] = useState({ ...initWebhookData, }); @@ -454,6 +468,63 @@ const UpdateWebhookModal = ({ + + + + Payload + + (example) + + + + + + +
+											{webhookPayloadExample}
+										
+ {isShowingPayload && ( + + copyTextToClipboard(webhookPayloadExample) + } + > + + + )} +
+
+