mirror of
				https://github.com/abhinavxd/libredesk.git
				synced 2025-10-31 12:03:33 +00:00 
			
		
		
		
	Compare commits
	
		
			31 Commits
		
	
	
		
			feat/api-u
			...
			v0.7.1-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b971619ea6 | ||
|  | 69accaebef | ||
|  | 27de73536e | ||
|  | df108a3363 | ||
|  | 266c3dab72 | ||
|  | bf2c1fff6f | ||
|  | 2930af0c4f | ||
|  | 389c4e3dd3 | ||
|  | 9a119e6dc3 | ||
|  | ee178d383d | ||
|  | fc4db676d9 | ||
|  | 70cb3d0f80 | ||
|  | c9920c3377 | ||
|  | 6d62c3a4ba | ||
|  | d9b5fb8f0f | ||
|  | 3de320f1fb | ||
|  | be977dcff2 | ||
|  | 5e19f13e18 | ||
|  | ccc5940dd9 | ||
|  | 4203b82e90 | ||
|  | ba07e224c2 | ||
|  | 3fff65150f | ||
|  | c4fcf6bd91 | ||
|  | 5ea1b9e84c | ||
|  | 5b522888bc | ||
|  | dc2250ce50 | ||
|  | 839a06f0d2 | ||
|  | d2e5d85e3a | ||
|  | 0737d22374 | ||
|  | d6af9d10ea | ||
|  | 6381fc23c2 | 
							
								
								
									
										16
									
								
								.github/ISSUE_TEMPLATE/confirmed-bug.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.github/ISSUE_TEMPLATE/confirmed-bug.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| --- | ||||
| name: Confirmed Bug Report | ||||
| about: Report a confirmed bug in Libredesk | ||||
| title: "[Bug] <brief summary>" | ||||
| labels: bug | ||||
| assignees: "" | ||||
| --- | ||||
|  | ||||
| **Version:** | ||||
| - libredesk: [eg: v0.7.0] | ||||
|  | ||||
| **Description of the bug and steps to reproduce:** | ||||
| A clear and concise description of what the bug is. | ||||
|  | ||||
| **Logs / Screenshots:** | ||||
| Attach any relevant logs or screenshots to help diagnose the issue. | ||||
							
								
								
									
										16
									
								
								.github/ISSUE_TEMPLATE/possible-bug.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.github/ISSUE_TEMPLATE/possible-bug.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| --- | ||||
| name: Possible Bug Report | ||||
| about: Something in Libredesk might be broken but needs confirmation | ||||
| title: "[Possible Bug] <brief summary>" | ||||
| labels: bug, needs-investigation | ||||
| assignees: "" | ||||
| --- | ||||
|  | ||||
| **Version:** | ||||
|  - libredesk: [eg: v0.7.0] | ||||
|   | ||||
| **Description of the bug and steps to reproduce:** | ||||
| A clear and concise description of what the bug is. | ||||
|  | ||||
| **Logs / Screenshots:** | ||||
| Attach any relevant logs or screenshots to help diagnose the issue. | ||||
							
								
								
									
										2
									
								
								.github/workflows/crowdin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/crowdin.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,6 +12,8 @@ on: | ||||
| jobs: | ||||
|   crowdin: | ||||
|     runs-on: ubuntu-latest | ||||
|     # Only run on the original repository, not forks | ||||
|     if: github.event.repository.fork == false | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|   | ||||
| @@ -15,7 +15,7 @@ Visit [libredesk.io](https://libredesk.io) for more info. Check out the [**Live | ||||
| ## Features | ||||
|  | ||||
| - **Multi Shared Inbox**   | ||||
|   Libredesk supports multiple shares inboxes, letting you manage conversations across teams effortlessly. | ||||
|   Libredesk supports multiple shared inboxes, letting you manage conversations across teams effortlessly. | ||||
| - **Granular Permissions**   | ||||
|   Create custom roles with granular permissions for teams and individual agents. | ||||
| - **Smart Automation**   | ||||
| @@ -85,6 +85,11 @@ __________________ | ||||
| ## Developers | ||||
| If you are interested in contributing, refer to the [developer setup](https://libredesk.io/docs/developer-setup/). The backend is written in Go and the frontend is Vue js 3 with Shadcn for UI components. | ||||
|  | ||||
| ## Development Status | ||||
|  | ||||
| Libredesk is under active development.   | ||||
| Track roadmap and progress on the GitHub Project Board:   [https://github.com/users/abhinavxd/projects/1](https://github.com/users/abhinavxd/projects/1) | ||||
|  | ||||
|  | ||||
| ## Translators | ||||
| You can help translate Libredesk into your language on [Crowdin](https://crowdin.com/project/libredesk).   | ||||
|   | ||||
| @@ -45,10 +45,11 @@ func handleToggleAutomationRule(r *fastglue.Request) error { | ||||
| 		app   = r.Context.(*App) | ||||
| 		id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) | ||||
| 	) | ||||
| 	if err := app.automation.ToggleRule(id); err != nil { | ||||
| 	toggledRule, err := app.automation.ToggleRule(id) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(toggledRule) | ||||
| } | ||||
|  | ||||
| // handleUpdateAutomationRule updates an automation rule | ||||
| @@ -66,10 +67,11 @@ func handleUpdateAutomationRule(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	if err = app.automation.UpdateRule(id, rule); err != nil { | ||||
| 	updatedRule, err := app.automation.UpdateRule(id, rule) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedRule) | ||||
| } | ||||
|  | ||||
| // handleCreateAutomationRule creates a new automation rule | ||||
| @@ -81,10 +83,11 @@ func handleCreateAutomationRule(r *fastglue.Request) error { | ||||
| 	if err := r.Decode(&rule, "json"); err != nil { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) | ||||
| 	} | ||||
| 	if err := app.automation.CreateRule(rule); err != nil { | ||||
| 	createdRule, err := app.automation.CreateRule(rule) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdRule) | ||||
| } | ||||
|  | ||||
| // handleDeleteAutomationRule deletes an automation rule | ||||
|   | ||||
| @@ -55,11 +55,12 @@ func handleCreateBusinessHours(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.businessHours.Create(businessHours.Name, businessHours.Description, businessHours.IsAlwaysOpen, businessHours.Hours, businessHours.Holidays); err != nil { | ||||
| 	createdBusinessHours, err := app.businessHours.Create(businessHours.Name, businessHours.Description, businessHours.IsAlwaysOpen, businessHours.Hours, businessHours.Holidays) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdBusinessHours) | ||||
| } | ||||
|  | ||||
| // handleDeleteBusinessHour deletes the business hour with the given id. | ||||
| @@ -93,8 +94,9 @@ func handleUpdateBusinessHours(r *fastglue.Request) error { | ||||
| 	if businessHours.Name == "" { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
| 	if err := app.businessHours.Update(id, businessHours.Name, businessHours.Description, businessHours.IsAlwaysOpen, businessHours.Hours, businessHours.Holidays); err != nil { | ||||
| 	updatedBusinessHours, err := app.businessHours.Update(id, businessHours.Name, businessHours.Description, businessHours.IsAlwaysOpen, businessHours.Hours, businessHours.Holidays) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedBusinessHours) | ||||
| } | ||||
|   | ||||
| @@ -744,7 +744,7 @@ func handleCreateConversation(r *fastglue.Request) error { | ||||
| 	} | ||||
|  | ||||
| 	// Send reply to the created conversation. | ||||
| 	if err := app.conversation.SendReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil { | ||||
| 	if _, err := app.conversation.SendReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil { | ||||
| 		// Delete the conversation if reply fails. | ||||
| 		if err := app.conversation.DeleteConversation(conversationUUID); err != nil { | ||||
| 			app.lo.Error("error deleting conversation", "error", err) | ||||
|   | ||||
| @@ -70,10 +70,11 @@ func handleCreateCustomAttribute(r *fastglue.Request) error { | ||||
| 	if err := validateCustomAttribute(app, attribute); err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	if err := app.customAttribute.Create(attribute); err != nil { | ||||
| 	createdAttr, err := app.customAttribute.Create(attribute) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdAttr) | ||||
| } | ||||
|  | ||||
| // handleUpdateCustomAttribute updates an existing custom attribute in the database. | ||||
| @@ -92,10 +93,11 @@ func handleUpdateCustomAttribute(r *fastglue.Request) error { | ||||
| 	if err := validateCustomAttribute(app, attribute); err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	if err = app.customAttribute.Update(id, attribute); err != nil { | ||||
| 	updatedAttr, err := app.customAttribute.Update(id, attribute) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedAttr) | ||||
| } | ||||
|  | ||||
| // handleDeleteCustomAttribute deletes a custom attribute from the database. | ||||
|   | ||||
| @@ -47,11 +47,12 @@ func handleCreateInbox(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), err.Error(), envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.inbox.Create(inbox); err != nil { | ||||
| 	createdInbox, err := app.inbox.Create(inbox) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	if err := validateInbox(app, inbox); err != nil { | ||||
| 	if err := validateInbox(app, createdInbox); err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| @@ -59,7 +60,13 @@ func handleCreateInbox(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	// Clear passwords before returning. | ||||
| 	if err := createdInbox.ClearPasswords(); err != nil { | ||||
| 		app.lo.Error("error clearing inbox passwords from response", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(createdInbox) | ||||
| } | ||||
|  | ||||
| // handleUpdateInbox updates an inbox | ||||
| @@ -82,7 +89,7 @@ func handleUpdateInbox(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	err = app.inbox.Update(id, inbox) | ||||
| 	updatedInbox, err := app.inbox.Update(id, inbox) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| @@ -91,7 +98,13 @@ func handleUpdateInbox(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(inbox) | ||||
| 	// Clear passwords before returning. | ||||
| 	if err := updatedInbox.ClearPasswords(); err != nil { | ||||
| 		app.lo.Error("error clearing inbox passwords from response", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(updatedInbox) | ||||
| } | ||||
|  | ||||
| // handleToggleInbox toggles an inbox | ||||
| @@ -105,7 +118,8 @@ func handleToggleInbox(r *fastglue.Request) error { | ||||
| 			app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	if err = app.inbox.Toggle(id); err != nil { | ||||
| 	toggledInbox, err := app.inbox.Toggle(id) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| @@ -113,7 +127,13 @@ func handleToggleInbox(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	// Clear passwords before returning | ||||
| 	if err := toggledInbox.ClearPasswords(); err != nil { | ||||
| 		app.lo.Error("error clearing inbox passwords from response", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(toggledInbox) | ||||
| } | ||||
|  | ||||
| // handleDeleteInbox deletes an inbox | ||||
|   | ||||
							
								
								
									
										10
									
								
								cmd/macro.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/macro.go
									
									
									
									
									
								
							| @@ -81,11 +81,12 @@ func handleCreateMacro(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.macro.Create(macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions); err != nil { | ||||
| 	createdMacro, err := app.macro.Create(macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(macro) | ||||
| 	return r.SendEnvelope(createdMacro) | ||||
| } | ||||
|  | ||||
| // handleUpdateMacro updates a macro. | ||||
| @@ -109,11 +110,12 @@ func handleUpdateMacro(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	if err = app.macro.Update(id, macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions); err != nil { | ||||
| 	updatedMacro, err := app.macro.Update(id, macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(macro) | ||||
| 	return r.SendEnvelope(updatedMacro) | ||||
| } | ||||
|  | ||||
| // handleDeleteMacro deletes macro. | ||||
|   | ||||
| @@ -162,13 +162,15 @@ func handleSendMessage(r *fastglue.Request) error { | ||||
| 	} | ||||
|  | ||||
| 	if req.Private { | ||||
| 		if err := app.conversation.SendPrivateNote(media, user.ID, cuuid, req.Message); err != nil { | ||||
| 		message, err := app.conversation.SendPrivateNote(media, user.ID, cuuid, req.Message) | ||||
| 		if err != nil { | ||||
| 			return sendErrorEnvelope(r, err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if err := app.conversation.SendReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/); err != nil { | ||||
| 		return r.SendEnvelope(message) | ||||
| 	} | ||||
| 	message, err := app.conversation.SendReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(message) | ||||
| } | ||||
|   | ||||
							
								
								
									
										18
									
								
								cmd/oidc.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								cmd/oidc.go
									
									
									
									
									
								
							| @@ -65,7 +65,8 @@ func handleCreateOIDC(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.oidc.Create(req); err != nil { | ||||
| 	createdOIDC, err := app.oidc.Create(req) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| @@ -73,7 +74,11 @@ func handleCreateOIDC(r *fastglue.Request) error { | ||||
| 	if err := reloadAuth(app); err != nil { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError) | ||||
| 	} | ||||
| 	return r.SendEnvelope("OIDC created successfully") | ||||
| 	 | ||||
| 	// Clear client secret before returning | ||||
| 	createdOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10) | ||||
| 	 | ||||
| 	return r.SendEnvelope(createdOIDC) | ||||
| } | ||||
|  | ||||
| // handleUpdateOIDC updates an OIDC record. | ||||
| @@ -96,7 +101,8 @@ func handleUpdateOIDC(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	if err = app.oidc.Update(id, req); err != nil { | ||||
| 	updatedOIDC, err := app.oidc.Update(id, req) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| @@ -104,7 +110,11 @@ func handleUpdateOIDC(r *fastglue.Request) error { | ||||
| 	if err := reloadAuth(app); err != nil { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.couldNotReload", "name", "OIDC"), nil, envelope.GeneralError) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	 | ||||
| 	// Clear client secret before returning | ||||
| 	updatedOIDC.ClientSecret = strings.Repeat(stringutil.PasswordDummy, 10) | ||||
| 	 | ||||
| 	return r.SendEnvelope(updatedOIDC) | ||||
| } | ||||
|  | ||||
| // handleDeleteOIDC deletes an OIDC record. | ||||
|   | ||||
							
								
								
									
										10
									
								
								cmd/roles.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/roles.go
									
									
									
									
									
								
							| @@ -55,10 +55,11 @@ func handleCreateRole(r *fastglue.Request) error { | ||||
| 	if err := r.Decode(&req, "json"); err != nil { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) | ||||
| 	} | ||||
| 	if err := app.role.Create(req); err != nil { | ||||
| 	createdRole, err := app.role.Create(req) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdRole) | ||||
| } | ||||
|  | ||||
| // handleUpdateRole updates a role | ||||
| @@ -71,8 +72,9 @@ func handleUpdateRole(r *fastglue.Request) error { | ||||
| 	if err := r.Decode(&req, "json"); err != nil { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) | ||||
| 	} | ||||
| 	if err := app.role.Update(id, req); err != nil { | ||||
| 	updatedRole, err := app.role.Update(id, req) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedRole) | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								cmd/sla.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/sla.go
									
									
									
									
									
								
							| @@ -54,11 +54,12 @@ func handleCreateSLA(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.sla.Create(sla.Name, sla.Description, sla.FirstResponseTime, sla.ResolutionTime, sla.NextResponseTime, sla.Notifications); err != nil { | ||||
| 	createdSLA, err := app.sla.Create(sla.Name, sla.Description, sla.FirstResponseTime, sla.ResolutionTime, sla.NextResponseTime, sla.Notifications) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope("SLA created successfully.") | ||||
| 	return r.SendEnvelope(createdSLA) | ||||
| } | ||||
|  | ||||
| // handleUpdateSLA updates the SLA with the given ID. | ||||
| @@ -81,11 +82,12 @@ func handleUpdateSLA(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.sla.Update(id, sla.Name, sla.Description, sla.FirstResponseTime, sla.ResolutionTime, sla.NextResponseTime, sla.Notifications); err != nil { | ||||
| 	updatedSLA, err := app.sla.Update(id, sla.Name, sla.Description, sla.FirstResponseTime, sla.ResolutionTime, sla.NextResponseTime, sla.Notifications) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedSLA) | ||||
| } | ||||
|  | ||||
| // handleDeleteSLA deletes the SLA with the given ID. | ||||
|   | ||||
| @@ -33,12 +33,12 @@ func handleCreateStatus(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	err := app.status.Create(status.Name) | ||||
| 	createdStatus, err := app.status.Create(status.Name) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdStatus) | ||||
| } | ||||
|  | ||||
| func handleDeleteStatus(r *fastglue.Request) error { | ||||
| @@ -74,10 +74,10 @@ func handleUpdateStatus(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	err = app.status.Update(id, status.Name) | ||||
| 	updatedStatus, err := app.status.Update(id, status.Name) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedStatus) | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								cmd/tags.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/tags.go
									
									
									
									
									
								
							| @@ -35,11 +35,12 @@ func handleCreateTag(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.tag.Create(tag.Name); err != nil { | ||||
| 	createdTag, err := app.tag.Create(tag.Name) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdTag) | ||||
| } | ||||
|  | ||||
| // handleDeleteTag deletes a tag from the database. | ||||
| @@ -78,9 +79,10 @@ func handleUpdateTag(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	if err = app.tag.Update(id, tag.Name); err != nil { | ||||
| 	updatedTag, err := app.tag.Update(id, tag.Name) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedTag) | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								cmd/teams.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/teams.go
									
									
									
									
									
								
							| @@ -60,10 +60,11 @@ func handleCreateTeam(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil)) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.team.Create(req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations); err != nil { | ||||
| 	createdTeam, err := app.team.Create(req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdTeam) | ||||
| } | ||||
|  | ||||
| // handleUpdateTeam updates an existing team. | ||||
| @@ -82,10 +83,11 @@ func handleUpdateTeam(r *fastglue.Request) error { | ||||
| 		return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil)) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.team.Update(id, req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations); err != nil { | ||||
| 	updatedTeam, err := app.team.Update(id, req.Name, req.Timezone, req.ConversationAssignmentType, req.BusinessHoursID, req.SLAPolicyID, req.Emoji.String, req.MaxAutoAssignedConversations); | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedTeam) | ||||
| } | ||||
|  | ||||
| // handleDeleteTeam deletes a team | ||||
|   | ||||
| @@ -53,10 +53,11 @@ func handleCreateTemplate(r *fastglue.Request) error { | ||||
| 	if req.Name == "" { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
| 	if err := app.tmpl.Create(req); err != nil { | ||||
| 	template, err := app.tmpl.Create(req) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(template) | ||||
| } | ||||
|  | ||||
| // handleUpdateTemplate updates a template. | ||||
| @@ -76,10 +77,11 @@ func handleUpdateTemplate(r *fastglue.Request) error { | ||||
| 	if req.Name == "" { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError) | ||||
| 	} | ||||
| 	if err = app.tmpl.Update(id, req); err != nil { | ||||
| 	updatedTemplate, err := app.tmpl.Update(id, req) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedTemplate) | ||||
| } | ||||
|  | ||||
| // handleDeleteTemplate deletes a template. | ||||
|   | ||||
							
								
								
									
										10
									
								
								cmd/views.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								cmd/views.go
									
									
									
									
									
								
							| @@ -47,10 +47,11 @@ func handleCreateUserView(r *fastglue.Request) error { | ||||
| 	if string(view.Filters) == "" { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`Filters`"), nil, envelope.InputError) | ||||
| 	} | ||||
| 	if err := app.view.Create(view.Name, view.Filters, user.ID); err != nil { | ||||
| 	createdView, err := app.view.Create(view.Name, view.Filters, user.ID) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(createdView) | ||||
| } | ||||
|  | ||||
| // handleDeleteUserView deletes a view for a user. | ||||
| @@ -111,8 +112,9 @@ func handleUpdateUserView(r *fastglue.Request) error { | ||||
| 	if v.UserID != user.ID { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("globals.messages.denied", "name", "{globals.terms.permission}"), nil, envelope.PermissionError) | ||||
| 	} | ||||
| 	if err = app.view.Update(id, view.Name, view.Filters); err != nil { | ||||
| 	updatedView, err := app.view.Update(id, view.Name, view.Filters) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
| 	return r.SendEnvelope(true) | ||||
| 	return r.SendEnvelope(updatedView) | ||||
| } | ||||
|   | ||||
| @@ -67,12 +67,15 @@ func handleCreateWebhook(r *fastglue.Request) error { | ||||
| 		return r.SendEnvelope(err) | ||||
| 	} | ||||
|  | ||||
| 	_, err := app.webhook.Create(webhook) | ||||
| 	webhook, err := app.webhook.Create(webhook) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	// Clear secret before returning | ||||
| 	webhook.Secret = strings.Repeat(stringutil.PasswordDummy, 10) | ||||
|  | ||||
| 	return r.SendEnvelope(webhook) | ||||
| } | ||||
|  | ||||
| // handleUpdateWebhook updates an existing webhook in the database. | ||||
| @@ -105,11 +108,15 @@ func handleUpdateWebhook(r *fastglue.Request) error { | ||||
| 		webhook.Secret = existingWebhook.Secret | ||||
| 	} | ||||
|  | ||||
| 	if err := app.webhook.Update(id, webhook); err != nil { | ||||
| 	updatedWebhook, err := app.webhook.Update(id, webhook) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	// Clear secret before returning | ||||
| 	updatedWebhook.Secret = strings.Repeat(stringutil.PasswordDummy, 10) | ||||
|  | ||||
| 	return r.SendEnvelope(updatedWebhook) | ||||
| } | ||||
|  | ||||
| // handleDeleteWebhook deletes a webhook from the database. | ||||
| @@ -140,11 +147,15 @@ func handleToggleWebhook(r *fastglue.Request) error { | ||||
| 		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError) | ||||
| 	} | ||||
|  | ||||
| 	if err := app.webhook.Toggle(id); err != nil { | ||||
| 	toggledWebhook, err := app.webhook.Toggle(id) | ||||
| 	if err != nil { | ||||
| 		return sendErrorEnvelope(r, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.SendEnvelope(true) | ||||
| 	// Clear secret before returning | ||||
| 	toggledWebhook.Secret = strings.Repeat(stringutil.PasswordDummy, 10) | ||||
|  | ||||
| 	return r.SendEnvelope(toggledWebhook) | ||||
| } | ||||
|  | ||||
| // handleTestWebhook sends a test payload to a webhook. | ||||
|   | ||||
							
								
								
									
										30
									
								
								docs/docs/api-getting-started.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								docs/docs/api-getting-started.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| # API getting started | ||||
|  | ||||
| You can access the Libredesk API to interact with your instance programmatically. | ||||
|  | ||||
| ## Generating API keys | ||||
|  | ||||
| 1. **Edit agent**: Go to Admin → Teammate → Agent → Edit | ||||
| 2. **Generate new API key**: An API Key and API Secret will be generated for the agent | ||||
| 3. **Save the credentials**: Keep both the API Key and API Secret secure | ||||
| 4. **Key management**: You can revoke / regenerate API keys at any time from the same page | ||||
|  | ||||
| ## Using the API | ||||
|  | ||||
| LibreDesk supports two authentication schemes: | ||||
|  | ||||
| ### Basic authentication | ||||
| ```bash | ||||
| curl -X GET "https://your-libredesk-instance.com/api/endpoint" \ | ||||
|   -H "Authorization: Basic <base64_encoded_key:secret>" | ||||
| ``` | ||||
|  | ||||
| ### Token authentication | ||||
| ```bash | ||||
| curl -X GET "https://your-libredesk-instance.com/api/endpoint" \ | ||||
|   -H "Authorization: token your_api_key:your_api_secret" | ||||
| ``` | ||||
|  | ||||
| ## API Documentation | ||||
|  | ||||
| Complete API documentation with available endpoints and examples coming soon. | ||||
| @@ -210,7 +210,7 @@ Triggered when an existing message is updated. | ||||
|  | ||||
| ## Delivery and Retries | ||||
|  | ||||
| - Webhooks are delivered with a 10-second timeout | ||||
| - Webhooks requests timeout can be configured in the `config.toml` file | ||||
| - Failed deliveries are not automatically retried | ||||
| - Webhook delivery runs in a background worker pool for better performance | ||||
| - If the webhook queue is full (configurable in config.toml file), new events may be dropped | ||||
|   | ||||
| @@ -32,6 +32,7 @@ nav: | ||||
|       - Email Templates: templating.md | ||||
|       - SSO Setup: sso.md | ||||
|       - Webhooks: webhooks.md | ||||
|       - API Getting Started: api-getting-started.md | ||||
|   - Contributions: | ||||
|       - Developer Setup: developer-setup.md | ||||
|       - Translate Libredesk: translations.md | ||||
|   | ||||
| @@ -18,6 +18,8 @@ | ||||
|     "format": "prettier --write src/" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@codemirror/lang-html": "^6.4.9", | ||||
|     "@codemirror/theme-one-dark": "^6.1.3", | ||||
|     "@formkit/auto-animate": "^0.8.2", | ||||
|     "@internationalized/date": "^3.5.5", | ||||
|     "@radix-icons/vue": "^1.0.0", | ||||
| @@ -40,7 +42,7 @@ | ||||
|     "axios": "^1.8.2", | ||||
|     "class-variance-authority": "^0.7.0", | ||||
|     "clsx": "^2.1.1", | ||||
|     "codeflask": "^1.4.1", | ||||
|     "codemirror": "^6.0.2", | ||||
|     "date-fns": "^3.6.0", | ||||
|     "lucide-vue-next": "^0.378.0", | ||||
|     "mitt": "^3.0.1", | ||||
|   | ||||
							
								
								
									
										210
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										210
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -8,6 +8,12 @@ importers: | ||||
|  | ||||
|   .: | ||||
|     dependencies: | ||||
|       '@codemirror/lang-html': | ||||
|         specifier: ^6.4.9 | ||||
|         version: 6.4.9 | ||||
|       '@codemirror/theme-one-dark': | ||||
|         specifier: ^6.1.3 | ||||
|         version: 6.1.3 | ||||
|       '@formkit/auto-animate': | ||||
|         specifier: ^0.8.2 | ||||
|         version: 0.8.2 | ||||
| @@ -74,9 +80,9 @@ importers: | ||||
|       clsx: | ||||
|         specifier: ^2.1.1 | ||||
|         version: 2.1.1 | ||||
|       codeflask: | ||||
|         specifier: ^1.4.1 | ||||
|         version: 1.4.1 | ||||
|       codemirror: | ||||
|         specifier: ^6.0.2 | ||||
|         version: 6.0.2 | ||||
|       date-fns: | ||||
|         specifier: ^3.6.0 | ||||
|         version: 3.6.0 | ||||
| @@ -234,6 +240,39 @@ packages: | ||||
|   '@bassist/utils@0.4.0': | ||||
|     resolution: {integrity: sha512-aoFTl0jUjm8/tDZodP41wnEkvB+C5O9NFCuYN/ztL6jSUSsuBkXq90/1ifBm1XhV/zySHgLYlU1+tgo3XtQ+nA==} | ||||
|  | ||||
|   '@codemirror/autocomplete@6.18.6': | ||||
|     resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==} | ||||
|  | ||||
|   '@codemirror/commands@6.8.1': | ||||
|     resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} | ||||
|  | ||||
|   '@codemirror/lang-css@6.3.1': | ||||
|     resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} | ||||
|  | ||||
|   '@codemirror/lang-html@6.4.9': | ||||
|     resolution: {integrity: sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==} | ||||
|  | ||||
|   '@codemirror/lang-javascript@6.2.4': | ||||
|     resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} | ||||
|  | ||||
|   '@codemirror/language@6.11.1': | ||||
|     resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} | ||||
|  | ||||
|   '@codemirror/lint@6.8.5': | ||||
|     resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==} | ||||
|  | ||||
|   '@codemirror/search@6.5.11': | ||||
|     resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} | ||||
|  | ||||
|   '@codemirror/state@6.5.2': | ||||
|     resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} | ||||
|  | ||||
|   '@codemirror/theme-one-dark@6.1.3': | ||||
|     resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} | ||||
|  | ||||
|   '@codemirror/view@6.37.2': | ||||
|     resolution: {integrity: sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==} | ||||
|  | ||||
|   '@colors/colors@1.5.0': | ||||
|     resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} | ||||
|     engines: {node: '>=0.1.90'} | ||||
| @@ -508,6 +547,24 @@ packages: | ||||
|   '@juggle/resize-observer@3.4.0': | ||||
|     resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} | ||||
|  | ||||
|   '@lezer/common@1.2.3': | ||||
|     resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} | ||||
|  | ||||
|   '@lezer/css@1.2.1': | ||||
|     resolution: {integrity: sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg==} | ||||
|  | ||||
|   '@lezer/highlight@1.2.1': | ||||
|     resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} | ||||
|  | ||||
|   '@lezer/html@1.3.10': | ||||
|     resolution: {integrity: sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==} | ||||
|  | ||||
|   '@lezer/javascript@1.5.1': | ||||
|     resolution: {integrity: sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==} | ||||
|  | ||||
|   '@lezer/lr@1.4.2': | ||||
|     resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} | ||||
|  | ||||
|   '@mapbox/geojson-rewind@0.5.2': | ||||
|     resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} | ||||
|     hasBin: true | ||||
| @@ -535,6 +592,9 @@ packages: | ||||
|     resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} | ||||
|     engines: {node: '>=6.0.0'} | ||||
|  | ||||
|   '@marijn/find-cluster-break@1.0.2': | ||||
|     resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} | ||||
|  | ||||
|   '@nodelib/fs.scandir@2.1.5': | ||||
|     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} | ||||
|     engines: {node: '>= 8'} | ||||
| @@ -1109,9 +1169,6 @@ packages: | ||||
|   '@types/pbf@3.0.5': | ||||
|     resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} | ||||
|  | ||||
|   '@types/prismjs@1.26.5': | ||||
|     resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} | ||||
|  | ||||
|   '@types/sinonjs__fake-timers@8.1.1': | ||||
|     resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} | ||||
|  | ||||
| @@ -1532,8 +1589,8 @@ packages: | ||||
|     resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   codeflask@1.4.1: | ||||
|     resolution: {integrity: sha512-4vb2IbE/iwvP0Uubhd2ixVeysm3KNC2pl7SoDaisxq1juhZzvap3qbaX7B2CtpQVvv5V9sjcQK8hO0eTcY0V9Q==} | ||||
|   codemirror@6.0.2: | ||||
|     resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} | ||||
|  | ||||
|   color-convert@2.0.1: | ||||
|     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} | ||||
| @@ -2780,10 +2837,6 @@ packages: | ||||
|     resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   prismjs@1.29.0: | ||||
|     resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} | ||||
|     engines: {node: '>=6'} | ||||
|  | ||||
|   process@0.11.10: | ||||
|     resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} | ||||
|     engines: {node: '>= 0.6.0'} | ||||
| @@ -3093,6 +3146,9 @@ packages: | ||||
|   striptags@3.2.0: | ||||
|     resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} | ||||
|  | ||||
|   style-mod@4.1.2: | ||||
|     resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} | ||||
|  | ||||
|   stylis@4.2.0: | ||||
|     resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} | ||||
|  | ||||
| @@ -3550,6 +3606,89 @@ snapshots: | ||||
|     dependencies: | ||||
|       '@withtypes/mime': 0.1.2 | ||||
|  | ||||
|   '@codemirror/autocomplete@6.18.6': | ||||
|     dependencies: | ||||
|       '@codemirror/language': 6.11.1 | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       '@lezer/common': 1.2.3 | ||||
|  | ||||
|   '@codemirror/commands@6.8.1': | ||||
|     dependencies: | ||||
|       '@codemirror/language': 6.11.1 | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       '@lezer/common': 1.2.3 | ||||
|  | ||||
|   '@codemirror/lang-css@6.3.1': | ||||
|     dependencies: | ||||
|       '@codemirror/autocomplete': 6.18.6 | ||||
|       '@codemirror/language': 6.11.1 | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@lezer/common': 1.2.3 | ||||
|       '@lezer/css': 1.2.1 | ||||
|  | ||||
|   '@codemirror/lang-html@6.4.9': | ||||
|     dependencies: | ||||
|       '@codemirror/autocomplete': 6.18.6 | ||||
|       '@codemirror/lang-css': 6.3.1 | ||||
|       '@codemirror/lang-javascript': 6.2.4 | ||||
|       '@codemirror/language': 6.11.1 | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       '@lezer/common': 1.2.3 | ||||
|       '@lezer/css': 1.2.1 | ||||
|       '@lezer/html': 1.3.10 | ||||
|  | ||||
|   '@codemirror/lang-javascript@6.2.4': | ||||
|     dependencies: | ||||
|       '@codemirror/autocomplete': 6.18.6 | ||||
|       '@codemirror/language': 6.11.1 | ||||
|       '@codemirror/lint': 6.8.5 | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       '@lezer/common': 1.2.3 | ||||
|       '@lezer/javascript': 1.5.1 | ||||
|  | ||||
|   '@codemirror/language@6.11.1': | ||||
|     dependencies: | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       '@lezer/common': 1.2.3 | ||||
|       '@lezer/highlight': 1.2.1 | ||||
|       '@lezer/lr': 1.4.2 | ||||
|       style-mod: 4.1.2 | ||||
|  | ||||
|   '@codemirror/lint@6.8.5': | ||||
|     dependencies: | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       crelt: 1.0.6 | ||||
|  | ||||
|   '@codemirror/search@6.5.11': | ||||
|     dependencies: | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       crelt: 1.0.6 | ||||
|  | ||||
|   '@codemirror/state@6.5.2': | ||||
|     dependencies: | ||||
|       '@marijn/find-cluster-break': 1.0.2 | ||||
|  | ||||
|   '@codemirror/theme-one-dark@6.1.3': | ||||
|     dependencies: | ||||
|       '@codemirror/language': 6.11.1 | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|       '@lezer/highlight': 1.2.1 | ||||
|  | ||||
|   '@codemirror/view@6.37.2': | ||||
|     dependencies: | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       crelt: 1.0.6 | ||||
|       style-mod: 4.1.2 | ||||
|       w3c-keyname: 2.2.8 | ||||
|  | ||||
|   '@colors/colors@1.5.0': | ||||
|     optional: true | ||||
|  | ||||
| @@ -3815,6 +3954,34 @@ snapshots: | ||||
|  | ||||
|   '@juggle/resize-observer@3.4.0': {} | ||||
|  | ||||
|   '@lezer/common@1.2.3': {} | ||||
|  | ||||
|   '@lezer/css@1.2.1': | ||||
|     dependencies: | ||||
|       '@lezer/common': 1.2.3 | ||||
|       '@lezer/highlight': 1.2.1 | ||||
|       '@lezer/lr': 1.4.2 | ||||
|  | ||||
|   '@lezer/highlight@1.2.1': | ||||
|     dependencies: | ||||
|       '@lezer/common': 1.2.3 | ||||
|  | ||||
|   '@lezer/html@1.3.10': | ||||
|     dependencies: | ||||
|       '@lezer/common': 1.2.3 | ||||
|       '@lezer/highlight': 1.2.1 | ||||
|       '@lezer/lr': 1.4.2 | ||||
|  | ||||
|   '@lezer/javascript@1.5.1': | ||||
|     dependencies: | ||||
|       '@lezer/common': 1.2.3 | ||||
|       '@lezer/highlight': 1.2.1 | ||||
|       '@lezer/lr': 1.4.2 | ||||
|  | ||||
|   '@lezer/lr@1.4.2': | ||||
|     dependencies: | ||||
|       '@lezer/common': 1.2.3 | ||||
|  | ||||
|   '@mapbox/geojson-rewind@0.5.2': | ||||
|     dependencies: | ||||
|       get-stream: 6.0.1 | ||||
| @@ -3836,6 +4003,8 @@ snapshots: | ||||
|  | ||||
|   '@mapbox/whoots-js@3.1.0': {} | ||||
|  | ||||
|   '@marijn/find-cluster-break@1.0.2': {} | ||||
|  | ||||
|   '@nodelib/fs.scandir@2.1.5': | ||||
|     dependencies: | ||||
|       '@nodelib/fs.stat': 2.0.5 | ||||
| @@ -4378,8 +4547,6 @@ snapshots: | ||||
|  | ||||
|   '@types/pbf@3.0.5': {} | ||||
|  | ||||
|   '@types/prismjs@1.26.5': {} | ||||
|  | ||||
|   '@types/sinonjs__fake-timers@8.1.1': {} | ||||
|  | ||||
|   '@types/sizzle@2.3.9': {} | ||||
| @@ -4906,10 +5073,15 @@ snapshots: | ||||
|  | ||||
|   clsx@2.1.1: {} | ||||
|  | ||||
|   codeflask@1.4.1: | ||||
|   codemirror@6.0.2: | ||||
|     dependencies: | ||||
|       '@types/prismjs': 1.26.5 | ||||
|       prismjs: 1.29.0 | ||||
|       '@codemirror/autocomplete': 6.18.6 | ||||
|       '@codemirror/commands': 6.8.1 | ||||
|       '@codemirror/language': 6.11.1 | ||||
|       '@codemirror/lint': 6.8.5 | ||||
|       '@codemirror/search': 6.5.11 | ||||
|       '@codemirror/state': 6.5.2 | ||||
|       '@codemirror/view': 6.37.2 | ||||
|  | ||||
|   color-convert@2.0.1: | ||||
|     dependencies: | ||||
| @@ -6200,8 +6372,6 @@ snapshots: | ||||
|  | ||||
|   pretty-bytes@5.6.0: {} | ||||
|  | ||||
|   prismjs@1.29.0: {} | ||||
|  | ||||
|   process@0.11.10: {} | ||||
|  | ||||
|   prosemirror-changeset@2.2.1: | ||||
| @@ -6601,6 +6771,8 @@ snapshots: | ||||
|  | ||||
|   striptags@3.2.0: {} | ||||
|  | ||||
|   style-mod@4.1.2: {} | ||||
|  | ||||
|   stylis@4.2.0: {} | ||||
|  | ||||
|   stylus@0.57.0: | ||||
|   | ||||
| @@ -207,10 +207,6 @@ | ||||
| } | ||||
| // End Scrollbar | ||||
|  | ||||
| .code-editor { | ||||
|   @apply rounded border shadow h-[65vh] min-h-[250px] w-full relative; | ||||
| } | ||||
|  | ||||
| .show-quoted-text { | ||||
|   blockquote { | ||||
|     @apply block; | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| <template> | ||||
|     <div ref="codeEditor" id="code-editor" class="code-editor" /> | ||||
|     <div ref="codeEditor" @click="editorView?.focus()" class="w-full h-[28rem] border rounded-md" /> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ref, onMounted, watch, nextTick } from 'vue' | ||||
| import CodeFlask from 'codeflask' | ||||
| import { ref, onMounted, watch, nextTick, useTemplateRef } from 'vue' | ||||
| import { EditorView, basicSetup } from 'codemirror' | ||||
| import { html } from '@codemirror/lang-html' | ||||
| import { oneDark } from '@codemirror/theme-one-dark' | ||||
| import { useColorMode } from '@vueuse/core' | ||||
|  | ||||
| const props = defineProps({ | ||||
|     modelValue: { type: String, default: '' }, | ||||
| @@ -13,45 +16,38 @@ const props = defineProps({ | ||||
| }) | ||||
|  | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
| const codeEditor = ref(null) | ||||
| const data = ref('') | ||||
| const flask = ref(null) | ||||
| let editorView = null  | ||||
| const codeEditor = useTemplateRef('codeEditor') | ||||
|  | ||||
| const initCodeEditor = (body) => { | ||||
|     const el = document.createElement('code-flask') | ||||
|     el.attachShadow({ mode: 'open' }) | ||||
|     el.shadowRoot.innerHTML = ` | ||||
|       <style> | ||||
|         .codeflask .codeflask__flatten { | ||||
|           font-size: 15px; | ||||
|           white-space: pre-wrap; | ||||
|           word-break: break-word; | ||||
|         } | ||||
|         .codeflask .codeflask__lines { background: #fafafa; z-index: 10; } | ||||
|         .codeflask .token.tag { font-weight: bold; } | ||||
|         .codeflask .token.attr-name { color: #111; } | ||||
|         .codeflask .token.attr-value { color: #000 !important; } | ||||
|       </style> | ||||
|       <div id="area"></div> | ||||
|     ` | ||||
|     codeEditor.value.appendChild(el) | ||||
|     const isDark = useColorMode().value === 'dark' | ||||
|  | ||||
|     flask.value = new CodeFlask(el.shadowRoot.getElementById('area'), { | ||||
|         language: props.language, | ||||
|         lineNumbers: false, | ||||
|         styleParent: el.shadowRoot, | ||||
|         readonly: props.disabled | ||||
|     }) | ||||
|  | ||||
|     flask.value.onUpdate((v) => { | ||||
|     editorView = new EditorView({ | ||||
|         doc: body, | ||||
|         extensions: [ | ||||
|             basicSetup, | ||||
|             html(), | ||||
|             ...(isDark ? [oneDark] : []), | ||||
|             EditorView.editable.of(!props.disabled), | ||||
|             EditorView.theme({ | ||||
|                 '&': { height: '100%' }, | ||||
|                 '.cm-editor': { height: '100%' }, | ||||
|                 '.cm-scroller': { overflow: 'auto' } | ||||
|             }), | ||||
|             EditorView.updateListener.of((update) => { | ||||
|                 if (!update.docChanged) return | ||||
|                 const v = update.state.doc.toString() | ||||
|                 emit('update:modelValue', v) | ||||
|                 data.value = v | ||||
|                  | ||||
|             }) | ||||
|         ], | ||||
|         parent: codeEditor.value | ||||
|     }) | ||||
|  | ||||
|     flask.value.updateCode(body) | ||||
|  | ||||
|     nextTick(() => { | ||||
|         document.querySelector('code-flask').shadowRoot.querySelector('textarea').focus() | ||||
|         editorView?.focus() | ||||
|     }) | ||||
| } | ||||
|  | ||||
| @@ -61,7 +57,9 @@ onMounted(() => { | ||||
|  | ||||
| watch(() => props.modelValue, (newVal) => { | ||||
|     if (newVal !== data.value) { | ||||
|         flask.value.updateCode(newVal) | ||||
|         editorView?.dispatch({ | ||||
|             changes: { from: 0, to: editorView.state.doc.length, insert: newVal } | ||||
|         }) | ||||
|     } | ||||
| }) | ||||
| </script> | ||||
| @@ -5,7 +5,7 @@ import { | ||||
|   accountNavItems, | ||||
|   contactNavItems | ||||
| } from '@/constants/navigation' | ||||
| import { RouterLink, useRoute } from 'vue-router' | ||||
| import { RouterLink, useRoute, useRouter } from 'vue-router' | ||||
| import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' | ||||
| import { | ||||
|   Sidebar, | ||||
| @@ -43,14 +43,17 @@ import { useStorage } from '@vueuse/core' | ||||
| import { computed, ref, watch } from 'vue' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { useUserStore } from '@/stores/user' | ||||
| import { useConversationStore } from '@/stores/conversation' | ||||
|  | ||||
| defineProps({ | ||||
|   userTeams: { type: Array, default: () => [] }, | ||||
|   userViews: { type: Array, default: () => [] } | ||||
| }) | ||||
| const userStore = useUserStore() | ||||
| const conversationStore = useConversationStore() | ||||
| const settingsStore = useAppSettingsStore() | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
| const { t } = useI18n() | ||||
| const emit = defineEmits(['createView', 'editView', 'deleteView', 'createConversation']) | ||||
|  | ||||
| @@ -74,6 +77,58 @@ const deleteView = (view) => { | ||||
|   emit('deleteView', view) | ||||
| } | ||||
|  | ||||
| // Navigation methods with conversation retention | ||||
| const navigateToInbox = (type) => { | ||||
|   if (conversationStore.hasConversationOpen && conversationStore.conversation.data?.uuid) { | ||||
|     router.push({ | ||||
|       name: 'inbox-conversation', | ||||
|       params: { | ||||
|         type, | ||||
|         uuid: conversationStore.conversation.data.uuid | ||||
|       } | ||||
|     }) | ||||
|   } else { | ||||
|     router.push({ | ||||
|       name: 'inbox', | ||||
|       params: { type } | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| const navigateToTeamInbox = (teamID) => { | ||||
|   if (conversationStore.hasConversationOpen && conversationStore.conversation.data?.uuid) { | ||||
|     router.push({ | ||||
|       name: 'team-inbox-conversation', | ||||
|       params: { | ||||
|         teamID, | ||||
|         uuid: conversationStore.conversation.data.uuid | ||||
|       } | ||||
|     }) | ||||
|   } else { | ||||
|     router.push({ | ||||
|       name: 'team-inbox', | ||||
|       params: { teamID } | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| const navigateToViewInbox = (viewID) => { | ||||
|   if (conversationStore.hasConversationOpen && conversationStore.conversation.data?.uuid) { | ||||
|     router.push({ | ||||
|       name: 'view-inbox-conversation', | ||||
|       params: { | ||||
|         viewID, | ||||
|         uuid: conversationStore.conversation.data.uuid | ||||
|       } | ||||
|     }) | ||||
|   } else { | ||||
|     router.push({ | ||||
|       name: 'view-inbox', | ||||
|       params: { viewID } | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| const filteredAdminNavItems = computed(() => filterNavItems(adminNavItems, userStore.can)) | ||||
| const filteredReportsNavItems = computed(() => filterNavItems(reportsNavItems, userStore.can)) | ||||
| const filteredContactsNavItems = computed(() => filterNavItems(contactNavItems, userStore.can)) | ||||
| @@ -322,32 +377,32 @@ const viewInboxOpen = useStorage('viewInboxOpen', true) | ||||
|               </SidebarMenuItem> | ||||
|               <SidebarMenuItem> | ||||
|                 <SidebarMenuButton asChild :isActive="isActiveParent('/inboxes/assigned')"> | ||||
|                   <router-link :to="{ name: 'inbox', params: { type: 'assigned' } }"> | ||||
|                   <a href="#" @click.prevent="navigateToInbox('assigned')"> | ||||
|                     <User /> | ||||
|                     <span>{{ t('globals.terms.myInbox') }}</span> | ||||
|                   </router-link> | ||||
|                   </a> | ||||
|                 </SidebarMenuButton> | ||||
|               </SidebarMenuItem> | ||||
|  | ||||
|               <SidebarMenuItem> | ||||
|                 <SidebarMenuButton asChild :isActive="isActiveParent('/inboxes/unassigned')"> | ||||
|                   <router-link :to="{ name: 'inbox', params: { type: 'unassigned' } }"> | ||||
|                   <a href="#" @click.prevent="navigateToInbox('unassigned')"> | ||||
|                     <CircleDashed /> | ||||
|                     <span> | ||||
|                       {{ t('globals.terms.unassigned') }} | ||||
|                     </span> | ||||
|                   </router-link> | ||||
|                   </a> | ||||
|                 </SidebarMenuButton> | ||||
|               </SidebarMenuItem> | ||||
|  | ||||
|               <SidebarMenuItem> | ||||
|                 <SidebarMenuButton asChild :isActive="isActiveParent('/inboxes/all')"> | ||||
|                   <router-link :to="{ name: 'inbox', params: { type: 'all' } }"> | ||||
|                   <a href="#" @click.prevent="navigateToInbox('all')"> | ||||
|                     <List /> | ||||
|                     <span> | ||||
|                       {{ t('globals.messages.all') }} | ||||
|                     </span> | ||||
|                   </router-link> | ||||
|                   </a> | ||||
|                 </SidebarMenuButton> | ||||
|               </SidebarMenuItem> | ||||
|  | ||||
| @@ -380,9 +435,9 @@ const viewInboxOpen = useStorage('viewInboxOpen', true) | ||||
|                           :is-active="route.params.teamID == team.id" | ||||
|                           asChild | ||||
|                         > | ||||
|                           <router-link :to="{ name: 'team-inbox', params: { teamID: team.id } }"> | ||||
|                           <a href="#" @click.prevent="navigateToTeamInbox(team.id)"> | ||||
|                             {{ team.emoji }}<span>{{ team.name }}</span> | ||||
|                           </router-link> | ||||
|                           </a> | ||||
|                         </SidebarMenuButton> | ||||
|                       </SidebarMenuSubItem> | ||||
|                     </SidebarMenuSub> | ||||
| @@ -423,7 +478,7 @@ const viewInboxOpen = useStorage('viewInboxOpen', true) | ||||
|                           :isActive="route.params.viewID == view.id" | ||||
|                           asChild | ||||
|                         > | ||||
|                           <router-link :to="{ name: 'view-inbox', params: { viewID: view.id } }"> | ||||
|                           <a href="#" @click.prevent="navigateToViewInbox(view.id)"> | ||||
|                             <span class="break-words w-32 truncate">{{ view.name }}</span> | ||||
|                             <SidebarMenuAction :showOnHover="true" class="mr-3"> | ||||
|                               <DropdownMenu> | ||||
| @@ -440,7 +495,7 @@ const viewInboxOpen = useStorage('viewInboxOpen', true) | ||||
|                                 </DropdownMenuContent> | ||||
|                               </DropdownMenu> | ||||
|                             </SidebarMenuAction> | ||||
|                           </router-link> | ||||
|                           </a> | ||||
|                         </SidebarMenuButton> | ||||
|                       </SidebarMenuSubItem> | ||||
|                     </SidebarMenuSub> | ||||
|   | ||||
| @@ -62,7 +62,7 @@ | ||||
|                   :checked="!!selectedDays[day]" | ||||
|                   @update:checked="handleDayToggle(day, $event)" | ||||
|                 /> | ||||
|                 <Label :for="day" class="font-medium text-gray-800">{{ day }}</Label> | ||||
|                 <Label :for="day" class="font-medium">{{ day }}</Label> | ||||
|               </div> | ||||
|               <div class="flex space-x-2 items-center"> | ||||
|                 <div class="flex flex-col items-start"> | ||||
| @@ -156,7 +156,7 @@ | ||||
|         </div> | ||||
|         <DialogFooter> | ||||
|           <Button :disabled="!holidayName || !holidayDate" @click="saveHoliday"> | ||||
|             {{ t('globals.messages.saveChanges') }} | ||||
|             {{ t('globals.messages.add') }} | ||||
|           </Button> | ||||
|         </DialogFooter> | ||||
|       </DialogContent> | ||||
| @@ -231,9 +231,16 @@ const { t } = useI18n() | ||||
|  | ||||
| const form = useForm({ | ||||
|   validationSchema: toTypedSchema(createFormSchema(t)), | ||||
|   initialValues: props.initialValues | ||||
|   initialValues: { | ||||
|     is_always_open: true | ||||
|   } | ||||
| }) | ||||
|  | ||||
| // Sync form field with local state | ||||
| const syncHoursToForm = () => { | ||||
|   form.setFieldValue('hours', { ...hours.value }) | ||||
| } | ||||
|  | ||||
| const saveHoliday = () => { | ||||
|   holidays.push({ | ||||
|     name: holidayName.value, | ||||
| @@ -252,21 +259,15 @@ const deleteHoliday = (item) => { | ||||
| } | ||||
|  | ||||
| const handleDayToggle = (day, checked) => { | ||||
|   selectedDays.value = { | ||||
|     ...selectedDays.value, | ||||
|     [day]: checked | ||||
|   selectedDays.value[day] = checked | ||||
|  | ||||
|   if (checked) { | ||||
|     hours.value[day] = hours.value[day] || { open: '09:00', close: '17:00' } | ||||
|   } else { | ||||
|     delete hours.value[day] | ||||
|   } | ||||
|  | ||||
|   if (checked && !hours.value[day]) { | ||||
|     hours.value[day] = { open: '09:00', close: '17:00' } | ||||
|   } else if (!checked) { | ||||
|     const newHours = { ...hours.value } | ||||
|     delete newHours[day] | ||||
|     hours.value = newHours | ||||
|   } | ||||
|  | ||||
|   // Sync with form values | ||||
|   form.setFieldValue('hours', { ...hours.value }) | ||||
|   syncHoursToForm() | ||||
| } | ||||
|  | ||||
| const updateHours = (day, type, value) => { | ||||
| @@ -274,50 +275,48 @@ const updateHours = (day, type, value) => { | ||||
|     hours.value[day] = { open: '09:00', close: '17:00' } | ||||
|   } | ||||
|   hours.value[day][type] = value | ||||
|  | ||||
|   // Sync with form values | ||||
|   form.setFieldValue('hours', { ...hours.value }) | ||||
|   syncHoursToForm() | ||||
| } | ||||
|  | ||||
| const onSubmit = form.handleSubmit((values) => { | ||||
|   const businessHours = | ||||
|     values.is_always_open === true | ||||
|       ? {} | ||||
|       : Object.keys(selectedDays.value) | ||||
|           .filter((day) => selectedDays.value[day]) | ||||
|           .reduce((acc, day) => { | ||||
|             acc[day] = hours.value[day] | ||||
|             return acc | ||||
|           }, {}) | ||||
|   const businessHours = values.is_always_open === true ? {} : { ...hours.value } | ||||
|  | ||||
|   const finalValues = { | ||||
|     ...values, | ||||
|     is_always_open: values.is_always_open, | ||||
|     hours: businessHours, | ||||
|     holidays: holidays | ||||
|     holidays: [...holidays] | ||||
|   } | ||||
|   props.submitForm(finalValues) | ||||
| }) | ||||
|  | ||||
| // Watch for initial values | ||||
| watch( | ||||
|   () => props.initialValues, | ||||
|   (newValues) => { | ||||
|     if (!newValues || Object.keys(newValues).length === 0) { | ||||
|       return | ||||
|     } | ||||
|     // Set business hours if provided | ||||
|     if (newValues.is_always_open === false) { | ||||
|       hours.value = newValues.hours || {} | ||||
|       selectedDays.value = Object.keys(hours.value).reduce((acc, day) => { | ||||
| // Initialize state from props | ||||
| const initializeFromValues = (values) => { | ||||
|   if (!values) return | ||||
|  | ||||
|   // Reset state | ||||
|   hours.value = {} | ||||
|   selectedDays.value = {} | ||||
|   holidays.length = 0 | ||||
|  | ||||
|   // Set hours and selected days | ||||
|   if (values.hours && typeof values.hours === 'object') { | ||||
|     hours.value = { ...values.hours } | ||||
|     selectedDays.value = Object.keys(values.hours).reduce((acc, day) => { | ||||
|       acc[day] = true | ||||
|       return acc | ||||
|     }, {}) | ||||
|   } | ||||
|     // Set other form values | ||||
|     form.setValues(newValues) | ||||
|     holidays.length = 0 | ||||
|     holidays.push(...(newValues.holidays || [])) | ||||
|   }, | ||||
|   { deep: true } | ||||
| ) | ||||
|  | ||||
|   // Set holidays | ||||
|   if (values.holidays) { | ||||
|     holidays.push(...values.holidays) | ||||
|   } | ||||
|  | ||||
|   // Update form | ||||
|   form.setValues(values) | ||||
|   syncHoursToForm() | ||||
| } | ||||
|  | ||||
| // Watch for initial values | ||||
| watch(() => props.initialValues, initializeFromValues, { immediate: true, deep: true }) | ||||
| </script> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/ | ||||
| export const createFormSchema = (t) => z.object({ | ||||
|     name: z.string().min(1, t('globals.messages.required')), | ||||
|     description: z.string().min(1, t('globals.messages.required')), | ||||
|     is_always_open: z.boolean().default(true), | ||||
|     is_always_open: z.boolean(), | ||||
|     hours: z.record( | ||||
|         z.object({ | ||||
|             open: z.string().regex(timeRegex, t('form.error.time.invalid')), | ||||
|   | ||||
| @@ -20,7 +20,7 @@ export const createColumns = (t) => [ | ||||
|     }, | ||||
|     cell: function ({ row }) { | ||||
|       const url = row.getValue('url') | ||||
|       return h('div', { class: 'text-center font-mono text-sm max-w-sm truncate' }, url) | ||||
|       return h('div', { class: 'text-center font-mono mt-1 max-w-sm truncate' }, url) | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|   | ||||
| @@ -66,11 +66,11 @@ | ||||
|       <Tooltip> | ||||
|         <TooltipTrigger> | ||||
|           <span class="text-muted-foreground text-xs mt-1"> | ||||
|             {{ format(message.updated_at, 'h:mm a') }} | ||||
|             {{ formatMessageTimestamp(message.created_at) }} | ||||
|           </span> | ||||
|         </TooltipTrigger> | ||||
|         <TooltipContent> | ||||
|           {{ format(message.updated_at, "MMMM dd, yyyy 'at' HH:mm") }} | ||||
|           {{ formatFullTimestamp(message.created_at) }} | ||||
|         </TooltipContent> | ||||
|       </Tooltip> | ||||
|     </div> | ||||
| @@ -79,12 +79,12 @@ | ||||
|  | ||||
| <script setup> | ||||
| import { computed } from 'vue' | ||||
| import { format } from 'date-fns' | ||||
| import { useConversationStore } from '@/stores/conversation' | ||||
| import { Lock, RotateCcw, Check } from 'lucide-vue-next' | ||||
| import { revertCIDToImageSrc } from '@/utils/strings' | ||||
| import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' | ||||
| import { Spinner } from '@/components/ui/spinner' | ||||
| import { formatMessageTimestamp, formatFullTimestamp } from '@/utils/datetime' | ||||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' | ||||
| import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue' | ||||
| import MessageEnvelope from './MessageEnvelope.vue' | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|           <Letter | ||||
|             :html="sanitizedMessageContent" | ||||
|             :allowedSchemas="['cid', 'https', 'http', 'mailto']" | ||||
|             class="mb-1 native-html" | ||||
|             class="mb-1 native-html break-all" | ||||
|             :class="{ 'mb-3': message.attachments.length > 0 }" | ||||
|           /> | ||||
|  | ||||
| @@ -60,12 +60,12 @@ | ||||
|       <Tooltip> | ||||
|         <TooltipTrigger> | ||||
|           <span class="text-muted-foreground text-xs mt-1"> | ||||
|             {{ format(message.updated_at, 'h:mm a') }} | ||||
|             {{ formatMessageTimestamp(message.created_at) }} | ||||
|           </span> | ||||
|         </TooltipTrigger> | ||||
|         <TooltipContent> | ||||
|           <p> | ||||
|             {{ format(message.updated_at, "MMMM dd, yyyy 'at' HH:mm") }} | ||||
|             {{ formatFullTimestamp(message.created_at) }} | ||||
|           </p> | ||||
|         </TooltipContent> | ||||
|       </Tooltip> | ||||
| @@ -75,11 +75,11 @@ | ||||
|  | ||||
| <script setup> | ||||
| import { computed, ref } from 'vue' | ||||
| import { format } from 'date-fns' | ||||
| import { useConversationStore } from '@/stores/conversation' | ||||
| import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' | ||||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' | ||||
| import { Letter } from 'vue-letter' | ||||
| import { formatMessageTimestamp, formatFullTimestamp } from '@/utils/datetime' | ||||
| import { useAppSettingsStore } from '@/stores/appSettings' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue' | ||||
|   | ||||
| @@ -1,33 +1,32 @@ | ||||
| <template> | ||||
|   <div class="max-w-5xl mx-auto p-6 min-h-screen"> | ||||
|     <div class="space-y-8"> | ||||
|       <div | ||||
|         v-for="(items, type) in results" | ||||
|         :key="type" | ||||
|         class="bg-card rounded shadow overflow-hidden" | ||||
|       > | ||||
|         <!-- Header for each section --> | ||||
|         <h2 | ||||
|           class="bg-primary dark:bg-primary text-lg font-bold text-white dark:text-primary-foreground py-2 px-6 capitalize" | ||||
|         > | ||||
|           {{ type }} | ||||
|         </h2> | ||||
|     <Tabs :default-value="defaultTab" v-model="activeTab"> | ||||
|       <TabsList class="grid w-full mb-6" :class="tabsGridClass"> | ||||
|         <TabsTrigger v-for="(items, type) in results" :key="type" :value="type" class="capitalize"> | ||||
|           {{ type }} ({{ items.length }}) | ||||
|         </TabsTrigger> | ||||
|       </TabsList> | ||||
|  | ||||
|       <TabsContent v-for="(items, type) in results" :key="type" :value="type" class="mt-0"> | ||||
|         <div class="bg-background rounded border overflow-hidden"> | ||||
|           <!-- No results message --> | ||||
|         <div v-if="items.length === 0" class="p-6 text-gray-500 dark:text-muted-foreground"> | ||||
|           <div v-if="items.length === 0" class="p-8 text-center text-muted-foreground"> | ||||
|             <div class="text-lg font-medium mb-2"> | ||||
|               {{ | ||||
|                 $t('globals.messages.noResults', { | ||||
|                   name: type | ||||
|                 }) | ||||
|               }} | ||||
|             </div> | ||||
|             <div class="text-sm">{{ $t('search.adjustSearchTerms') }}</div> | ||||
|           </div> | ||||
|  | ||||
|           <!-- Results list --> | ||||
|         <div class="divide-y divide-gray-200 dark:divide-border"> | ||||
|           <div v-else class="divide-y divide-border"> | ||||
|             <div | ||||
|               v-for="item in items" | ||||
|               :key="item.id || item.uuid" | ||||
|             class="p-6 hover:bg-gray-100 dark:hover:bg-accent transition duration-300 ease-in-out group" | ||||
|               class="p-6 hover:bg-accent/50 transition duration-200 ease-in-out group" | ||||
|             > | ||||
|               <router-link | ||||
|                 :to="{ | ||||
| @@ -43,7 +42,7 @@ | ||||
|                   <div class="flex-grow"> | ||||
|                     <!-- Reference number --> | ||||
|                     <div | ||||
|                     class="text-sm font-semibold mb-2 group-hover:text-primary dark:group-hover:text-primary transition duration-300" | ||||
|                       class="text-sm font-semibold mb-2 text-muted-foreground group-hover:text-primary transition duration-200" | ||||
|                     > | ||||
|                       #{{ | ||||
|                         type === 'conversations' | ||||
| @@ -54,15 +53,18 @@ | ||||
|  | ||||
|                     <!-- Content --> | ||||
|                     <div | ||||
|                     class="text-gray-900 dark:text-card-foreground font-medium mb-2 text-lg group-hover:text-gray-950 dark:group-hover:text-foreground transition duration-300" | ||||
|                       class="text-foreground font-medium mb-2 text-lg group-hover:text-primary transition duration-200" | ||||
|                     > | ||||
|                       {{ | ||||
|                       truncateText(type === 'conversations' ? item.subject : item.text_content, 100) | ||||
|                         truncateText( | ||||
|                           type === 'conversations' ? item.subject : item.text_content, | ||||
|                           100 | ||||
|                         ) | ||||
|                       }} | ||||
|                     </div> | ||||
|  | ||||
|                     <!-- Timestamp --> | ||||
|                   <div class="text-sm text-gray-500 dark:text-muted-foreground flex items-center"> | ||||
|                     <div class="text-sm text-muted-foreground flex items-center"> | ||||
|                       <ClockIcon class="h-4 w-4 mr-1" /> | ||||
|                       {{ | ||||
|                         formatDate( | ||||
| @@ -74,10 +76,10 @@ | ||||
|  | ||||
|                   <!-- Right arrow icon --> | ||||
|                   <div | ||||
|                   class="bg-gray-200 dark:bg-secondary rounded-full p-2 group-hover:bg-primary dark:group-hover:bg-primary transition duration-300" | ||||
|                     class="bg-secondary rounded-full p-2 group-hover:bg-primary transition duration-200" | ||||
|                   > | ||||
|                     <ChevronRightIcon | ||||
|                     class="h-5 w-5 text-gray-700 dark:text-secondary-foreground group-hover:text-white dark:group-hover:text-primary-foreground" | ||||
|                       class="h-5 w-5 text-secondary-foreground group-hover:text-primary-foreground" | ||||
|                       aria-hidden="true" | ||||
|                     /> | ||||
|                   </div> | ||||
| @@ -86,20 +88,52 @@ | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|     </div> | ||||
|       </TabsContent> | ||||
|     </Tabs> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup> | ||||
| import { computed, ref, watch } from 'vue' | ||||
| import { ChevronRightIcon, ClockIcon } from 'lucide-vue-next' | ||||
| import { format, parseISO } from 'date-fns' | ||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' | ||||
|  | ||||
| defineProps({ | ||||
| const props = defineProps({ | ||||
|   results: { | ||||
|     type: Object, | ||||
|     required: true | ||||
|   } | ||||
| }) | ||||
|  | ||||
| // Get the first available tab as default | ||||
| const defaultTab = computed(() => { | ||||
|   const types = Object.keys(props.results) | ||||
|   return types.length > 0 ? types[0] : '' | ||||
| }) | ||||
|  | ||||
| const activeTab = ref('') | ||||
|  | ||||
| // Watch for changes in results and set the first tab as active | ||||
| watch( | ||||
|   () => props.results, | ||||
|   (newResults) => { | ||||
|     const types = Object.keys(newResults) | ||||
|     if (types.length > 0 && !activeTab.value) { | ||||
|       activeTab.value = types[0] | ||||
|     } | ||||
|   }, | ||||
|   { immediate: true } | ||||
| ) | ||||
|  | ||||
| // Dynamic grid class based on number of tabs | ||||
| const tabsGridClass = computed(() => { | ||||
|   const tabCount = Object.keys(props.results).length | ||||
|   if (tabCount <= 2) return 'grid-cols-2' | ||||
|   if (tabCount <= 3) return 'grid-cols-3' | ||||
|   if (tabCount <= 4) return 'grid-cols-4' | ||||
|   return 'grid-cols-5' | ||||
| }) | ||||
|  | ||||
| const formatDate = (dateString) => { | ||||
|   const date = parseISO(dateString) | ||||
|   return format(date, 'MMM d, yyyy HH:mm') | ||||
|   | ||||
| @@ -71,8 +71,8 @@ const routes = [ | ||||
|             path: '', | ||||
|             name: 'team-inbox', | ||||
|             component: () => import('@/views/inbox/InboxView.vue'), | ||||
|             meta: { title: 'Team inbox' } | ||||
|           }, | ||||
|             meta: { title: 'Team inbox' }, | ||||
|             children: [ | ||||
|               { | ||||
|                 path: 'conversation/:uuid', | ||||
|                 name: 'team-inbox-conversation', | ||||
| @@ -81,6 +81,8 @@ const routes = [ | ||||
|                 meta: { title: 'Team inbox', hidePageHeader: true } | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         path: '/inboxes/views/:viewID', | ||||
| @@ -93,8 +95,8 @@ const routes = [ | ||||
|             path: '', | ||||
|             name: 'view-inbox', | ||||
|             component: () => import('@/views/inbox/InboxView.vue'), | ||||
|             meta: { title: 'View inbox' } | ||||
|           }, | ||||
|             meta: { title: 'View inbox' }, | ||||
|             children: [ | ||||
|               { | ||||
|                 path: 'conversation/:uuid', | ||||
|                 name: 'view-inbox-conversation', | ||||
| @@ -103,6 +105,8 @@ const routes = [ | ||||
|                 meta: { title: 'View inbox', hidePageHeader: true } | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         path: 'inboxes/search', | ||||
|   | ||||
| @@ -378,9 +378,6 @@ export const useConversationStore = defineStore('conversation', () => { | ||||
|     if (conversations.listType !== listType || conversations.teamID !== teamID || conversations.viewID !== viewID) { | ||||
|       resetConversations() | ||||
|     } | ||||
|     if (conversations.listType !== listType) { | ||||
|       resetCurrentConversation() | ||||
|     } | ||||
|     if (listType) conversations.listType = listType | ||||
|     if (teamID) conversations.teamID = teamID | ||||
|     if (viewID) conversations.viewID = viewID | ||||
|   | ||||
| @@ -26,3 +26,11 @@ export const formatDuration = (seconds, showSeconds = true) => { | ||||
|   const secs = totalSeconds % 60 | ||||
|   return `${hours}h ${mins}m ${showSeconds ? `${secs}s` : ''}` | ||||
| } | ||||
|  | ||||
| export const formatMessageTimestamp = (time) => { | ||||
|   return format(time, 'd MMM, hh:mm a') | ||||
| } | ||||
|  | ||||
| export const formatFullTimestamp = (time) => { | ||||
|   return format(time, 'd MMM yyyy, hh:mm a') | ||||
| } | ||||
| @@ -12,7 +12,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { watch, onMounted, onUnmounted } from 'vue' | ||||
| import { watch, onMounted } from 'vue' | ||||
| import { useConversationStore } from '@/stores/conversation' | ||||
| import Conversation from '@/features/conversation/Conversation.vue' | ||||
| import ConversationSideBarWrapper from '@/features/conversation/sidebar/ConversationSideBarWrapper.vue' | ||||
| @@ -37,10 +37,6 @@ onMounted(() => { | ||||
|   if (props.uuid) fetchConversation(props.uuid) | ||||
| }) | ||||
|  | ||||
| onUnmounted(() => { | ||||
|   conversationStore.resetCurrentConversation() | ||||
| }) | ||||
|  | ||||
| // Watcher for UUID changes | ||||
| watch( | ||||
|   () => props.uuid, | ||||
|   | ||||
| @@ -25,7 +25,6 @@ onMounted(() => { | ||||
|     if (!conversationStore.getListStatus) { | ||||
|       conversationStore.setListStatus(CONVERSATION_DEFAULT_STATUSES.OPEN, false) | ||||
|     } | ||||
|     conversationStore.resetCurrentConversation() | ||||
|     conversationStore.fetchConversationsList(true, type.value) | ||||
|   } | ||||
|   // Fetch team list. | ||||
| @@ -34,7 +33,6 @@ onMounted(() => { | ||||
|     if (!conversationStore.getListStatus) { | ||||
|       conversationStore.setListStatus(CONVERSATION_DEFAULT_STATUSES.OPEN, false) | ||||
|     } | ||||
|     conversationStore.resetCurrentConversation() | ||||
|     conversationStore.fetchConversationsList( | ||||
|       true, | ||||
|       CONVERSATION_LIST_TYPE.TEAM_UNASSIGNED, | ||||
| @@ -45,7 +43,6 @@ onMounted(() => { | ||||
|   if (viewID.value) { | ||||
|     // Empty out list status as views are already filtered. | ||||
|     conversationStore.setListStatus('', false) | ||||
|     conversationStore.resetCurrentConversation() | ||||
|     conversationStore.fetchConversationsList(true, CONVERSATION_LIST_TYPE.VIEW, 0, [], viewID.value) | ||||
|   } | ||||
| }) | ||||
|   | ||||
| @@ -29,6 +29,7 @@ export default defineConfig({ | ||||
|     }, | ||||
|   }, | ||||
|   build: { | ||||
|     chunkSizeWarningLimit: 600, | ||||
|     rollupOptions: { | ||||
|       output: { | ||||
|         manualChunks: { | ||||
|   | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -38,7 +38,7 @@ require ( | ||||
| 	github.com/zerodha/simplesessions/v3 v3.0.0 | ||||
| 	golang.org/x/crypto v0.38.0 | ||||
| 	golang.org/x/mod v0.17.0 | ||||
| 	golang.org/x/oauth2 v0.21.0 | ||||
| 	golang.org/x/oauth2 v0.27.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
|   | ||||
							
								
								
									
										6
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
									
									
									
									
								
							| @@ -140,8 +140,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM= | ||||
| github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= | ||||
| github.com/rhnvrm/simples3 v0.9.0 h1:It6/glyqRTRooRzXcYOuqpKwjGg3lsXgNmeGgxpBtjA= | ||||
| github.com/rhnvrm/simples3 v0.9.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= | ||||
| github.com/rhnvrm/simples3 v0.9.1 h1:pYfEe2wTjx8B2zFzUdy4kZn3I3Otd9ZvzIhHkFR85kE= | ||||
| github.com/rhnvrm/simples3 v0.9.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| @@ -211,8 +209,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug | ||||
| golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||
| golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= | ||||
| golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= | ||||
| golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= | ||||
| golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||||
| golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= | ||||
| golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|   "globals.terms.setting": "Setting | Settings", | ||||
|   "globals.terms.template": "Template | Templates", | ||||
|   "globals.terms.rule": "Rule | Rules", | ||||
|   "globals.terms.businessHour": "Business Hour | Business Hours", | ||||
|   "globals.terms.businessHour": "Business hour | Business hours", | ||||
|   "globals.terms.priority": "Priority | Priorities", | ||||
|   "globals.terms.status": "Status | Statuses", | ||||
|   "globals.terms.secret": "Secret | Secrets", | ||||
| @@ -398,7 +398,7 @@ | ||||
|   "admin.general.allowedFileUploadExtensions.description": "Allowed file upload extensions. Use `*` to allow all file types.", | ||||
|   "admin.businessHours.unauthorized": "You do not have permission to view business hours.", | ||||
|   "admin.businessHours.setBusinessHours": "Set business hours", | ||||
|   "admin.businessHours.alwaysOpen24x7": "Always open 24/7", | ||||
|   "admin.businessHours.alwaysOpen24x7": "Always open (24/7)", | ||||
|   "admin.businessHours.customBusinessHours": "Custom business hours", | ||||
|   "admin.businessHours.hours.required": "Business hours are required", | ||||
|   "admin.businessHours.openClose.required": "Open and close time are required", | ||||
| @@ -533,7 +533,7 @@ | ||||
|   "admin.automation.event.message.incoming": "Incoming message", | ||||
|   "admin.automation.invalid": "Make sure you have atleast one action and one rule and their values are not empty.", | ||||
|   "admin.notification.restartApp": "Settings updated successfully, Please restart the app for changes to take effect.", | ||||
|   "admin.template.outgoingEmailTemplates": "Outgoing Email Templates", | ||||
|   "admin.template.outgoingEmailTemplates": "Outgoing email templates", | ||||
|   "admin.template.emailNotificationTemplates": "Email notification templates", | ||||
|   "admin.template.makeSureTemplateHasContent": "Make sure the template has {content} only once.", | ||||
|   "admin.template.onlyOneDefaultOutgoingTemplate": "You can have only one default outgoing email template.", | ||||
| @@ -568,6 +568,7 @@ | ||||
|   "search.noResultsForQuery": "No results found for query `{query}`. Try a different search term.", | ||||
|   "search.minQueryLength": " Please enter at least {length} characters to search.", | ||||
|   "search.searchBy": "Search by reference number, contact email address or messages in conversations.", | ||||
|   "search.adjustSearchTerms": "Try adjusting your search terms or filters.", | ||||
|   "sla.overdueBy": "Overdue by", | ||||
|   "sla.met": "SLA met", | ||||
|   "view.form.description": "Create and save custom filter views for quick access to your conversations.", | ||||
|   | ||||
| @@ -198,42 +198,45 @@ func (e *Engine) GetRule(id int) (models.RuleRecord, error) { | ||||
| } | ||||
|  | ||||
| // ToggleRule toggles the active status of a rule by ID. | ||||
| func (e *Engine) ToggleRule(id int) error { | ||||
| 	if _, err := e.q.ToggleRule.Exec(id); err != nil { | ||||
| func (e *Engine) ToggleRule(id int) (models.RuleRecord, error) { | ||||
| 	var result models.RuleRecord | ||||
| 	if err := e.q.ToggleRule.Get(&result, id); err != nil { | ||||
| 		e.lo.Error("error toggling rule", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, e.i18n.Ts("globals.messages.errorUpdating", "name", e.i18n.Ts("globals.terms.rule")), nil) | ||||
| 		return models.RuleRecord{}, envelope.NewError(envelope.GeneralError, e.i18n.Ts("globals.messages.errorUpdating", "name", e.i18n.Ts("globals.terms.rule")), nil) | ||||
| 	} | ||||
| 	// Reload rules. | ||||
| 	e.ReloadRules() | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // UpdateRule updates an existing rule. | ||||
| func (e *Engine) UpdateRule(id int, rule models.RuleRecord) error { | ||||
| func (e *Engine) UpdateRule(id int, rule models.RuleRecord) (models.RuleRecord, error) { | ||||
| 	if rule.Events == nil { | ||||
| 		rule.Events = pq.StringArray{} | ||||
| 	} | ||||
| 	if _, err := e.q.UpdateRule.Exec(id, rule.Name, rule.Description, rule.Type, rule.Events, rule.Rules, rule.Enabled); err != nil { | ||||
| 	var result models.RuleRecord | ||||
| 	if err := e.q.UpdateRule.Get(&result, id, rule.Name, rule.Description, rule.Type, rule.Events, rule.Rules, rule.Enabled); err != nil { | ||||
| 		e.lo.Error("error updating rule", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, e.i18n.Ts("globals.messages.errorUpdating", "name", e.i18n.Ts("globals.terms.rule")), nil) | ||||
| 		return models.RuleRecord{}, envelope.NewError(envelope.GeneralError, e.i18n.Ts("globals.messages.errorUpdating", "name", e.i18n.Ts("globals.terms.rule")), nil) | ||||
| 	} | ||||
| 	// Reload rules. | ||||
| 	e.ReloadRules() | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // CreateRule creates a new rule. | ||||
| func (e *Engine) CreateRule(rule models.RuleRecord) error { | ||||
| func (e *Engine) CreateRule(rule models.RuleRecord) (models.RuleRecord, error) { | ||||
| 	if rule.Events == nil { | ||||
| 		rule.Events = pq.StringArray{} | ||||
| 	} | ||||
| 	if _, err := e.q.InsertRule.Exec(rule.Name, rule.Description, rule.Type, rule.Events, rule.Rules); err != nil { | ||||
| 	var result models.RuleRecord | ||||
| 	if err := e.q.InsertRule.Get(&result, rule.Name, rule.Description, rule.Type, rule.Events, rule.Rules); err != nil { | ||||
| 		e.lo.Error("error creating rule", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, e.i18n.Ts("globals.messages.errorCreating", "name", e.i18n.Ts("globals.terms.rule")), nil) | ||||
| 		return models.RuleRecord{}, envelope.NewError(envelope.GeneralError, e.i18n.Ts("globals.messages.errorCreating", "name", e.i18n.Ts("globals.terms.rule")), nil) | ||||
| 	} | ||||
| 	// Reload rules. | ||||
| 	e.ReloadRules() | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // DeleteRule deletes a rule by ID. | ||||
| @@ -328,7 +331,7 @@ func (e *Engine) handleNewConversation(conversation cmodels.Conversation) { | ||||
| 	e.lo.Debug("handling new conversation for automation rule evaluation", "uuid", conversation.UUID) | ||||
| 	rules := e.filterRulesByType(models.RuleTypeNewConversation, "") | ||||
| 	if len(rules) == 0 { | ||||
| 		e.lo.Warn("no rules to evaluate for new conversation rule evaluation", "uuid", conversation.UUID) | ||||
| 		e.lo.Info("no rules to evaluate for new conversation rule evaluation", "uuid", conversation.UUID) | ||||
| 		return | ||||
| 	} | ||||
| 	e.evalConversationRules(rules, conversation) | ||||
| @@ -339,7 +342,7 @@ func (e *Engine) handleUpdateConversation(conversation cmodels.Conversation, eve | ||||
| 	e.lo.Debug("handling update conversation for automation rule evaluation", "uuid", conversation.UUID, "event_type", eventType) | ||||
| 	rules := e.filterRulesByType(models.RuleTypeConversationUpdate, eventType) | ||||
| 	if len(rules) == 0 { | ||||
| 		e.lo.Warn("no rules to evaluate for conversation update", "uuid", conversation.UUID, "event_type", eventType) | ||||
| 		e.lo.Info("no rules to evaluate for conversation update", "uuid", conversation.UUID, "event_type", eventType) | ||||
| 		return | ||||
| 	} | ||||
| 	e.evalConversationRules(rules, conversation) | ||||
| @@ -356,7 +359,7 @@ func (e *Engine) handleTimeTrigger() { | ||||
| 	} | ||||
| 	rules := e.filterRulesByType(models.RuleTypeTimeTrigger, "") | ||||
| 	if len(rules) == 0 { | ||||
| 		e.lo.Warn("no rules to evaluate for time trigger") | ||||
| 		e.lo.Info("no rules to evaluate for time trigger") | ||||
| 		return | ||||
| 	} | ||||
| 	e.lo.Info("fetched conversations for evaluating time triggers", "conversations_count", len(conversations), "rules_count", len(rules)) | ||||
|   | ||||
| @@ -24,10 +24,13 @@ DO UPDATE SET | ||||
|     rules = EXCLUDED.rules, | ||||
|     enabled = EXCLUDED.enabled, | ||||
|     updated_at = now() | ||||
| WHERE $1 > 0; | ||||
| WHERE $1 > 0 | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: insert-rule | ||||
| INSERT into automation_rules (name, description, type, events, rules) values ($1, $2, $3, $4, $5); | ||||
| INSERT into automation_rules (name, description, type, events, rules)  | ||||
| values ($1, $2, $3, $4, $5) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete-rule | ||||
| delete from automation_rules where id = $1; | ||||
| @@ -35,7 +38,8 @@ delete from automation_rules where id = $1; | ||||
| -- name: toggle-rule | ||||
| UPDATE automation_rules  | ||||
| SET enabled = NOT enabled, updated_at = NOW()  | ||||
| WHERE id = $1; | ||||
| WHERE id = $1 | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: update-rule-weight | ||||
| UPDATE automation_rules | ||||
|   | ||||
| @@ -80,12 +80,13 @@ func (m *Manager) GetAll() ([]models.BusinessHours, error) { | ||||
| } | ||||
|  | ||||
| // Create creates new business hours. | ||||
| func (m *Manager) Create(name string, description null.String, isAlwaysOpen bool, workingHrs, holidays types.JSONText) error { | ||||
| 	if _, err := m.q.InsertBusinessHours.Exec(name, description, isAlwaysOpen, workingHrs, holidays); err != nil { | ||||
| func (m *Manager) Create(name string, description null.String, isAlwaysOpen bool, workingHrs, holidays types.JSONText) (models.BusinessHours, error) { | ||||
| 	var result models.BusinessHours | ||||
| 	if err := m.q.InsertBusinessHours.Get(&result, name, description, isAlwaysOpen, workingHrs, holidays); err != nil { | ||||
| 		m.lo.Error("error inserting business hours", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.businessHour}"), nil) | ||||
| 		return models.BusinessHours{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.businessHour}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes business hours by ID. | ||||
| @@ -101,10 +102,11 @@ func (m *Manager) Delete(id int) error { | ||||
| } | ||||
|  | ||||
| // Update updates business hours by ID. | ||||
| func (m *Manager) Update(id int, name string, description null.String, isAlwaysOpen bool, workingHrs, holidays types.JSONText) error { | ||||
| 	if _, err := m.q.UpdateBusinessHours.Exec(id, name, description, isAlwaysOpen, workingHrs, holidays); err != nil { | ||||
| func (m *Manager) Update(id int, name string, description null.String, isAlwaysOpen bool, workingHrs, holidays types.JSONText) (models.BusinessHours, error) { | ||||
| 	var result models.BusinessHours | ||||
| 	if err := m.q.UpdateBusinessHours.Get(&result, id, name, description, isAlwaysOpen, workingHrs, holidays); err != nil { | ||||
| 		m.lo.Error("error updating business hours", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.businessHour}"), nil) | ||||
| 		return models.BusinessHours{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.businessHour}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|   | ||||
| @@ -27,7 +27,8 @@ INSERT INTO business_hours ( | ||||
|         hours, | ||||
|         holidays | ||||
|     ) | ||||
| VALUES ($1, $2, $3, $4, $5); | ||||
| VALUES ($1, $2, $3, $4, $5) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete-business-hours | ||||
| DELETE FROM business_hours | ||||
| @@ -41,4 +42,5 @@ SET "name" = $2, | ||||
|     hours = $5, | ||||
|     holidays = $6, | ||||
|     updated_at = NOW() | ||||
| WHERE id = $1; | ||||
| WHERE id = $1 | ||||
| RETURNING *; | ||||
| @@ -920,14 +920,17 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio | ||||
| 		} | ||||
| 		return m.UpdateConversationStatus(conv.UUID, statusID, "", "", user) | ||||
| 	case amodels.ActionSendPrivateNote: | ||||
| 		return m.SendPrivateNote([]mmodels.Media{}, user.ID, conv.UUID, action.Value[0]) | ||||
| 		_, err := m.SendPrivateNote([]mmodels.Media{}, user.ID, conv.UUID, action.Value[0]) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("sending private note: %w", err) | ||||
| 		} | ||||
| 	case amodels.ActionReply: | ||||
| 		// Make recipient list. | ||||
| 		to, cc, bcc, err := m.makeRecipients(conv.ID, conv.Contact.Email.String, conv.InboxMail) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("making recipients for reply action: %w", err) | ||||
| 		} | ||||
| 		return m.SendReply( | ||||
| 		_, err = m.SendReply( | ||||
| 			[]mmodels.Media{}, | ||||
| 			conv.InboxID, | ||||
| 			user.ID, | ||||
| @@ -938,6 +941,9 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio | ||||
| 			bcc, | ||||
| 			map[string]any{}, /**meta**/ | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("sending reply: %w", err) | ||||
| 		} | ||||
| 	case amodels.ActionSetSLA: | ||||
| 		slaID, err := strconv.Atoi(action.Value[0]) | ||||
| 		if err != nil { | ||||
| @@ -951,6 +957,7 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio | ||||
| 	default: | ||||
| 		return fmt.Errorf("unknown action: %s", action.Type) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // RemoveConversationAssignee removes the assignee from the conversation. | ||||
| @@ -991,10 +998,16 @@ func (m *Manager) SendCSATReply(actorUserID int, conversation models.Conversatio | ||||
| 	// Make recipient list. | ||||
| 	to, cc, bcc, err := m.makeRecipients(conversation.ID, conversation.Contact.Email.String, conversation.InboxMail) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("making recipients for CSAT reply: %w", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	return m.SendReply(nil /**media**/, conversation.InboxID, actorUserID, conversation.UUID, message, to, cc, bcc, meta) | ||||
| 	// Send CSAT reply. | ||||
| 	_, err = m.SendReply(nil /**media**/, conversation.InboxID, actorUserID, conversation.UUID, message, to, cc, bcc, meta) | ||||
| 	if err != nil { | ||||
| 		m.lo.Error("error sending CSAT reply", "conversation_uuid", conversation.UUID, "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DeleteConversation deletes a conversation. | ||||
|   | ||||
| @@ -356,8 +356,7 @@ func (m *Manager) MarkMessageAsPending(uuid string) error { | ||||
| } | ||||
|  | ||||
| // SendPrivateNote inserts a private message in a conversation. | ||||
| func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversationUUID, content string) error { | ||||
| 	// Insert Message. | ||||
| func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversationUUID, content string) (models.Message, error) { | ||||
| 	message := models.Message{ | ||||
| 		ConversationUUID: conversationUUID, | ||||
| 		SenderID:         senderID, | ||||
| @@ -369,18 +368,25 @@ func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversat | ||||
| 		Private:          true, | ||||
| 		Media:            media, | ||||
| 	} | ||||
| 	return m.InsertMessage(&message) | ||||
| 	if err := m.InsertMessage(&message); err != nil { | ||||
| 		return models.Message{}, err | ||||
| 	} | ||||
| 	return message, nil | ||||
| } | ||||
|  | ||||
| // SendReply inserts a reply message in a conversation. | ||||
| func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conversationUUID, content string, to, cc, bcc []string, meta map[string]interface{}) error { | ||||
| 	// Save to, cc and bcc in meta. | ||||
| func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conversationUUID, content string, to, cc, bcc []string, meta map[string]interface{}) (models.Message, error) { | ||||
| 	var ( | ||||
| 		message = models.Message{} | ||||
| 	) | ||||
|  | ||||
| 	// Clear empty fields in to, cc, bcc. | ||||
| 	to = stringutil.RemoveEmpty(to) | ||||
| 	cc = stringutil.RemoveEmpty(cc) | ||||
| 	bcc = stringutil.RemoveEmpty(bcc) | ||||
|  | ||||
| 	if len(to) == 0 { | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.empty", "name", "`to`"), nil) | ||||
| 		return message, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.empty", "name", "`to`"), nil) | ||||
| 	} | ||||
| 	meta["to"] = to | ||||
|  | ||||
| @@ -393,22 +399,22 @@ func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conver | ||||
|  | ||||
| 	metaJSON, err := json.Marshal(meta) | ||||
| 	if err != nil { | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorMarshalling", "name", "{globals.terms.meta}"), nil) | ||||
| 		return message, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorMarshalling", "name", "{globals.terms.meta}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	// Generage unique source ID i.e. message-id for email. | ||||
| 	inbox, err := m.inboxStore.GetDBRecord(inboxID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return message, err | ||||
| 	} | ||||
| 	sourceID, err := stringutil.GenerateEmailMessageID(conversationUUID, inbox.From) | ||||
| 	if err != nil { | ||||
| 		m.lo.Error("error generating source message id", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.T("conversation.errorGeneratingMessageID"), nil) | ||||
| 		return message, envelope.NewError(envelope.GeneralError, m.i18n.T("conversation.errorGeneratingMessageID"), nil) | ||||
| 	} | ||||
|  | ||||
| 	// Insert Message. | ||||
| 	message := models.Message{ | ||||
| 	message = models.Message{ | ||||
| 		ConversationUUID: conversationUUID, | ||||
| 		SenderID:         senderID, | ||||
| 		Type:             models.MessageOutgoing, | ||||
| @@ -421,16 +427,17 @@ func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conver | ||||
| 		Meta:             metaJSON, | ||||
| 		SourceID:         null.StringFrom(sourceID), | ||||
| 	} | ||||
| 	return m.InsertMessage(&message) | ||||
| 	if err := m.InsertMessage(&message); err != nil { | ||||
| 		return models.Message{}, err | ||||
| 	} | ||||
| 	return message, nil | ||||
| } | ||||
|  | ||||
| // InsertMessage inserts a message and attaches the media to the message. | ||||
| func (m *Manager) InsertMessage(message *models.Message) error { | ||||
| 	// Private message is always sent. | ||||
| 	if message.Private { | ||||
| 		message.Status = models.MessageStatusSent | ||||
| 	} | ||||
|  | ||||
| 	if len(message.Meta) == 0 || string(message.Meta) == "null" { | ||||
| 		message.Meta = json.RawMessage(`{}`) | ||||
| 	} | ||||
| @@ -438,14 +445,16 @@ func (m *Manager) InsertMessage(message *models.Message) error { | ||||
| 	// Convert HTML content to text for search. | ||||
| 	message.TextContent = stringutil.HTML2Text(message.Content) | ||||
|  | ||||
| 	// Insert Message. | ||||
| 	if err := m.q.InsertMessage.QueryRow(message.Type, message.Status, message.ConversationID, message.ConversationUUID, message.Content, message.TextContent, message.SenderID, message.SenderType, | ||||
| 		message.Private, message.ContentType, message.SourceID, message.Meta).Scan(&message.ID, &message.UUID, &message.CreatedAt); err != nil { | ||||
| 	// Insert and scan the message into the struct. | ||||
| 	if err := m.q.InsertMessage.Get(message, | ||||
| 		message.Type, message.Status, message.ConversationID, message.ConversationUUID, | ||||
| 		message.Content, message.TextContent, message.SenderID, message.SenderType, | ||||
| 		message.Private, message.ContentType, message.SourceID, message.Meta); err != nil { | ||||
| 		m.lo.Error("error inserting message in db", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorInserting", "name", "{globals.terms.message}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	// Attach message to the media. | ||||
| 	// Attach just inserted message to the media. | ||||
| 	for _, media := range message.Media { | ||||
| 		m.mediaStore.Attach(media.ID, mmodels.ModelMessages, message.ID) | ||||
| 	} | ||||
| @@ -465,13 +474,19 @@ func (m *Manager) InsertMessage(message *models.Message) error { | ||||
| 	// Broadcast new message. | ||||
| 	m.BroadcastNewMessage(message) | ||||
|  | ||||
| 	// Refetch message and send webhook event for message created. | ||||
| 	updatedMessage, err := m.GetMessage(message.UUID) | ||||
| 	// Refetch message if this message has media attachments, as media gets linked after inserting the message. | ||||
| 	if len(message.Media) > 0 { | ||||
| 		refetchedMessage, err := m.GetMessage(message.UUID) | ||||
| 		if err != nil { | ||||
| 		m.lo.Error("error fetching updated message for webhook event", "uuid", message.UUID, "error", err) | ||||
| 			m.lo.Error("error fetching message after insert", "error", err) | ||||
| 		} else { | ||||
| 		m.webhookStore.TriggerEvent(wmodels.EventMessageCreated, updatedMessage) | ||||
| 			// Replace the message in the struct with the refetched message. | ||||
| 			*message = refetchedMessage | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Trigger webhook for new message created. | ||||
| 	m.webhookStore.TriggerEvent(wmodels.EventMessageCreated, message) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -530,7 +545,7 @@ func (m *Manager) InsertConversationActivity(activityType, conversationUUID, new | ||||
| 	content, err := m.getMessageActivityContent(activityType, newValue, actor.FullName()) | ||||
| 	if err != nil { | ||||
| 		m.lo.Error("error could not generate activity content", "error", err) | ||||
| 		return err | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorGenerating", "name", "{globals.terms.activityMessage}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	message := models.Message{ | ||||
|   | ||||
| @@ -438,6 +438,7 @@ SELECT | ||||
|     m.sender_type, | ||||
|     m.sender_id, | ||||
|     m.meta, | ||||
|     c.uuid as conversation_uuid, | ||||
|     COALESCE( | ||||
|         json_agg( | ||||
|             json_build_object( | ||||
| @@ -452,10 +453,11 @@ SELECT | ||||
|         '[]'::json | ||||
|     ) AS attachments | ||||
| FROM conversation_messages m | ||||
| INNER JOIN conversations c ON c.id = m.conversation_id | ||||
| LEFT JOIN media ON media.model_type = 'messages' AND media.model_id = m.id | ||||
| WHERE m.uuid = $1 | ||||
| GROUP BY  | ||||
|     m.id, m.created_at, m.updated_at, m.status, m.type, m.content, m.uuid, m.private, m.sender_type | ||||
|     m.id, m.created_at, m.updated_at, m.status, m.type, m.content, m.uuid, m.private, m.sender_type, c.uuid | ||||
| ORDER BY m.created_at; | ||||
|  | ||||
| -- name: get-messages | ||||
| @@ -509,9 +511,9 @@ inserted_msg AS ( | ||||
|        $1, $2, (SELECT id FROM conversation_id), | ||||
|        $5, $6, $7, $8, $9, $10, $11, $12 | ||||
|    ) | ||||
|    RETURNING id, uuid, created_at, conversation_id | ||||
|    RETURNING * | ||||
| ) | ||||
| SELECT id, uuid, created_at FROM inserted_msg; | ||||
| SELECT * FROM inserted_msg; | ||||
|  | ||||
| -- name: message-exists-by-source-id | ||||
| SELECT conversation_id | ||||
|   | ||||
| @@ -12,10 +12,10 @@ select id, | ||||
| from conversation_statuses; | ||||
|  | ||||
| -- name: insert-status | ||||
| INSERT into conversation_statuses(name) values ($1); | ||||
| INSERT into conversation_statuses(name) values ($1) RETURNING *; | ||||
|  | ||||
| -- name: delete-status | ||||
| DELETE from conversation_statuses where id = $1; | ||||
|  | ||||
| -- name: update-status | ||||
| UPDATE conversation_statuses set name = $2 where id = $1; | ||||
| UPDATE conversation_statuses set name = $2 where id = $1 RETURNING *; | ||||
| @@ -70,15 +70,16 @@ func (m *Manager) GetAll() ([]models.Status, error) { | ||||
| } | ||||
|  | ||||
| // Create creates a new status. | ||||
| func (m *Manager) Create(name string) error { | ||||
| func (m *Manager) Create(name string) (models.Status, error) { | ||||
| 	var status models.Status | ||||
| 	if err := m.validateStatusName(name); err != nil { | ||||
| 		return err | ||||
| 		return status, err | ||||
| 	} | ||||
| 	if _, err := m.q.InsertStatus.Exec(name); err != nil { | ||||
| 	if err := m.q.InsertStatus.Get(&status, name); err != nil { | ||||
| 		m.lo.Error("error inserting status", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", m.i18n.T("globals.terms.status")), nil) | ||||
| 		return status, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", m.i18n.T("globals.terms.status")), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return status, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes a status by ID. | ||||
| @@ -104,25 +105,26 @@ func (m *Manager) Delete(id int) error { | ||||
| } | ||||
|  | ||||
| // Update updates a status by id. | ||||
| func (m *Manager) Update(id int, name string) error { | ||||
| func (m *Manager) Update(id int, name string) (models.Status, error) { | ||||
| 	var updatedStatus models.Status | ||||
| 	if err := m.validateStatusName(name); err != nil { | ||||
| 		return err | ||||
| 		return updatedStatus, err | ||||
| 	} | ||||
| 	// Disallow updating of default statuses. | ||||
| 	status, err := m.Get(id) | ||||
| 	if err != nil { | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorFetching", "name", m.i18n.Ts("globals.terms.status")), nil) | ||||
| 		return updatedStatus, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorFetching", "name", m.i18n.Ts("globals.terms.status")), nil) | ||||
| 	} | ||||
|  | ||||
| 	if slices.Contains(models.DefaultStatuses, status.Name) { | ||||
| 		return envelope.NewError(envelope.InputError, m.i18n.T("conversationStatus.cannotUpdateDefault"), nil) | ||||
| 		return updatedStatus, envelope.NewError(envelope.InputError, m.i18n.T("conversationStatus.cannotUpdateDefault"), nil) | ||||
| 	} | ||||
|  | ||||
| 	if _, err := m.q.UpdateStatus.Exec(id, name); err != nil { | ||||
| 	if err := m.q.UpdateStatus.Get(&updatedStatus, id, name); err != nil { | ||||
| 		m.lo.Error("error updating status", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", m.i18n.Ts("globals.terms.status")), nil) | ||||
| 		return updatedStatus, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", m.i18n.Ts("globals.terms.status")), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return updatedStatus, nil | ||||
| } | ||||
|  | ||||
| // Get retrieves a status by ID. | ||||
|   | ||||
| @@ -78,24 +78,26 @@ func (m *Manager) GetAll(appliesTo string) ([]models.CustomAttribute, error) { | ||||
| } | ||||
|  | ||||
| // Create creates a new custom attribute. | ||||
| func (m *Manager) Create(attr models.CustomAttribute) error { | ||||
| 	if _, err := m.q.InsertCustomAttribute.Exec(attr.AppliesTo, attr.Name, attr.Description, attr.Key, pq.Array(attr.Values), attr.DataType, attr.Regex, attr.RegexHint); err != nil { | ||||
| func (m *Manager) Create(attr models.CustomAttribute) (models.CustomAttribute, error) { | ||||
| 	var createdAttr models.CustomAttribute | ||||
| 	if err := m.q.InsertCustomAttribute.Get(&createdAttr, attr.AppliesTo, attr.Name, attr.Description, attr.Key, pq.Array(attr.Values), attr.DataType, attr.Regex, attr.RegexHint); err != nil { | ||||
| 		if dbutil.IsUniqueViolationError(err) { | ||||
| 			return envelope.NewError(envelope.InputError, m.i18n.Ts("globals.messages.errorAlreadyExists", "name", m.i18n.P("globals.terms.customAttribute")), nil) | ||||
| 			return models.CustomAttribute{}, envelope.NewError(envelope.InputError, m.i18n.Ts("globals.messages.errorAlreadyExists", "name", m.i18n.P("globals.terms.customAttribute")), nil) | ||||
| 		} | ||||
| 		m.lo.Error("error inserting custom attribute", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.customAttribute}"), nil) | ||||
| 		return models.CustomAttribute{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.customAttribute}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return createdAttr, nil | ||||
| } | ||||
|  | ||||
| // Update updates a custom attribute by ID. | ||||
| func (m *Manager) Update(id int, attr models.CustomAttribute) error { | ||||
| 	if _, err := m.q.UpdateCustomAttribute.Exec(id, attr.AppliesTo, attr.Name, attr.Description, pq.Array(attr.Values), attr.Regex, attr.RegexHint); err != nil { | ||||
| func (m *Manager) Update(id int, attr models.CustomAttribute) (models.CustomAttribute, error) { | ||||
| 	var updatedAttr models.CustomAttribute | ||||
| 	if err := m.q.UpdateCustomAttribute.Get(&updatedAttr, id, attr.AppliesTo, attr.Name, attr.Description, pq.Array(attr.Values), attr.Regex, attr.RegexHint); err != nil { | ||||
| 		m.lo.Error("error updating custom attribute", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.customAttribute}"), nil) | ||||
| 		return models.CustomAttribute{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.customAttribute}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return updatedAttr, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes a custom attribute by ID. | ||||
|   | ||||
| @@ -43,6 +43,7 @@ INSERT INTO | ||||
|     custom_attribute_definitions (applies_to, name, description, key, values, data_type, regex, regex_hint) | ||||
| VALUES | ||||
|     ($1, $2, $3, $4, $5, $6, $7, $8) | ||||
| RETURNING * | ||||
|  | ||||
| -- name: delete-custom-attribute | ||||
| DELETE FROM | ||||
| @@ -62,4 +63,5 @@ SET | ||||
|     regex_hint = $7, | ||||
|     updated_at = NOW() | ||||
| WHERE | ||||
|     id = $1; | ||||
|     id = $1 | ||||
| RETURNING *; | ||||
| @@ -169,12 +169,13 @@ func (m *Manager) GetAll() ([]imodels.Inbox, error) { | ||||
| } | ||||
|  | ||||
| // Create creates an inbox in the DB. | ||||
| func (m *Manager) Create(inbox imodels.Inbox) error { | ||||
| 	if _, err := m.queries.InsertInbox.Exec(inbox.Channel, inbox.Config, inbox.Name, inbox.From, inbox.CSATEnabled); err != nil { | ||||
| func (m *Manager) Create(inbox imodels.Inbox) (imodels.Inbox, error) { | ||||
| 	var createdInbox imodels.Inbox | ||||
| 	if err := m.queries.InsertInbox.Get(&createdInbox, inbox.Channel, inbox.Config, inbox.Name, inbox.From, inbox.CSATEnabled); err != nil { | ||||
| 		m.lo.Error("error creating inbox", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.inbox}"), nil) | ||||
| 		return imodels.Inbox{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.inbox}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return createdInbox, nil | ||||
| } | ||||
|  | ||||
| // InitInboxes initializes and registers active inboxes with the manager. | ||||
| @@ -254,10 +255,10 @@ func (m *Manager) Reload(ctx context.Context, initFn initFn) error { | ||||
| } | ||||
|  | ||||
| // Update updates an inbox in the DB. | ||||
| func (m *Manager) Update(id int, inbox imodels.Inbox) error { | ||||
| func (m *Manager) Update(id int, inbox imodels.Inbox) (imodels.Inbox, error) { | ||||
| 	current, err := m.GetDBRecord(id) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return imodels.Inbox{}, err | ||||
| 	} | ||||
|  | ||||
| 	// Preserve existing passwords if update has empty password | ||||
| @@ -274,22 +275,22 @@ func (m *Manager) Update(id int, inbox imodels.Inbox) error { | ||||
|  | ||||
| 		if err := json.Unmarshal(current.Config, ¤tCfg); err != nil { | ||||
| 			m.lo.Error("error unmarshalling current config", "id", id, "error", err) | ||||
| 			return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.config}"), nil) | ||||
| 			return imodels.Inbox{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.config}"), nil) | ||||
| 		} | ||||
| 		if len(inbox.Config) == 0 { | ||||
| 			return envelope.NewError(envelope.InputError, m.i18n.Ts("globals.messages.empty", "name", "{globals.terms.config}"), nil) | ||||
| 			return imodels.Inbox{}, envelope.NewError(envelope.InputError, m.i18n.Ts("globals.messages.empty", "name", "{globals.terms.config}"), nil) | ||||
| 		} | ||||
| 		if err := json.Unmarshal(inbox.Config, &updateCfg); err != nil { | ||||
| 			m.lo.Error("error unmarshalling update config", "id", id, "error", err) | ||||
| 			return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.config}"), nil) | ||||
| 			return imodels.Inbox{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.config}"), nil) | ||||
| 		} | ||||
|  | ||||
| 		if len(updateCfg.IMAP) == 0 { | ||||
| 			return envelope.NewError(envelope.InputError, m.i18n.T("inbox.emptyIMAP"), nil) | ||||
| 			return imodels.Inbox{}, envelope.NewError(envelope.InputError, m.i18n.T("inbox.emptyIMAP"), nil) | ||||
| 		} | ||||
|  | ||||
| 		if len(updateCfg.SMTP) == 0 { | ||||
| 			return envelope.NewError(envelope.InputError, m.i18n.T("inbox.emptySMTP"), nil) | ||||
| 			return imodels.Inbox{}, envelope.NewError(envelope.InputError, m.i18n.T("inbox.emptySMTP"), nil) | ||||
| 		} | ||||
|  | ||||
| 		// Preserve existing IMAP passwords if update has empty password | ||||
| @@ -308,27 +309,29 @@ func (m *Manager) Update(id int, inbox imodels.Inbox) error { | ||||
| 		updatedConfig, err := json.Marshal(updateCfg) | ||||
| 		if err != nil { | ||||
| 			m.lo.Error("error marshalling updated config", "id", id, "error", err) | ||||
| 			return err | ||||
| 			return imodels.Inbox{}, err | ||||
| 		} | ||||
| 		inbox.Config = updatedConfig | ||||
| 	} | ||||
|  | ||||
| 	// Update the inbox in the DB. | ||||
| 	if _, err := m.queries.Update.Exec(id, inbox.Channel, inbox.Config, inbox.Name, inbox.From, inbox.CSATEnabled, inbox.Enabled); err != nil { | ||||
| 	var updatedInbox imodels.Inbox | ||||
| 	if err := m.queries.Update.Get(&updatedInbox, id, inbox.Channel, inbox.Config, inbox.Name, inbox.From, inbox.CSATEnabled, inbox.Enabled); err != nil { | ||||
| 		m.lo.Error("error updating inbox", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.inbox}"), nil) | ||||
| 		return imodels.Inbox{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.inbox}"), nil) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	return updatedInbox, nil | ||||
| } | ||||
|  | ||||
| // Toggle toggles the status of an inbox in the DB. | ||||
| func (m *Manager) Toggle(id int) error { | ||||
| 	if _, err := m.queries.Toggle.Exec(id); err != nil { | ||||
| func (m *Manager) Toggle(id int) (imodels.Inbox, error) { | ||||
| 	var updatedInbox imodels.Inbox | ||||
| 	if err := m.queries.Toggle.Get(&updatedInbox, id); err != nil { | ||||
| 		m.lo.Error("error toggling inbox", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.inbox}"), nil) | ||||
| 		return imodels.Inbox{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.inbox}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return updatedInbox, nil | ||||
| } | ||||
|  | ||||
| // SoftDelete soft deletes an inbox in the DB. | ||||
|   | ||||
| @@ -8,6 +8,7 @@ SELECT id, created_at, updated_at, name, channel, enabled from inboxes where del | ||||
| INSERT INTO inboxes | ||||
| (channel, config, "name", "from", csat_enabled) | ||||
| VALUES($1, $2, $3, $4, $5) | ||||
| RETURNING * | ||||
|  | ||||
| -- name: get-inbox | ||||
| SELECT * from inboxes where id = $1 and deleted_at is NULL; | ||||
| @@ -15,7 +16,8 @@ SELECT * from inboxes where id = $1 and deleted_at is NULL; | ||||
| -- name: update | ||||
| UPDATE inboxes | ||||
| set channel = $2, config = $3, "name" = $4, "from" = $5, csat_enabled = $6, enabled = $7, updated_at = now() | ||||
| where id = $1 and deleted_at is NULL; | ||||
| where id = $1 and deleted_at is NULL | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: soft-delete | ||||
| UPDATE inboxes set deleted_at = now(), config = '{}' where id = $1 and deleted_at is NULL; | ||||
| @@ -23,4 +25,5 @@ UPDATE inboxes set deleted_at = now(), config = '{}' where id = $1 and deleted_a | ||||
| -- name: toggle | ||||
| UPDATE inboxes  | ||||
| SET enabled = NOT enabled, updated_at = NOW()  | ||||
| WHERE id = $1; | ||||
| WHERE id = $1 | ||||
| RETURNING *; | ||||
| @@ -68,26 +68,25 @@ func (m *Manager) Get(id int) (models.Macro, error) { | ||||
| } | ||||
|  | ||||
| // Create adds a new macro. | ||||
| func (m *Manager) Create(name, messageContent string, userID, teamID *int, visibility string, visibleWhen []string, actions json.RawMessage) error { | ||||
| 	_, err := m.q.Create.Exec(name, messageContent, userID, teamID, visibility, pq.StringArray(visibleWhen), actions) | ||||
| func (m *Manager) Create(name, messageContent string, userID, teamID *int, visibility string, visibleWhen []string, actions json.RawMessage) (models.Macro, error) { | ||||
| 	var createdMacro models.Macro | ||||
| 	err := m.q.Create.Get(&createdMacro, name, messageContent, userID, teamID, visibility, pq.StringArray(visibleWhen), actions) | ||||
| 	if err != nil { | ||||
| 		m.lo.Error("error creating macro", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.macro}"), nil) | ||||
| 		return models.Macro{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.macro}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return createdMacro, nil | ||||
| } | ||||
|  | ||||
| // Update modifies an existing macro. | ||||
| func (m *Manager) Update(id int, name, messageContent string, userID, teamID *int, visibility string, visibleWhen []string, actions json.RawMessage) error { | ||||
| 	result, err := m.q.Update.Exec(id, name, messageContent, userID, teamID, visibility, pq.StringArray(visibleWhen), actions) | ||||
| func (m *Manager) Update(id int, name, messageContent string, userID, teamID *int, visibility string, visibleWhen []string, actions json.RawMessage) (models.Macro, error) { | ||||
| 	var updatedMacro models.Macro | ||||
| 	err := m.q.Update.Get(&updatedMacro, id, name, messageContent, userID, teamID, visibility, pq.StringArray(visibleWhen), actions) | ||||
| 	if err != nil { | ||||
| 		m.lo.Error("error updating macro", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.macro}"), nil) | ||||
| 		return models.Macro{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.macro}"), nil) | ||||
| 	} | ||||
| 	if rows, _ := result.RowsAffected(); rows == 0 { | ||||
| 		return envelope.NewError(envelope.NotFoundError, m.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.macro}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return updatedMacro, nil | ||||
| } | ||||
|  | ||||
| // GetAll returns all macros. | ||||
|   | ||||
| @@ -38,7 +38,8 @@ ORDER BY | ||||
| INSERT INTO | ||||
|     macros (name, message_content, user_id, team_id, visibility, visible_when, actions) | ||||
| VALUES | ||||
|     ($1, $2, $3, $4, $5, $6, $7); | ||||
|     ($1, $2, $3, $4, $5, $6, $7) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: update | ||||
| UPDATE | ||||
| @@ -53,7 +54,8 @@ SET | ||||
|     actions = $8, | ||||
|     updated_at = NOW() | ||||
| WHERE | ||||
|     id = $1; | ||||
|     id = $1 | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete | ||||
| DELETE FROM | ||||
|   | ||||
| @@ -125,28 +125,30 @@ func (o *Manager) GetAllEnabled() ([]models.OIDC, error) { | ||||
| } | ||||
|  | ||||
| // Create adds a new oidc. | ||||
| func (o *Manager) Create(oidc models.OIDC) error { | ||||
| 	if _, err := o.q.InsertOIDC.Exec(oidc.Name, oidc.Provider, oidc.ProviderURL, oidc.ClientID, oidc.ClientSecret); err != nil { | ||||
| func (o *Manager) Create(oidc models.OIDC) (models.OIDC, error) { | ||||
| 	var createdOIDC models.OIDC | ||||
| 	if err := o.q.InsertOIDC.Get(&createdOIDC, oidc.Name, oidc.Provider, oidc.ProviderURL, oidc.ClientID, oidc.ClientSecret); err != nil { | ||||
| 		o.lo.Error("error inserting oidc", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, o.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.oidcProvider}"), nil) | ||||
| 		return models.OIDC{}, envelope.NewError(envelope.GeneralError, o.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.oidcProvider}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return createdOIDC, nil | ||||
| } | ||||
|  | ||||
| // Update updates a oidc by id. | ||||
| func (o *Manager) Update(id int, oidc models.OIDC) error { | ||||
| func (o *Manager) Update(id int, oidc models.OIDC) (models.OIDC, error) { | ||||
| 	current, err := o.Get(id, true) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return models.OIDC{}, err | ||||
| 	} | ||||
| 	if oidc.ClientSecret == "" { | ||||
| 		oidc.ClientSecret = current.ClientSecret | ||||
| 	} | ||||
| 	if _, err := o.q.UpdateOIDC.Exec(id, oidc.Name, oidc.Provider, oidc.ProviderURL, oidc.ClientID, oidc.ClientSecret, oidc.Enabled); err != nil { | ||||
| 	var updatedOIDC models.OIDC | ||||
| 	if err := o.q.UpdateOIDC.Get(&updatedOIDC, id, oidc.Name, oidc.Provider, oidc.ProviderURL, oidc.ClientID, oidc.ClientSecret, oidc.Enabled); err != nil { | ||||
| 		o.lo.Error("error updating oidc", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, o.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.oidcProvider}"), nil) | ||||
| 		return models.OIDC{}, envelope.NewError(envelope.GeneralError, o.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.oidcProvider}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return updatedOIDC, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes a oidc by its id. | ||||
|   | ||||
| @@ -9,12 +9,14 @@ SELECT * FROM oidc WHERE id = $1; | ||||
|  | ||||
| -- name: insert-oidc | ||||
| INSERT INTO oidc (name, provider, provider_url, client_id, client_secret)  | ||||
| VALUES ($1, $2, $3, $4, $5); | ||||
| VALUES ($1, $2, $3, $4, $5) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: update-oidc | ||||
| UPDATE oidc  | ||||
| SET name = $2, provider = $3, provider_url = $4, client_id = $5, client_secret = $6, enabled = $7, updated_at = now() | ||||
| WHERE id = $1; | ||||
| WHERE id = $1 | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete-oidc | ||||
| DELETE FROM oidc WHERE id = $1; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ SELECT * FROM roles where id = $1; | ||||
| DELETE FROM roles where id = $1; | ||||
|  | ||||
| -- name: insert-role | ||||
| INSERT INTO roles (name, description, permissions) VALUES ($1, $2, $3); | ||||
| INSERT INTO roles (name, description, permissions) VALUES ($1, $2, $3) RETURNING *; | ||||
|  | ||||
| -- name: update-role | ||||
| UPDATE roles SET name = $2, description = $3, permissions = $4 WHERE id = $1; | ||||
| UPDATE roles SET name = $2, description = $3, permissions = $4 WHERE id = $1 RETURNING *; | ||||
| @@ -101,47 +101,49 @@ func (u *Manager) Delete(id int) error { | ||||
| } | ||||
|  | ||||
| // Create creates a new role. | ||||
| func (u *Manager) Create(r models.Role) error { | ||||
| func (u *Manager) Create(r models.Role) (models.Role, error) { | ||||
| 	validPermissions, err := u.filterValidPermissions(r.Permissions) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return models.Role{}, err | ||||
| 	} | ||||
| 	if len(validPermissions) == 0 { | ||||
| 		return envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.empty", "name", u.i18n.P("globals.terms.permission")), nil) | ||||
| 		return models.Role{}, envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.empty", "name", u.i18n.P("globals.terms.permission")), nil) | ||||
| 	} | ||||
| 	if _, err := u.q.Insert.Exec(r.Name, r.Description, pq.Array(validPermissions)); err != nil { | ||||
| 	var result models.Role | ||||
| 	if err := u.q.Insert.Get(&result, r.Name, r.Description, pq.Array(validPermissions)); err != nil { | ||||
| 		if dbutil.IsUniqueViolationError(err) { | ||||
| 			return envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.errorAlreadyExists", "name", "{globals.terms.role}"), nil) | ||||
| 			return models.Role{}, envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.errorAlreadyExists", "name", "{globals.terms.role}"), nil) | ||||
| 		} | ||||
| 		u.lo.Error("error inserting role", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.role}"), nil) | ||||
| 		return models.Role{}, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.role}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Update updates an existing role. | ||||
| func (u *Manager) Update(id int, r models.Role) error { | ||||
| func (u *Manager) Update(id int, r models.Role) (models.Role, error) { | ||||
| 	validPermissions, err := u.filterValidPermissions(r.Permissions) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return models.Role{}, err | ||||
| 	} | ||||
| 	if len(validPermissions) == 0 { | ||||
| 		return envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.empty", "name", u.i18n.P("globals.terms.permission")), nil) | ||||
| 		return models.Role{}, envelope.NewError(envelope.InputError, u.i18n.Ts("globals.messages.empty", "name", u.i18n.P("globals.terms.permission")), nil) | ||||
| 	} | ||||
| 	// Disallow updating `Admin` role, as the main System login requires it. | ||||
| 	role, err := u.Get(id) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return models.Role{}, err | ||||
| 	} | ||||
| 	if role.Name == models.RoleAdmin { | ||||
| 		return envelope.NewError(envelope.InputError, u.i18n.T("admin.role.cannotModifyAdminRole"), nil) | ||||
| 		return models.Role{}, envelope.NewError(envelope.InputError, u.i18n.T("admin.role.cannotModifyAdminRole"), nil) | ||||
| 	} | ||||
|  | ||||
| 	if _, err := u.q.Update.Exec(id, r.Name, r.Description, pq.Array(validPermissions)); err != nil { | ||||
| 	var result models.Role | ||||
| 	if err := u.q.Update.Get(&result, id, r.Name, r.Description, pq.Array(validPermissions)); err != nil { | ||||
| 		u.lo.Error("error updating role", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.role}"), nil) | ||||
| 		return models.Role{}, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.role}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // filterValidPermissions filters out invalid permissions, logs warnings for unknown permissions. | ||||
|   | ||||
| @@ -12,7 +12,8 @@ INSERT INTO sla_policies ( | ||||
|    resolution_time, | ||||
|    next_response_time, | ||||
|    notifications | ||||
| ) VALUES ($1, $2, $3, $4, $5, $6); | ||||
| ) VALUES ($1, $2, $3, $4, $5, $6) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: update-sla-policy | ||||
| UPDATE sla_policies SET | ||||
| @@ -23,7 +24,8 @@ UPDATE sla_policies SET | ||||
|    next_response_time = $6, | ||||
|    notifications = $7, | ||||
|    updated_at = NOW() | ||||
| WHERE id = $1; | ||||
| WHERE id = $1 | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete-sla-policy | ||||
| DELETE FROM sla_policies WHERE id = $1; | ||||
|   | ||||
| @@ -186,21 +186,23 @@ func (m *Manager) GetAll() ([]models.SLAPolicy, error) { | ||||
| } | ||||
|  | ||||
| // Create creates a new SLA policy. | ||||
| func (m *Manager) Create(name, description string, firstResponseTime, resolutionTime, nextResponseTime null.String, notifications models.SlaNotifications) error { | ||||
| 	if _, err := m.q.InsertSLAPolicy.Exec(name, description, firstResponseTime, resolutionTime, nextResponseTime, notifications); err != nil { | ||||
| func (m *Manager) Create(name, description string, firstResponseTime, resolutionTime, nextResponseTime null.String, notifications models.SlaNotifications) (models.SLAPolicy, error) { | ||||
| 	var result models.SLAPolicy | ||||
| 	if err := m.q.InsertSLAPolicy.Get(&result, name, description, firstResponseTime, resolutionTime, nextResponseTime, notifications); err != nil { | ||||
| 		m.lo.Error("error inserting SLA", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.sla}"), nil) | ||||
| 		return models.SLAPolicy{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.sla}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Update updates a SLA policy. | ||||
| func (m *Manager) Update(id int, name, description string, firstResponseTime, resolutionTime, nextResponseTime null.String, notifications models.SlaNotifications) error { | ||||
| 	if _, err := m.q.UpdateSLAPolicy.Exec(id, name, description, firstResponseTime, resolutionTime, nextResponseTime, notifications); err != nil { | ||||
| func (m *Manager) Update(id int, name, description string, firstResponseTime, resolutionTime, nextResponseTime null.String, notifications models.SlaNotifications) (models.SLAPolicy, error) { | ||||
| 	var result models.SLAPolicy | ||||
| 	if err := m.q.UpdateSLAPolicy.Get(&result, id, name, description, firstResponseTime, resolutionTime, nextResponseTime, notifications); err != nil { | ||||
| 		m.lo.Error("error updating SLA", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.sla}"), nil) | ||||
| 		return models.SLAPolicy{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.sla}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes an SLA policy. | ||||
|   | ||||
| @@ -11,7 +11,8 @@ from | ||||
| INSERT into | ||||
|     tags (name) | ||||
| values | ||||
|     ($1); | ||||
|     ($1) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete-tag | ||||
| DELETE from | ||||
| @@ -26,4 +27,5 @@ set | ||||
|     name = $2, | ||||
|     updated_at = now() | ||||
| where | ||||
|     id = $1; | ||||
|     id = $1 | ||||
| RETURNING *; | ||||
| @@ -64,15 +64,16 @@ func (t *Manager) GetAll() ([]models.Tag, error) { | ||||
| } | ||||
|  | ||||
| // Create creates a new tag. | ||||
| func (t *Manager) Create(name string) error { | ||||
| 	if _, err := t.q.InsertTag.Exec(name); err != nil { | ||||
| func (t *Manager) Create(name string) (models.Tag, error) { | ||||
| 	var tag models.Tag | ||||
| 	if err := t.q.InsertTag.Get(&tag, name); err != nil { | ||||
| 		if dbutil.IsUniqueViolationError(err) { | ||||
| 			return envelope.NewError(envelope.ConflictError, t.i18n.Ts("globals.messages.errorAlreadyExists", "name", "{globals.terms.tag}"), nil) | ||||
| 			return tag, envelope.NewError(envelope.ConflictError, t.i18n.Ts("globals.messages.errorAlreadyExists", "name", "{globals.terms.tag}"), nil) | ||||
| 		} | ||||
| 		t.lo.Error("error inserting tag", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, t.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.tag}"), nil) | ||||
| 		return tag, envelope.NewError(envelope.GeneralError, t.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.tag}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return tag, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes a tag by ID. | ||||
| @@ -85,10 +86,11 @@ func (t *Manager) Delete(id int) error { | ||||
| } | ||||
|  | ||||
| // Update updates a tag by id. | ||||
| func (t *Manager) Update(id int, name string) error { | ||||
| 	if _, err := t.q.UpdateTag.Exec(id, name); err != nil { | ||||
| func (t *Manager) Update(id int, name string) (models.Tag, error) { | ||||
| 	var tag models.Tag | ||||
| 	if err := t.q.UpdateTag.Get(&tag, id, name); err != nil { | ||||
| 		t.lo.Error("error updating tag", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, t.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.tag}"), nil) | ||||
| 		return tag, envelope.NewError(envelope.GeneralError, t.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.tag}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return tag, nil | ||||
| } | ||||
|   | ||||
| @@ -18,10 +18,10 @@ JOIN teams t ON t.id = tm.team_id | ||||
| WHERE t.id = $1 AND u.deleted_at IS NULL AND u.type = 'agent' AND u.enabled = true; | ||||
|  | ||||
| -- name: insert-team | ||||
| INSERT INTO teams (name, timezone, conversation_assignment_type, business_hours_id, sla_policy_id, emoji, max_auto_assigned_conversations) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id; | ||||
| INSERT INTO teams (name, timezone, conversation_assignment_type, business_hours_id, sla_policy_id, emoji, max_auto_assigned_conversations) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; | ||||
|  | ||||
| -- name: update-team | ||||
| UPDATE teams set name = $2, timezone = $3, conversation_assignment_type = $4, business_hours_id = $5, sla_policy_id = $6, emoji = $7, max_auto_assigned_conversations = $8, updated_at = now() where id = $1; | ||||
| UPDATE teams set name = $2, timezone = $3, conversation_assignment_type = $4, business_hours_id = $5, sla_policy_id = $6, emoji = $7, max_auto_assigned_conversations = $8, updated_at = now() where id = $1 RETURNING *; | ||||
|  | ||||
| -- name: upsert-user-teams | ||||
| WITH delete_old_teams AS ( | ||||
|   | ||||
| @@ -105,24 +105,26 @@ func (u *Manager) Get(id int) (models.Team, error) { | ||||
| } | ||||
|  | ||||
| // Create creates a new team. | ||||
| func (u *Manager) Create(name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string, maxAutoAssignedConversations int) error { | ||||
| 	if _, err := u.q.InsertTeam.Exec(name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji, maxAutoAssignedConversations); err != nil { | ||||
| func (u *Manager) Create(name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string, maxAutoAssignedConversations int) (models.Team, error) { | ||||
| 	var team models.Team | ||||
| 	if err := u.q.InsertTeam.Get(&team, name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji, maxAutoAssignedConversations); err != nil { | ||||
| 		if dbutil.IsUniqueViolationError(err) { | ||||
| 			return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorAlreadyExists", "name", "{globals.terms.team}"), nil) | ||||
| 			return team, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorAlreadyExists", "name", "{globals.terms.team}"), nil) | ||||
| 		} | ||||
| 		u.lo.Error("error inserting team", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.team}"), nil) | ||||
| 		return team, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.team}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return team, nil | ||||
| } | ||||
|  | ||||
| // Update updates an existing team. | ||||
| func (u *Manager) Update(id int, name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string, maxAutoAssignedConversations int) error { | ||||
| 	if _, err := u.q.UpdateTeam.Exec(id, name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji, maxAutoAssignedConversations); err != nil { | ||||
| func (u *Manager) Update(id int, name, timezone, conversationAssignmentType string, businessHrsID, slaPolicyID null.Int, emoji string, maxAutoAssignedConversations int) (models.Team, error) { | ||||
| 	var team models.Team | ||||
| 	if err := u.q.UpdateTeam.Get(&team, id, name, timezone, conversationAssignmentType, businessHrsID, slaPolicyID, emoji, maxAutoAssignedConversations); err != nil { | ||||
| 		u.lo.Error("error updating team", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.team}"), nil) | ||||
| 		return team, envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.team}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return team, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes a team by ID also deletes all the team members and unassigns all the conversations belonging to the team. | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| -- name: insert | ||||
| INSERT INTO templates ("name", body, is_default, subject, type) | ||||
| VALUES ($1, $2, $3, $4, $5); | ||||
| VALUES ($1, $2, $3, $4, $5) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: update | ||||
| WITH u AS ( | ||||
| @@ -13,11 +14,9 @@ WITH u AS ( | ||||
|         type = $6::template_type, | ||||
|         updated_at = NOW() | ||||
|     WHERE id = $1 | ||||
|     RETURNING id | ||||
|     RETURNING * | ||||
| ) | ||||
| UPDATE templates | ||||
| SET is_default = FALSE | ||||
| WHERE id != $1 AND $4 = TRUE; | ||||
| SELECT * FROM u LIMIT 1; | ||||
|  | ||||
| -- name: get-default | ||||
| SELECT id, type, name, body, subject FROM templates WHERE is_default is TRUE; | ||||
|   | ||||
| @@ -66,27 +66,29 @@ func New(lo *logf.Logger, db *sqlx.DB, webTpls *template.Template, tpls *templat | ||||
| } | ||||
|  | ||||
| // Update updates a new template with the given name, and body. | ||||
| func (m *Manager) Update(id int, t models.Template) error { | ||||
| 	if _, err := m.q.UpdateTemplate.Exec(id, t.Name, t.Body, t.IsDefault, t.Subject, t.Type); err != nil { | ||||
| func (m *Manager) Update(id int, t models.Template) (models.Template, error) { | ||||
| 	var result models.Template | ||||
| 	if err := m.q.UpdateTemplate.Get(&result, id, t.Name, t.Body, t.IsDefault, t.Subject, t.Type); err != nil { | ||||
| 		m.lo.Error("error updating template", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.template}"), nil) | ||||
| 		return models.Template{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.template}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Create creates a template. | ||||
| func (m *Manager) Create(t models.Template) error { | ||||
| func (m *Manager) Create(t models.Template) (models.Template, error) { | ||||
| 	if t.IsDefault { | ||||
| 		t.Type = TypeEmailOutgoing | ||||
| 	} | ||||
| 	if _, err := m.q.InsertTemplate.Exec(t.Name, t.Body, t.IsDefault, t.Subject, t.Type); err != nil { | ||||
| 	var result models.Template | ||||
| 	if err := m.q.InsertTemplate.Get(&result, t.Name, t.Body, t.IsDefault, t.Subject, t.Type); err != nil { | ||||
| 		if dbutil.IsUniqueViolationError(err) && t.IsDefault { | ||||
| 			return envelope.NewError(envelope.GeneralError, m.i18n.T("template.defaultTemplateAlreadyExists"), nil) | ||||
| 			return models.Template{}, envelope.NewError(envelope.GeneralError, m.i18n.T("template.defaultTemplateAlreadyExists"), nil) | ||||
| 		} | ||||
| 		m.lo.Error("error inserting template", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.template}"), nil) | ||||
| 		return models.Template{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.template}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // GetAll returns all templates by type. | ||||
|   | ||||
| @@ -8,7 +8,8 @@ FROM views WHERE user_id = $1; | ||||
|  | ||||
| -- name: insert-view | ||||
| INSERT INTO views (name, filters, user_id) | ||||
| VALUES ($1, $2, $3); | ||||
| VALUES ($1, $2, $3) | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete-view | ||||
| DELETE FROM views | ||||
| @@ -18,3 +19,4 @@ WHERE id = $1; | ||||
| UPDATE views | ||||
| SET name = $2, filters = $3, updated_at = NOW() | ||||
| WHERE id = $1 | ||||
| RETURNING * | ||||
|   | ||||
| @@ -77,21 +77,23 @@ func (v *Manager) GetUsersViews(userID int) ([]models.View, error) { | ||||
| } | ||||
|  | ||||
| // Create creates a new view. | ||||
| func (v *Manager) Create(name string, filter []byte, userID int) error { | ||||
| 	if _, err := v.q.InsertView.Exec(name, filter, userID); err != nil { | ||||
| func (v *Manager) Create(name string, filter []byte, userID int) (models.View, error) { | ||||
| 	var createdView models.View | ||||
| 	if err := v.q.InsertView.Get(&createdView, name, filter, userID); err != nil { | ||||
| 		v.lo.Error("error inserting view", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, v.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.view}"), nil) | ||||
| 		return models.View{}, envelope.NewError(envelope.GeneralError, v.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.view}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return createdView, nil | ||||
| } | ||||
|  | ||||
| // Update updates a view by id. | ||||
| func (v *Manager) Update(id int, name string, filter []byte) error { | ||||
| 	if _, err := v.q.UpdateView.Exec(id, name, filter); err != nil { | ||||
| func (v *Manager) Update(id int, name string, filter []byte) (models.View, error) { | ||||
| 	var updatedView models.View | ||||
| 	if err := v.q.UpdateView.Get(&updatedView, id, name, filter); err != nil { | ||||
| 		v.lo.Error("error updating view", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, v.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.view}"), nil) | ||||
| 		return models.View{}, envelope.NewError(envelope.GeneralError, v.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.view}"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return updatedView, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes a view by ID. | ||||
|   | ||||
| @@ -15,7 +15,7 @@ type Webhook struct { | ||||
| 	Name      string         `db:"name" json:"name"` | ||||
| 	URL       string         `db:"url" json:"url"` | ||||
| 	Events    pq.StringArray `db:"events" json:"events"` | ||||
| 	Secret    string         `db:"secret" json:"secret,omitempty"` | ||||
| 	Secret    string         `db:"secret" json:"secret"` | ||||
| 	IsActive  bool           `db:"is_active" json:"is_active"` | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -64,7 +64,7 @@ INSERT INTO | ||||
|     webhooks (name, url, events, secret, is_active) | ||||
| VALUES | ||||
|     ($1, $2, $3, $4, $5) | ||||
| RETURNING id; | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: update-webhook | ||||
| UPDATE | ||||
| @@ -77,7 +77,8 @@ SET | ||||
|     is_active = $6, | ||||
|     updated_at = NOW() | ||||
| WHERE | ||||
|     id = $1; | ||||
|     id = $1 | ||||
| RETURNING *; | ||||
|  | ||||
| -- name: delete-webhook | ||||
| DELETE FROM | ||||
| @@ -92,4 +93,5 @@ SET | ||||
|     is_active = NOT is_active, | ||||
|     updated_at = NOW() | ||||
| WHERE | ||||
|     id = $1; | ||||
|     id = $1 | ||||
| RETURNING *; | ||||
|   | ||||
| @@ -89,7 +89,7 @@ func New(opts Opts) (*Manager, error) { | ||||
| 		db:            opts.DB, | ||||
| 		deliveryQueue: make(chan DeliveryTask, opts.QueueSize), | ||||
| 		httpClient: &http.Client{ | ||||
| 			Timeout: 10 * time.Second, | ||||
| 			Timeout: opts.Timeout, | ||||
| 			Transport: &http.Transport{ | ||||
| 				DialContext: (&net.Dialer{ | ||||
| 					Timeout:   3 * time.Second, | ||||
| @@ -127,25 +127,26 @@ func (m *Manager) Get(id int) (models.Webhook, error) { | ||||
| } | ||||
|  | ||||
| // Create creates a new webhook. | ||||
| func (m *Manager) Create(webhook models.Webhook) (int, error) { | ||||
| 	var id int | ||||
| 	if err := m.q.InsertWebhook.Get(&id, webhook.Name, webhook.URL, pq.Array(webhook.Events), webhook.Secret, webhook.IsActive); err != nil { | ||||
| func (m *Manager) Create(webhook models.Webhook) (models.Webhook, error) { | ||||
| 	var result models.Webhook | ||||
| 	if err := m.q.InsertWebhook.Get(&result, webhook.Name, webhook.URL, pq.Array(webhook.Events), webhook.Secret, webhook.IsActive); err != nil { | ||||
| 		if dbutil.IsUniqueViolationError(err) { | ||||
| 			return 0, envelope.NewError(envelope.ConflictError, m.i18n.Ts("globals.messages.errorAlreadyExists", "name", "webhook"), nil) | ||||
| 			return models.Webhook{}, envelope.NewError(envelope.ConflictError, m.i18n.Ts("globals.messages.errorAlreadyExists", "name", "webhook"), nil) | ||||
| 		} | ||||
| 		m.lo.Error("error inserting webhook", "error", err) | ||||
| 		return 0, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "webhook"), nil) | ||||
| 		return models.Webhook{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "webhook"), nil) | ||||
| 	} | ||||
| 	return id, nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Update updates a webhook by ID. | ||||
| func (m *Manager) Update(id int, webhook models.Webhook) error { | ||||
| 	if _, err := m.q.UpdateWebhook.Exec(id, webhook.Name, webhook.URL, pq.Array(webhook.Events), webhook.Secret, webhook.IsActive); err != nil { | ||||
| func (m *Manager) Update(id int, webhook models.Webhook) (models.Webhook, error) { | ||||
| 	var result models.Webhook | ||||
| 	if err := m.q.UpdateWebhook.Get(&result, id, webhook.Name, webhook.URL, pq.Array(webhook.Events), webhook.Secret, webhook.IsActive); err != nil { | ||||
| 		m.lo.Error("error updating webhook", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "webhook"), nil) | ||||
| 		return models.Webhook{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "webhook"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Delete deletes a webhook by ID. | ||||
| @@ -158,12 +159,13 @@ func (m *Manager) Delete(id int) error { | ||||
| } | ||||
|  | ||||
| // Toggle toggles the active status of a webhook by ID. | ||||
| func (m *Manager) Toggle(id int) error { | ||||
| 	if _, err := m.q.ToggleWebhook.Exec(id); err != nil { | ||||
| func (m *Manager) Toggle(id int) (models.Webhook, error) { | ||||
| 	var result models.Webhook | ||||
| 	if err := m.q.ToggleWebhook.Get(&result, id); err != nil { | ||||
| 		m.lo.Error("error toggling webhook", "error", err) | ||||
| 		return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "webhook"), nil) | ||||
| 		return models.Webhook{}, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorUpdating", "name", "webhook"), nil) | ||||
| 	} | ||||
| 	return nil | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // SendTestWebhook sends a test webhook to the specified webhook ID. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user