Merge remote-tracking branch 'upstream/master' into MM-47560
Этот коммит содержится в:
14
.github/codeql/codeql-config.yml
поставляемый
Обычный файл
14
.github/codeql/codeql-config.yml
поставляемый
Обычный файл
@@ -0,0 +1,14 @@
|
||||
name: "CodeQL config"
|
||||
|
||||
query-filters:
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- warning
|
||||
- recommendation
|
||||
- exclude:
|
||||
id: go/log-injection
|
||||
|
||||
paths-ignore:
|
||||
- templates
|
||||
- tests
|
||||
- 'api4/*_local.go'
|
||||
21
.github/workflows/codeql-analysis.yml
поставляемый
21
.github/workflows/codeql-analysis.yml
поставляемый
@@ -6,7 +6,7 @@ on:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '30 5,17 * * *'
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -20,17 +20,24 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript' ]
|
||||
language: [ 'go' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
debug: true
|
||||
debug: false
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
# Autobuild attempts to build any compiled languages
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# Perform Analysis
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
2
Makefile
2
Makefile
@@ -160,7 +160,7 @@ PLUGIN_PACKAGES += mattermost-plugin-jira-v2.4.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-nps-v1.2.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0
|
||||
PLUGIN_PACKAGES += focalboard-v7.4.2
|
||||
PLUGIN_PACKAGES += focalboard-v7.4.3
|
||||
PLUGIN_PACKAGES += mattermost-plugin-apps-v1.1.0
|
||||
|
||||
# Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target
|
||||
|
||||
@@ -1779,6 +1779,7 @@ func TestSearchGroupChannels(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteChannel(t *testing.T) {
|
||||
t.Skip("MM-47465")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
c := th.Client
|
||||
|
||||
@@ -158,14 +158,14 @@ func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// check if the email needs to be set
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
// this value will not be empty when both emails (user admin and CWS customer) are not business email and
|
||||
// we need to request a new email from the user via the request business email modal
|
||||
// a new business email was provided via the request business email modal
|
||||
var startTrialRequest *model.StartCloudTrialRequest
|
||||
if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -199,20 +199,20 @@ func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if appErr != nil {
|
||||
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
var emailToValidate *model.ValidateBusinessEmailRequest
|
||||
err = json.Unmarshal(bodyBytes, &emailToValidate)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -244,14 +244,14 @@ func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.R
|
||||
|
||||
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if userErr != nil {
|
||||
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, userErr.Error(), http.StatusInternalServerError)
|
||||
c.Err = userErr
|
||||
return
|
||||
}
|
||||
|
||||
// get the cloud customer email to validate if is a valid business email
|
||||
cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
emailErr := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email)
|
||||
|
||||
@@ -296,6 +296,20 @@ func Test_requestTrial(t *testing.T) {
|
||||
require.Equal(t, subscriptionChanged, subscription)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
})
|
||||
|
||||
t.Run("Empty body returns bad request", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
r, err := th.SystemAdminClient.DoAPIPutBytes("/cloud/request-trial", nil)
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_validateBusinessEmail(t *testing.T) {
|
||||
@@ -373,6 +387,20 @@ func Test_validateBusinessEmail(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, "200")
|
||||
})
|
||||
|
||||
t.Run("Empty body returns bad request", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-business-email", nil)
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_validateWorkspaceBusinessEmail(t *testing.T) {
|
||||
@@ -442,6 +470,39 @@ func Test_validateWorkspaceBusinessEmail(t *testing.T) {
|
||||
_, err := th.SystemAdminClient.ValidateWorkspaceBusinessEmail()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Error while grabbing the cloud customer returns bad request", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
cloudCustomerInfo := model.CloudCustomerInfo{
|
||||
Email: "badrequest@gmail.com",
|
||||
}
|
||||
|
||||
// return an error while getting the cloud customer so we validate the forbidden error return
|
||||
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(nil, errors.New("error while gettings the cloud customer"))
|
||||
|
||||
// required cloud mocks so the request doesn't fail
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(errors.New("invalid email"))
|
||||
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, th.SystemAdminUser.Email).Return(nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-workspace-business-email", nil)
|
||||
require.Error(t, err)
|
||||
closeBody(r)
|
||||
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCloudProducts(t *testing.T) {
|
||||
|
||||
241
api4/group.go
241
api4/group.go
@@ -18,82 +18,88 @@ import (
|
||||
|
||||
func (api *API) InitGroup() {
|
||||
// GET /api/v4/groups
|
||||
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(getGroups))).Methods("GET")
|
||||
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods("GET")
|
||||
|
||||
// POST /api/v4/groups
|
||||
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(createGroup))).Methods("POST")
|
||||
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(createGroup)).Methods("POST")
|
||||
|
||||
// GET /api/v4/groups/:group_id
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
|
||||
api.APISessionRequired(requireLicense(getGroup))).Methods("GET")
|
||||
api.APISessionRequired(getGroup)).Methods("GET")
|
||||
|
||||
// PUT /api/v4/groups/:group_id/patch
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch",
|
||||
api.APISessionRequired(requireLicense(patchGroup))).Methods("PUT")
|
||||
api.APISessionRequired(patchGroup)).Methods("PUT")
|
||||
|
||||
// POST /api/v4/groups/:group_id/teams/:team_id/link
|
||||
// POST /api/v4/groups/:group_id/channels/:channel_id/link
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
|
||||
api.APISessionRequired(requireLicense(linkGroupSyncable))).Methods("POST")
|
||||
api.APISessionRequired(linkGroupSyncable)).Methods("POST")
|
||||
|
||||
// DELETE /api/v4/groups/:group_id/teams/:team_id/link
|
||||
// DELETE /api/v4/groups/:group_id/channels/:channel_id/link
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
|
||||
api.APISessionRequired(requireLicense(unlinkGroupSyncable))).Methods("DELETE")
|
||||
api.APISessionRequired(unlinkGroupSyncable)).Methods("DELETE")
|
||||
|
||||
// GET /api/v4/groups/:group_id/teams/:team_id
|
||||
// GET /api/v4/groups/:group_id/channels/:channel_id
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}",
|
||||
api.APISessionRequired(requireLicense(getGroupSyncable))).Methods("GET")
|
||||
api.APISessionRequired(getGroupSyncable)).Methods("GET")
|
||||
|
||||
// GET /api/v4/groups/:group_id/teams
|
||||
// GET /api/v4/groups/:group_id/channels
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}",
|
||||
api.APISessionRequired(requireLicense(getGroupSyncables))).Methods("GET")
|
||||
api.APISessionRequired(getGroupSyncables)).Methods("GET")
|
||||
|
||||
// PUT /api/v4/groups/:group_id/teams/:team_id/patch
|
||||
// PUT /api/v4/groups/:group_id/channels/:channel_id/patch
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/patch",
|
||||
api.APISessionRequired(requireLicense(patchGroupSyncable))).Methods("PUT")
|
||||
api.APISessionRequired(patchGroupSyncable)).Methods("PUT")
|
||||
|
||||
// GET /api/v4/groups/:group_id/stats
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats",
|
||||
api.APISessionRequired(requireLicense(getGroupStats))).Methods("GET")
|
||||
api.APISessionRequired(getGroupStats)).Methods("GET")
|
||||
|
||||
// GET /api/v4/groups/:group_id/members
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
|
||||
api.APISessionRequired(requireLicense(getGroupMembers))).Methods("GET")
|
||||
api.APISessionRequired(getGroupMembers)).Methods("GET")
|
||||
|
||||
// GET /api/v4/users/:user_id/groups
|
||||
api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups",
|
||||
api.APISessionRequired(requireLicense(getGroupsByUserId))).Methods("GET")
|
||||
api.APISessionRequired(getGroupsByUserId)).Methods("GET")
|
||||
|
||||
// GET /api/v4/channels/:channel_id/groups
|
||||
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups",
|
||||
api.APISessionRequired(requireLicense(getGroupsByChannel))).Methods("GET")
|
||||
api.APISessionRequired(getGroupsByChannel)).Methods("GET")
|
||||
|
||||
// GET /api/v4/teams/:team_id/groups
|
||||
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups",
|
||||
api.APISessionRequired(requireLicense(getGroupsByTeam))).Methods("GET")
|
||||
api.APISessionRequired(getGroupsByTeam)).Methods("GET")
|
||||
|
||||
// GET /api/v4/teams/:team_id/groups_by_channels
|
||||
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels",
|
||||
api.APISessionRequired(requireLicense(getGroupsAssociatedToChannelsByTeam))).Methods("GET")
|
||||
api.APISessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods("GET")
|
||||
|
||||
// DELETE /api/v4/groups/:group_id
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
|
||||
api.APISessionRequired(requireLicense(deleteGroup))).Methods("DELETE")
|
||||
api.APISessionRequired(deleteGroup)).Methods("DELETE")
|
||||
|
||||
// POST /api/v4/groups/:group_id/members
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
|
||||
api.APISessionRequired(requireLicense(addGroupMembers))).Methods("POST")
|
||||
api.APISessionRequired(addGroupMembers)).Methods("POST")
|
||||
|
||||
// DELETE /api/v4/groups/:group_id/members
|
||||
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
|
||||
api.APISessionRequired(requireLicense(deleteGroupMembers))).Methods("DELETE")
|
||||
api.APISessionRequired(deleteGroupMembers)).Methods("DELETE")
|
||||
}
|
||||
|
||||
func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -130,6 +136,11 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func createGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
var group *model.GroupWithUserIds
|
||||
if err := json.NewDecoder(r.Body).Decode(&group); err != nil {
|
||||
c.SetInvalidParamWithErr("group", err)
|
||||
@@ -185,6 +196,11 @@ func createGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -277,6 +293,11 @@ func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -368,6 +389,11 @@ func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -411,6 +437,11 @@ func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -448,6 +479,11 @@ func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -529,6 +565,11 @@ func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -606,6 +647,11 @@ func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType
|
||||
}
|
||||
|
||||
func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -651,6 +697,11 @@ func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -686,6 +737,11 @@ func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -717,72 +773,46 @@ func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getGroupsByChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
b, appErr := getGroupsByChannelCommon(c, r)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
var permission *model.Permission
|
||||
if channel.Type == model.ChannelTypePrivate {
|
||||
permission = model.PermissionReadPrivateChannelGroups
|
||||
} else {
|
||||
permission = model.PermissionReadPublicChannelGroups
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) {
|
||||
c.SetPermissionError(permission)
|
||||
return
|
||||
}
|
||||
|
||||
opts := model.GroupSearchOpts{
|
||||
Q: c.Params.Q,
|
||||
IncludeMemberCount: c.Params.IncludeMemberCount,
|
||||
FilterAllowReference: c.Params.FilterAllowReference,
|
||||
}
|
||||
if c.Params.Paginate == nil || *c.Params.Paginate {
|
||||
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
|
||||
}
|
||||
|
||||
groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(struct {
|
||||
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
|
||||
Count int `json:"total_group_count"`
|
||||
}{
|
||||
Groups: groups,
|
||||
Count: totalCount,
|
||||
})
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
c.Err = model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
|
||||
|
||||
b, appError := getGroupsByTeamCommon(c, r)
|
||||
if appError != nil {
|
||||
c.Err = appError
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func getGroupsByTeamCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
opts := model.GroupSearchOpts{
|
||||
Q: c.Params.Q,
|
||||
@@ -795,8 +825,7 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
groups, totalCount, appErr := c.App.GetGroupsByTeam(c.Params.TeamId, opts)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
b, err := json.Marshal(struct {
|
||||
@@ -808,14 +837,64 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
return b, nil
|
||||
}
|
||||
func getGroupsByChannelCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
|
||||
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
|
||||
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
var permission *model.Permission
|
||||
if channel.Type == model.ChannelTypePrivate {
|
||||
permission = model.PermissionReadPrivateChannelGroups
|
||||
} else {
|
||||
permission = model.PermissionReadPublicChannelGroups
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) {
|
||||
return nil, c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
|
||||
}
|
||||
|
||||
opts := model.GroupSearchOpts{
|
||||
Q: c.Params.Q,
|
||||
IncludeMemberCount: c.Params.IncludeMemberCount,
|
||||
FilterAllowReference: c.Params.FilterAllowReference,
|
||||
}
|
||||
if c.Params.Paginate == nil || *c.Params.Paginate {
|
||||
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
|
||||
}
|
||||
|
||||
groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
b, err := json.Marshal(struct {
|
||||
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
|
||||
Count int `json:"total_group_count"`
|
||||
}{
|
||||
Groups: groups,
|
||||
Count: totalCount,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -855,6 +934,11 @@ func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
var teamID, channelID string
|
||||
|
||||
source := c.Params.GroupSource
|
||||
@@ -961,6 +1045,11 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -1004,6 +1093,11 @@ func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -1058,6 +1152,11 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
permissionErr := requireLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
c.RequireGroupId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
|
||||
@@ -3,7 +3,39 @@
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) InitGroupLocal() {
|
||||
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannel)).Methods("GET")
|
||||
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeam)).Methods("GET")
|
||||
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannelLocal)).Methods("GET")
|
||||
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeamLocal)).Methods("GET")
|
||||
}
|
||||
|
||||
func getGroupsByChannelLocal(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireChannelId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
b, appErr := getGroupsByChannelCommon(c, r)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func getGroupsByTeamLocal(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
b, appError := getGroupsByTeamCommon(c, r)
|
||||
if appError != nil {
|
||||
c.Err = appError
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
@@ -200,33 +200,27 @@ func (api *API) APILocal(h handlerFunc) http.Handler {
|
||||
return handler
|
||||
}
|
||||
|
||||
func requireLicense(f handlerFunc) handlerFunc {
|
||||
return func(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.App.Channels().License() == nil {
|
||||
c.Err = model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
f(c, w, r)
|
||||
func requireLicense(c *Context) *model.AppError {
|
||||
if c.App.Channels().License() == nil {
|
||||
err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func minimumProfessionalLicense(f handlerFunc) handlerFunc {
|
||||
return func(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
lic := c.App.Srv().License()
|
||||
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
|
||||
c.Err = model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
f(c, w, r)
|
||||
func minimumProfessionalLicense(c *Context) *model.AppError {
|
||||
lic := c.App.Srv().License()
|
||||
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
|
||||
err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rejectGuests(f handlerFunc) handlerFunc {
|
||||
return func(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
|
||||
c.Err = model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
f(c, w, r)
|
||||
func rejectGuests(c *Context) *model.AppError {
|
||||
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
|
||||
err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
201
api4/insights.go
201
api4/insights.go
@@ -13,31 +13,44 @@ import (
|
||||
|
||||
func (api *API) InitInsights() {
|
||||
// Reactions
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForTeamSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForUserSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET")
|
||||
|
||||
// Channels
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForTeamSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForUserSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET")
|
||||
|
||||
// Threads
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForTeamSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForUserSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(getTopThreadsForTeamSince)).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(getTopThreadsForUserSince)).Methods("GET")
|
||||
|
||||
// user DMs
|
||||
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopDMsForUserSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(getTopDMsForUserSince)).Methods("GET")
|
||||
|
||||
// Inactive channels
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForTeamSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForUserSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForTeamSince)).Methods("GET")
|
||||
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForUserSince)).Methods("GET")
|
||||
|
||||
// New teammembers
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getNewTeamMembersSince)))).Methods("GET")
|
||||
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(getNewTeamMembersSince)).Methods("GET")
|
||||
}
|
||||
|
||||
// Top Reactions
|
||||
|
||||
func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -60,7 +73,11 @@ func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topReactionList, appErr := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -82,6 +99,18 @@ func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.Params.TeamId = r.URL.Query().Get("team_id")
|
||||
|
||||
// TeamId is an optional parameter
|
||||
@@ -109,7 +138,11 @@ func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topReactionList, appErr := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -133,6 +166,18 @@ func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Requ
|
||||
// Top Channels
|
||||
|
||||
func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -156,7 +201,11 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
loc := user.GetTimezoneLocation()
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topChannels, appErr := c.App.GetTopChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -184,6 +233,18 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.Params.TeamId = r.URL.Query().Get("team_id")
|
||||
|
||||
// TeamId is an optional parameter
|
||||
@@ -212,7 +273,11 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
loc := user.GetTimezoneLocation()
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topChannels, appErr := c.App.GetTopChannelsForUserSince(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -241,6 +306,18 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// Top Threads
|
||||
func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -264,7 +341,11 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topThreads, appErr := c.App.GetTopThreadsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -286,6 +367,18 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.Params.TeamId = r.URL.Query().Get("team_id")
|
||||
|
||||
// restrict users with no access to team
|
||||
@@ -313,7 +406,11 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topThreads, appErr := c.App.GetTopThreadsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -336,13 +433,29 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// Top DMs
|
||||
func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -367,6 +480,18 @@ func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// Top Channels
|
||||
|
||||
func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -390,7 +515,11 @@ func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
loc := user.GetTimezoneLocation()
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -411,6 +540,18 @@ func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *ht
|
||||
// top inactive channels
|
||||
|
||||
func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.Params.TeamId = r.URL.Query().Get("team_id")
|
||||
|
||||
// TeamId is an optional parameter
|
||||
@@ -439,7 +580,11 @@ func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
loc := user.GetTimezoneLocation()
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
@@ -479,6 +624,18 @@ func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelLi
|
||||
}
|
||||
|
||||
func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// license and guest user check
|
||||
permissionErr := minimumProfessionalLicense(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
permissionErr = rejectGuests(c)
|
||||
if permissionErr != nil {
|
||||
c.Err = permissionErr
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -501,7 +658,11 @@ func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
loc := user.GetTimezoneLocation()
|
||||
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
ntms, count, err := c.App.GetNewTeamMembersSince(c.AppContext, c.Params.TeamId, &model.InsightsOpts{
|
||||
StartUnixMilli: startTime.UnixMilli(),
|
||||
|
||||
@@ -230,6 +230,12 @@ func TestGetTopReactionsForTeamSince(t *testing.T) {
|
||||
CheckNotFoundStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-reactions-for-team-since invalid time range", func(t *testing.T) {
|
||||
_, resp, err := client.GetTopReactionsForTeamSince(teamId, "7_days", 0, 5)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-reactions-for-team-since not a member of team", func(t *testing.T) {
|
||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||
_, resp, err := client.GetTopReactionsForTeamSince(teamId, model.TimeRangeToday, 0, 5)
|
||||
@@ -417,6 +423,12 @@ func TestGetTopReactionsForUserSince(t *testing.T) {
|
||||
CheckNotFoundStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-reactions-for-user-since invalid time range", func(t *testing.T) {
|
||||
_, resp, err := client.GetTopReactionsForUserSince(teamId, "7_days", 0, 5)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-reactions-for-user-since not a member of team", func(t *testing.T) {
|
||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||
_, resp, err := client.GetTopReactionsForUserSince(teamId, model.TimeRangeToday, 0, 5)
|
||||
@@ -515,6 +527,12 @@ func TestGetTopChannelsForTeamSince(t *testing.T) {
|
||||
CheckNotFoundStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-channels-for-team-since invalid time range", func(t *testing.T) {
|
||||
_, resp, err := client.GetTopChannelsForTeamSince(teamId, "7_days", 0, 5)
|
||||
assert.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-channels-for-team-since not a member of team", func(t *testing.T) {
|
||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||
_, resp, err := client.GetTopChannelsForTeamSince(teamId, model.TimeRangeToday, 0, 5)
|
||||
@@ -592,6 +610,12 @@ func TestGetTopChannelsForUserSince(t *testing.T) {
|
||||
CheckNotFoundStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-channels-for-user-since invalid time range", func(t *testing.T) {
|
||||
_, resp, err := client.GetTopChannelsForUserSince(teamId, "7_days", 0, 5)
|
||||
assert.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("get-top-channels-for-user-since not a member of team", func(t *testing.T) {
|
||||
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||
_, resp, err := client.GetTopChannelsForUserSince(teamId, model.TimeRangeToday, 0, 5)
|
||||
|
||||
@@ -106,7 +106,13 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// skip the restrictions if license is a sanctioned trial
|
||||
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
|
||||
canStartTrialLicense, err := c.App.Srv().Platform().LicenseManager().CanStartTrial()
|
||||
lm := c.App.Srv().Platform().LicenseManager()
|
||||
if lm == nil {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
canStartTrialLicense, err := lm.CanStartTrial()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, "", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -91,9 +91,6 @@ func TestUploadLicenseFile(t *testing.T) {
|
||||
mockLicenseValidator := mocks2.LicenseValidatorIface{}
|
||||
defer testutils.ResetLicenseValidator()
|
||||
|
||||
//startTimestamp, err := time.Parse("2 Jan 2006 3:04 pm", "1 Jan 2021 12:00 am")
|
||||
//require.Nil(t, err)
|
||||
|
||||
userCount := 100
|
||||
mills := model.GetMillis()
|
||||
|
||||
@@ -125,6 +122,37 @@ func TestUploadLicenseFile(t *testing.T) {
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("try to get gone through trial, with TE build", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = false })
|
||||
th.App.Srv().Platform().SetLicenseManager(nil)
|
||||
|
||||
mockLicenseValidator := mocks2.LicenseValidatorIface{}
|
||||
defer testutils.ResetLicenseValidator()
|
||||
|
||||
license := model.License{
|
||||
Id: model.NewId(),
|
||||
Features: &model.Features{
|
||||
Users: model.NewInt(100),
|
||||
},
|
||||
Customer: &model.Customer{
|
||||
Name: "Test",
|
||||
},
|
||||
StartsAt: model.GetMillis() + 100,
|
||||
ExpiresAt: model.GetMillis() + 100 + (30*(time.Hour*24) + (time.Hour * 8)).Milliseconds(),
|
||||
}
|
||||
|
||||
mockLicenseValidator.On("LicenseFromBytes", mock.Anything).Return(&license, nil).Once()
|
||||
licenseBytes, err := json.Marshal(license)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseBytes))
|
||||
utils.LicenseValidator = &mockLicenseValidator
|
||||
|
||||
resp, err := th.SystemAdminClient.UploadLicenseFile([]byte(""))
|
||||
CheckErrorID(t, err, "api.license.upgrade_needed.app_error")
|
||||
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("allow uploading sanctioned trials even if server already gone through trial", func(t *testing.T) {
|
||||
mockLicenseValidator := mocks2.LicenseValidatorIface{}
|
||||
defer testutils.ResetLicenseValidator()
|
||||
|
||||
@@ -2498,7 +2498,7 @@ func TestGetTopChannelsForTeamSince(t *testing.T) {
|
||||
{ID: channel5.Id, MessageCount: 2},
|
||||
}
|
||||
|
||||
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
|
||||
t.Run("get-top-channels-for-team-since", func(t *testing.T) {
|
||||
topChannels, err := th.App.GetTopChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
|
||||
@@ -2576,7 +2576,7 @@ func TestGetTopChannelsForUserSince(t *testing.T) {
|
||||
{ID: channel5.Id, MessageCount: 2},
|
||||
}
|
||||
|
||||
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
|
||||
t.Run("get-top-channels-for-user-since", func(t *testing.T) {
|
||||
topChannels, err := th.App.GetTopChannelsForUserSince(th.Context, th.BasicUser.Id, "", &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
|
||||
@@ -2787,7 +2787,7 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) {
|
||||
{ID: channel2.Id, MessageCount: 6},
|
||||
}
|
||||
|
||||
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
|
||||
t.Run("get-top-channels-for-team-since", func(t *testing.T) {
|
||||
topChannels, err := th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
|
||||
@@ -2885,7 +2885,7 @@ func TestGetTopInactiveChannelsForUserSince(t *testing.T) {
|
||||
{ID: channel2.Id, MessageCount: 6},
|
||||
}
|
||||
|
||||
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
|
||||
t.Run("get-top-channels-for-user-since", func(t *testing.T) {
|
||||
topChannels, err := th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 4})
|
||||
|
||||
@@ -201,6 +201,18 @@ func truncateUserNames(name string, i int) string {
|
||||
return name
|
||||
}
|
||||
|
||||
type FieldRow struct {
|
||||
Cells []*model.SlackAttachmentField
|
||||
}
|
||||
|
||||
type EmailMessageAttachment struct {
|
||||
model.SlackAttachment
|
||||
|
||||
Pretext template.HTML
|
||||
Text template.HTML
|
||||
FieldRows []FieldRow
|
||||
}
|
||||
|
||||
type postData struct {
|
||||
SenderName string
|
||||
ChannelName string
|
||||
@@ -211,6 +223,7 @@ type postData struct {
|
||||
Time string
|
||||
ShowChannelIcon bool
|
||||
OtherChannelMembersCount int
|
||||
MessageAttachments []*EmailMessageAttachment
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,6 +258,7 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos
|
||||
}
|
||||
pData.Message = template.HTML(normalizedPostMessage)
|
||||
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
|
||||
pData.MessageAttachments = a.processMessageAttachments(post)
|
||||
}
|
||||
|
||||
data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale)
|
||||
@@ -306,6 +320,81 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos
|
||||
return a.Srv().TemplatesContainer().RenderToString("messages_notification", data)
|
||||
}
|
||||
|
||||
func (a *App) processMessageAttachments(post *model.Post) []*EmailMessageAttachment {
|
||||
emailMessageAttachments := []*EmailMessageAttachment{}
|
||||
|
||||
for _, messageAttachment := range post.Attachments() {
|
||||
emailMessageAttachment := &EmailMessageAttachment{
|
||||
SlackAttachment: *messageAttachment,
|
||||
Pretext: a.prepareTextForEmail(messageAttachment.Pretext),
|
||||
Text: a.prepareTextForEmail(messageAttachment.Text),
|
||||
}
|
||||
|
||||
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
|
||||
if err != nil {
|
||||
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
stripedTitle = ""
|
||||
}
|
||||
|
||||
emailMessageAttachment.Title = stripedTitle
|
||||
|
||||
shortFieldRow := FieldRow{}
|
||||
|
||||
for i := range messageAttachment.Fields {
|
||||
// Create a new instance to avoid altering the original pointer reference
|
||||
// We update field value to parse markdown.
|
||||
// If we do that on the original pointer, the rendered text in mattermost
|
||||
// becomes invalid as its no longer a markdown string, but rather an HTML string.
|
||||
field := &model.SlackAttachmentField{
|
||||
Title: messageAttachment.Fields[i].Title,
|
||||
Value: messageAttachment.Fields[i].Value,
|
||||
Short: messageAttachment.Fields[i].Short,
|
||||
}
|
||||
|
||||
if stringValue, ok := field.Value.(string); ok {
|
||||
field.Value = a.prepareTextForEmail(stringValue)
|
||||
}
|
||||
|
||||
if !field.Short {
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
|
||||
} else {
|
||||
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
|
||||
|
||||
if len(shortFieldRow.Cells) == 2 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect any leftover short fields
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
|
||||
}
|
||||
|
||||
return emailMessageAttachments
|
||||
}
|
||||
|
||||
func (a *App) prepareTextForEmail(text string) template.HTML {
|
||||
escapedText := html.EscapeString(text)
|
||||
markdownText, err := utils.MarkdownToHTML(escapedText)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
|
||||
return template.HTML(text)
|
||||
}
|
||||
|
||||
return template.HTML(markdownText)
|
||||
}
|
||||
|
||||
type formattedPostTime struct {
|
||||
Time time.Time
|
||||
Year string
|
||||
|
||||
@@ -339,6 +339,103 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T)
|
||||
require.Contains(t, body, "14:30", fmt.Sprintf("Expected email text '14:30'. Got %s", body))
|
||||
}
|
||||
|
||||
func TestGetNotificationEmailBodyFullNotificationWithSlackAttachments(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
recipient := &model.User{}
|
||||
post := &model.Post{
|
||||
Message: "This is the message",
|
||||
}
|
||||
|
||||
messageAttachments := []*model.SlackAttachment{
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 1 pretext",
|
||||
AuthorName: "author name",
|
||||
AuthorLink: "https://example.com/slack_attachment_1/author_link",
|
||||
AuthorIcon: "https://example.com/slack_attachment_1/author_icon",
|
||||
Title: "message attachment 1 title",
|
||||
TitleLink: "https://example.com/slack_attachment_1/title_link",
|
||||
Text: "message attachment 1 text",
|
||||
ImageURL: "https://example.com/slack_attachment_1/image",
|
||||
ThumbURL: "https://example.com/slack_attachment_1/thumb",
|
||||
Fields: []*model.SlackAttachmentField{
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 1 title",
|
||||
Value: "message attachment 1 field 1 value",
|
||||
},
|
||||
{
|
||||
Short: false,
|
||||
Title: "message attachment 1 field 2 title",
|
||||
Value: "message attachment 1 field 2 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 3 title",
|
||||
Value: "message attachment 1 field 3 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 4 title",
|
||||
Value: "message attachment 1 field 4 value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 2 pretext",
|
||||
AuthorName: "author name 2",
|
||||
Text: "message attachment 2 text",
|
||||
},
|
||||
}
|
||||
|
||||
model.ParseSlackAttachment(post, messageAttachments)
|
||||
|
||||
channel := &model.Channel{
|
||||
DisplayName: "ChannelName",
|
||||
Type: model.ChannelTypeOpen,
|
||||
}
|
||||
|
||||
channelName := "ChannelName"
|
||||
senderName := "sender"
|
||||
teamName := "testteam"
|
||||
teamURL := "http://localhost:8065/testteam"
|
||||
emailNotificationContentsType := model.EmailNotificationContentsFull
|
||||
translateFunc := i18n.GetUserTranslations("en")
|
||||
|
||||
storeMock := th.App.Srv().Store().(*mocks.Store)
|
||||
teamStoreMock := mocks.TeamStore{}
|
||||
teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil)
|
||||
storeMock.On("Team").Return(&teamStoreMock)
|
||||
|
||||
body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, body, "#FF0000")
|
||||
require.Contains(t, body, "message attachment 1 pretext")
|
||||
require.Contains(t, body, "author name")
|
||||
require.Contains(t, body, "https://example.com/slack_attachment_1/author_link")
|
||||
require.Contains(t, body, "https://example.com/slack_attachment_1/author_icon")
|
||||
require.Contains(t, body, "message attachment 1 title")
|
||||
require.Contains(t, body, "https://example.com/slack_attachment_1/title_link")
|
||||
require.Contains(t, body, "message attachment 1 text")
|
||||
require.Contains(t, body, "https://example.com/slack_attachment_1/image")
|
||||
require.Contains(t, body, "https://example.com/slack_attachment_1/thumb")
|
||||
require.Contains(t, body, "message attachment 1 field 1 title")
|
||||
require.Contains(t, body, "message attachment 1 field 1 value")
|
||||
require.Contains(t, body, "message attachment 1 field 2 title")
|
||||
require.Contains(t, body, "message attachment 1 field 2 value")
|
||||
require.Contains(t, body, "message attachment 1 field 3 title")
|
||||
require.Contains(t, body, "message attachment 1 field 3 value")
|
||||
require.Contains(t, body, "message attachment 1 field 4 title")
|
||||
require.Contains(t, body, "message attachment 1 field 4 value")
|
||||
require.Contains(t, body, "https://example.com/slack_attachment_1/thumb")
|
||||
require.Contains(t, body, "message attachment 2 pretext")
|
||||
require.Contains(t, body, "author name 2")
|
||||
require.Contains(t, body, "message attachment 2 text")
|
||||
}
|
||||
|
||||
// from here
|
||||
func TestGetNotificationEmailBodyGenericNotificationPublicChannel(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
@@ -1440,20 +1440,19 @@ func TestPushNotificationRace(t *testing.T) {
|
||||
Return(&model.Preference{Value: "test"}, nil)
|
||||
mockStore.On("Preference").Return(&mockPreferenceStore)
|
||||
s := &Server{
|
||||
products: make(map[string]Product),
|
||||
Router: mux.NewRouter(),
|
||||
filestore: &fmocks.FileBackend{},
|
||||
products: make(map[string]Product),
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
var err error
|
||||
s.platform, err = platform.New(platform.ServiceConfig{
|
||||
ConfigStore: memoryStore,
|
||||
})
|
||||
}, platform.SetFileStore(&fmocks.FileBackend{}))
|
||||
s.SetStore(mockStore)
|
||||
require.NoError(t, err)
|
||||
serviceMap := map[ServiceKey]any{
|
||||
ConfigKey: s.platform,
|
||||
LicenseKey: &licenseWrapper{s},
|
||||
FilestoreKey: s.filestore,
|
||||
FilestoreKey: s.FileBackend(),
|
||||
}
|
||||
ch, err := NewChannels(s, serviceMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -54,7 +54,7 @@ func ConfigStore(configStore *config.Store) Option {
|
||||
|
||||
func SetFileStore(filestore filestore.FileBackend) Option {
|
||||
return func(s *Server) error {
|
||||
s.filestore = filestore
|
||||
s.platformOptions = append(s.platformOptions, platform.SetFileStore(filestore))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ type permissionsServiceWrapper struct {
|
||||
app AppIface
|
||||
}
|
||||
|
||||
func (s *permissionsServiceWrapper) HasPermissionTo(userID string, permission *model.Permission) bool {
|
||||
return s.app.HasPermissionTo(userID, permission)
|
||||
}
|
||||
|
||||
func (s *permissionsServiceWrapper) HasPermissionToTeam(userID string, teamID string, permission *model.Permission) bool {
|
||||
return s.app.HasPermissionToTeam(userID, teamID, permission)
|
||||
}
|
||||
|
||||
@@ -991,6 +991,27 @@ func (a *App) getPlaybooksPermissionsAddManageRoles() (permissionsMap, error) {
|
||||
return transformations, nil
|
||||
}
|
||||
|
||||
func (a *App) getProductsBoardsPermissions() (permissionsMap, error) {
|
||||
transformations := []permissionTransformation{}
|
||||
|
||||
permissionsProductsRead := []string{model.PermissionSysconsoleReadProductsBoards.Id}
|
||||
permissionsProductsWrite := []string{model.PermissionSysconsoleWriteProductsBoards.Id}
|
||||
|
||||
// Give the new subsection READ permissions to any user with SYSTEM_MANAGER
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: permissionOr(isRole(model.SystemManagerRoleId)),
|
||||
Add: permissionsProductsRead,
|
||||
})
|
||||
|
||||
// Give the new subsection WRITE permissions to any user with SYSTEM_ADMIN
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: permissionOr(isRole(model.SystemAdminRoleId)),
|
||||
Add: permissionsProductsWrite,
|
||||
})
|
||||
|
||||
return transformations, nil
|
||||
}
|
||||
|
||||
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
|
||||
func (a *App) DoPermissionsMigrations() error {
|
||||
return a.Srv().doPermissionsMigrations()
|
||||
@@ -1032,6 +1053,7 @@ func (s *Server) doPermissionsMigrations() error {
|
||||
{Key: model.MigrationKeyAddPlaybooksPermissions, Migration: a.getAddPlaybooksPermissions},
|
||||
{Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions},
|
||||
{Key: model.MigrationKeyAddPlayboosksManageRolesPermissions, Migration: a.getPlaybooksPermissionsAddManageRoles},
|
||||
{Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions},
|
||||
}
|
||||
|
||||
roles, err := s.Store().Role().GetAll()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -127,7 +127,7 @@ func SetupWithCluster(tb testing.TB, cluster einterfaces.ClusterInterface) *Test
|
||||
}
|
||||
|
||||
func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, options ...Option) *TestHelper {
|
||||
tempWorkspace, err := ioutil.TempDir("", "apptest")
|
||||
tempWorkspace, err := os.MkdirTemp("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/config"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
"github.com/mattermost/mattermost-server/v6/store/localcachelayer"
|
||||
@@ -74,6 +75,13 @@ func Config(dsn string, readOnly bool, configDefaults *model.Config) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func SetFileStore(filestore filestore.FileBackend) Option {
|
||||
return func(ps *PlatformService) error {
|
||||
ps.filestore = filestore
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing.
|
||||
func ConfigStore(configStore *config.Store) Option {
|
||||
return func(ps *PlatformService) error {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/services/cache"
|
||||
"github.com/mattermost/mattermost-server/v6/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v6/services/searchengine/bleveengine"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
"github.com/mattermost/mattermost-server/v6/store/localcachelayer"
|
||||
@@ -41,6 +42,8 @@ type PlatformService struct {
|
||||
|
||||
configStore *config.Store
|
||||
|
||||
filestore filestore.FileBackend
|
||||
|
||||
cacheProvider cache.Provider
|
||||
statusCache cache.Cache
|
||||
sessionCache cache.Cache
|
||||
@@ -213,6 +216,18 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
|
||||
}
|
||||
}
|
||||
|
||||
license := ps.License()
|
||||
// Step 3: Initialize filestore
|
||||
if ps.filestore == nil {
|
||||
insecure := ps.Config().ServiceSettings.EnableInsecureOutgoingConnections
|
||||
backend, err2 := filestore.NewFileBackend(ps.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
|
||||
if err2 != nil {
|
||||
return nil, fmt.Errorf("failed to initialize filebackend: %w", err2)
|
||||
}
|
||||
|
||||
ps.filestore = backend
|
||||
}
|
||||
|
||||
var err error
|
||||
ps.Store, err = ps.newStore()
|
||||
if err != nil {
|
||||
@@ -431,3 +446,7 @@ func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.App
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) FileBackend() filestore.FileBackend {
|
||||
return ps.filestore
|
||||
}
|
||||
|
||||
@@ -196,6 +196,7 @@ func TestPluginAPIGetUserPreferences(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPluginAPIDeleteUserPreferences(t *testing.T) {
|
||||
t.Skip("MM-47612")
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
api := th.SetupPluginAPI()
|
||||
|
||||
@@ -230,7 +230,7 @@ func TestGetTopReactionsForTeamSince(t *testing.T) {
|
||||
expectedTopReactions[3] = &model.TopReaction{EmojiName: "sad", Count: int64(3)}
|
||||
expectedTopReactions[4] = &model.TopReaction{EmojiName: "happy", Count: int64(2)}
|
||||
|
||||
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
|
||||
t.Run("get-top-reactions-for-team-since", func(t *testing.T) {
|
||||
topReactions, err := th.App.GetTopReactionsForTeamSince(teamId, userId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
|
||||
@@ -401,7 +401,7 @@ func TestGetTopReactionsForUserSince(t *testing.T) {
|
||||
expectedTopReactions[3] = &model.TopReaction{EmojiName: "heart", Count: int64(3)}
|
||||
expectedTopReactions[4] = &model.TopReaction{EmojiName: "blush", Count: int64(2)}
|
||||
|
||||
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
|
||||
|
||||
t.Run("get-top-reactions-for-user-since", func(t *testing.T) {
|
||||
topReactions, err := th.App.GetTopReactionsForUserSince(userId, teamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
|
||||
|
||||
@@ -137,7 +137,6 @@ type Server struct {
|
||||
openGraphDataCache cache.Cache
|
||||
clusterLeaderListenerId string
|
||||
loggerLicenseListenerId string
|
||||
filestore filestore.FileBackend
|
||||
|
||||
platform *platform.PlatformService
|
||||
platformOptions []platform.Option
|
||||
@@ -239,15 +238,6 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
s.LoadLicense()
|
||||
}
|
||||
|
||||
license := s.License()
|
||||
insecure := s.platform.Config().ServiceSettings.EnableInsecureOutgoingConnections
|
||||
// Step 3: Initialize filestore
|
||||
backend, err := filestore.NewFileBackend(s.platform.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to initialize filebackend")
|
||||
}
|
||||
s.filestore = backend
|
||||
|
||||
s.licenseWrapper = &licenseWrapper{
|
||||
srv: s,
|
||||
}
|
||||
@@ -272,7 +262,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
ChannelKey: &channelsWrapper{srv: s},
|
||||
ConfigKey: s.platform,
|
||||
LicenseKey: s.licenseWrapper,
|
||||
FilestoreKey: s.filestore,
|
||||
FilestoreKey: s.platform.FileBackend(),
|
||||
FileInfoStoreKey: &fileInfoWrapper{srv: s},
|
||||
ClusterKey: s.platform,
|
||||
UserKey: New(ServerConnector(s.Channels())),
|
||||
@@ -439,6 +429,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
mlog.Info("Printing current working", mlog.String("directory", pwd))
|
||||
mlog.Info("Loaded config", mlog.String("source", s.platform.DescribeConfig()))
|
||||
|
||||
license := s.License()
|
||||
allowAdvancedLogging := license != nil && *license.Features.AdvancedLogging
|
||||
|
||||
if s.Audit == nil {
|
||||
@@ -1425,7 +1416,7 @@ func (s *Server) SendRemoveExpiredLicenseEmail(email string, renewalLink, locale
|
||||
}
|
||||
|
||||
func (s *Server) FileBackend() filestore.FileBackend {
|
||||
return s.filestore
|
||||
return s.platform.FileBackend()
|
||||
}
|
||||
|
||||
func (s *Server) TotalWebsocketConnections() int {
|
||||
|
||||
@@ -90,28 +90,29 @@ func TestStartServerNoS3Bucket(t *testing.T) {
|
||||
}
|
||||
|
||||
s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port)
|
||||
configStore, _ := config.NewFileStore("config.json", true)
|
||||
store, _ := config.NewStoreFromBacking(configStore, nil, false)
|
||||
|
||||
cfg := store.Get()
|
||||
cfg.FileSettings = model.FileSettings{
|
||||
DriverName: model.NewString(model.ImageDriverS3),
|
||||
AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey),
|
||||
AmazonS3SecretAccessKey: model.NewString(model.MinioSecretKey),
|
||||
AmazonS3Bucket: model.NewString("nosuchbucket"),
|
||||
AmazonS3Endpoint: model.NewString(s3Endpoint),
|
||||
AmazonS3Region: model.NewString(""),
|
||||
AmazonS3PathPrefix: model.NewString(""),
|
||||
AmazonS3SSL: model.NewBool(false),
|
||||
}
|
||||
*cfg.ServiceSettings.ListenAddress = ":0"
|
||||
_, _, err := store.Set(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err := NewServer(func(server *Server) error {
|
||||
configStore, _ := config.NewFileStore("config.json", true)
|
||||
store, _ := config.NewStoreFromBacking(configStore, nil, false)
|
||||
var err error
|
||||
server.platform, err = platform.New(platform.ServiceConfig{
|
||||
ConfigStore: store,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
server.platform.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings = model.FileSettings{
|
||||
DriverName: model.NewString(model.ImageDriverS3),
|
||||
AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey),
|
||||
AmazonS3SecretAccessKey: model.NewString(model.MinioSecretKey),
|
||||
AmazonS3Bucket: model.NewString("nosuchbucket"),
|
||||
AmazonS3Endpoint: model.NewString(s3Endpoint),
|
||||
AmazonS3Region: model.NewString(""),
|
||||
AmazonS3PathPrefix: model.NewString(""),
|
||||
AmazonS3SSL: model.NewBool(false),
|
||||
}
|
||||
*cfg.ServiceSettings.ListenAddress = ":0"
|
||||
})
|
||||
var err2 error
|
||||
server.platform, err2 = platform.New(platform.ServiceConfig{}, platform.ConfigStore(store))
|
||||
require.NoError(t, err2)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -120,6 +121,8 @@ func TestStartServerNoS3Bucket(t *testing.T) {
|
||||
defer s.Shutdown()
|
||||
|
||||
// ensure that a new bucket was created
|
||||
require.IsType(t, &filestore.S3FileBackend{}, s.FileBackend())
|
||||
|
||||
err = s.FileBackend().(*filestore.S3FileBackend).TestConnection()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
ENV PATH="/mattermost/bin:${PATH}"
|
||||
ARG PUID=2000
|
||||
ARG PGID=2000
|
||||
ARG MM_PACKAGE="https://releases.mattermost.com/7.3.0/mattermost-7.3.0-linux-amd64.tar.gz?src=docker"
|
||||
ARG MM_PACKAGE="https://releases.mattermost.com/7.4.0/mattermost-7.4.0-linux-amd64.tar.gz?src=docker"
|
||||
|
||||
# # Install needed packages and indirect dependencies
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -73,6 +74,11 @@ func applyEnvKey(key, value string, rValueSubject reflect.Value) {
|
||||
}
|
||||
case reflect.SliceOf(reflect.TypeOf("")).Kind():
|
||||
rFieldValue.Set(reflect.ValueOf(strings.Split(value, " ")))
|
||||
case reflect.Map:
|
||||
target := reflect.New(rFieldValue.Type()).Interface()
|
||||
if err := json.Unmarshal([]byte(value), target); err == nil {
|
||||
rFieldValue.Set(reflect.ValueOf(target).Elem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,15 +31,54 @@ func TestRemoveEnvOverrides(t *testing.T) {
|
||||
expectedConfig *model.Config
|
||||
}{
|
||||
{
|
||||
name: "basic override",
|
||||
name: "config override",
|
||||
inputConfig: modifiedDefault(func(in *model.Config) {
|
||||
*in.ServiceSettings.TLSMinVer = "1.4"
|
||||
in.PluginSettings.PluginStates = map[string]*model.PluginState{
|
||||
"plugin1": {
|
||||
Enable: false,
|
||||
},
|
||||
}
|
||||
in.PluginSettings.Plugins = map[string]map[string]interface{}{
|
||||
"com.mattermost.plugin-1": {
|
||||
"key1": "value1",
|
||||
},
|
||||
"com_mattermost_plugin-2": {
|
||||
"key2": "value2",
|
||||
},
|
||||
}
|
||||
}),
|
||||
env: map[string]string{
|
||||
"MM_SERVICESETTINGS_TLSMINVER": "1.5",
|
||||
"MM_PLUGINSETTINGS_PLUGINSTATES": `{
|
||||
"plugin1": {
|
||||
"Enable": true
|
||||
}
|
||||
}`,
|
||||
"MM_PLUGINSETTINGS_PLUGINS": `{
|
||||
"com.mattermost.plugin-1": {
|
||||
"key1": "other-value"
|
||||
},
|
||||
"com_mattermost_plugin-2": {
|
||||
"key2": "other-value"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectedConfig: modifiedDefault(func(in *model.Config) {
|
||||
*in.ServiceSettings.TLSMinVer = "1.5"
|
||||
in.PluginSettings.PluginStates = map[string]*model.PluginState{
|
||||
"plugin1": {
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
in.PluginSettings.Plugins = map[string]map[string]interface{}{
|
||||
"com.mattermost.plugin-1": {
|
||||
"key1": "other-value",
|
||||
},
|
||||
"com_mattermost_plugin-2": {
|
||||
"key2": "other-value",
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -102,6 +141,41 @@ func TestRemoveEnvOverrides(t *testing.T) {
|
||||
in.SqlSettings.DataSourceReplicas = []string{"otherthing", "alsothis"}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "complex env settings",
|
||||
inputConfig: modifiedDefault(func(in *model.Config) {
|
||||
}),
|
||||
env: map[string]string{
|
||||
"MM_PLUGINSETTINGS_PLUGINSTATES": `{
|
||||
"com.mattermost.plugin-1": {
|
||||
"enable": true
|
||||
}
|
||||
}`,
|
||||
"MM_PLUGINSETTINGS_PLUGINS": `{
|
||||
"com.mattermost.plugin-1": {
|
||||
"key": {
|
||||
"key": "(?P<key>KEY)-(?P<id>\\d{1,6})(?P<comma>[,;]*)",
|
||||
"value": "[$key-$id](https://example.com/?$project-$id)$comma"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectedConfig: modifiedDefault(func(in *model.Config) {
|
||||
in.PluginSettings.PluginStates = map[string]*model.PluginState{
|
||||
"com.mattermost.plugin-1": {
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
in.PluginSettings.Plugins = map[string]map[string]interface{}{
|
||||
"com.mattermost.plugin-1": {
|
||||
"key": map[string]interface{}{
|
||||
"key": "(?P<key>KEY)-(?P<id>\\d{1,6})(?P<comma>[,;]*)",
|
||||
"value": "[$key-$id](https://example.com/?$project-$id)$comma",
|
||||
},
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "bad env",
|
||||
inputConfig: modifiedDefault(func(in *model.Config) {
|
||||
|
||||
@@ -4267,14 +4267,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Каналите в поканата трябва да са част от канещият екип."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Вашият лиценз не поддържа профили за гости"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Профилите за гости са забранени"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Грешка при анулиране на поканите по ел.поща."
|
||||
|
||||
44
i18n/de.json
44
i18n/de.json
@@ -1777,7 +1777,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_change_verify_body.title",
|
||||
"translation": "Du hast deine E-Mail-Adresse geändert"
|
||||
"translation": "Du hast deine E-Mail-Adresse erfolgreich geändert"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_change_verify_subject",
|
||||
@@ -4801,11 +4801,11 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.edge",
|
||||
"translation": "Version 44+"
|
||||
"translation": "Version 95+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.chrome",
|
||||
"translation": "Version 100+"
|
||||
"translation": "Version 106+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.learn_more",
|
||||
@@ -5623,14 +5623,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Die Kanäle der Einladung müssen Teil des Teams der Einladung sein."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Deine Lizenz unterstützt Gastkonten nicht"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Gastkonten sind deaktiviert"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Fehler beim Annulieren von E-Mail-Einladungen."
|
||||
@@ -8380,7 +8372,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.notification.body.dm.subTitle",
|
||||
"translation": "Währen du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet."
|
||||
"translation": "Während du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet."
|
||||
},
|
||||
{
|
||||
"id": "app.license.generate_renewal_token.no_license",
|
||||
@@ -9537,5 +9529,33 @@
|
||||
{
|
||||
"id": "app.user.get_badge_count.app_error",
|
||||
"translation": "Wir konnten den Nachrichtenzähler für den Benutzer nicht abfragen."
|
||||
},
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Fehler bei der Auftragsausführung."
|
||||
},
|
||||
{
|
||||
"id": "app.last_accessible_file.app_error",
|
||||
"translation": "Fehler beim Abrufen der letzten zugänglichen Datei"
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "Die Datei kann nicht abgerufen werden, da sie das Limit des Cloud-Plans überschritten hat."
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "Gruppenname existiert bereits als reservierter Name"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.product_mode.app_error",
|
||||
"translation": "Plugin {{.Name}} kann im Produktmodus nicht aktiviert werden."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.license.error",
|
||||
"translation": "Deine Lizenz unterstützt Gastkonten nicht"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.disabled.error",
|
||||
"translation": "Gastkonten sind deaktiviert"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8739,6 +8739,10 @@
|
||||
"id": "model.incoming_hook.username.app_error",
|
||||
"translation": "Invalid username."
|
||||
},
|
||||
{
|
||||
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
|
||||
"translation": "Invalid time range."
|
||||
},
|
||||
{
|
||||
"id": "model.job.is_valid.create_at.app_error",
|
||||
"translation": "Create at must be a valid time."
|
||||
|
||||
@@ -5459,14 +5459,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "The channels of the invite must be part of the team of the invite."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Your licence does not support guest accounts"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Guest accounts are disabled"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Error invalidating email invites."
|
||||
@@ -9537,5 +9529,21 @@
|
||||
{
|
||||
"id": "app.cloud.get_current_plan_name.app_error",
|
||||
"translation": "Unable to get current plan name"
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "group name already exists as a reserved name"
|
||||
},
|
||||
{
|
||||
"id": "app.last_accessible_file.app_error",
|
||||
"translation": "Error fetching last accessible file"
|
||||
},
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Error during job execution."
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "Unable to retrieve file: cloud storage limit exceeded."
|
||||
}
|
||||
]
|
||||
|
||||
346
i18n/es.json
346
i18n/es.json
@@ -621,7 +621,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.desc",
|
||||
"translation": "Abrir la página de ayuda de Mattermost"
|
||||
"translation": "Muestra el mensaje de ayuda de Mattermost"
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.name",
|
||||
@@ -1781,7 +1781,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_change_verify_body.title",
|
||||
"translation": "Haz actualizado tu correo electrónico"
|
||||
"translation": "Haz actualizado satisfactoriamente tu correo electrónico"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_change_verify_subject",
|
||||
@@ -1857,7 +1857,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.post_body.button",
|
||||
"translation": "Ver mensaje"
|
||||
"translation": "Responder en Mattermost"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.reset_body.button",
|
||||
@@ -3305,7 +3305,7 @@
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetoldap.duplicate_field",
|
||||
"translation": "No se pueden migrar los usuarios de AD/LDAP con el campo especificado. Se ha detectado un entrada. Por favor, retire todos los duplicados e inténtalo de nuevo."
|
||||
"translation": "No se pueden migrar los usuarios de AD/LDAP con el campo especificado. Se ha detectado un entrada duplicada. Por favor, elimina todos los duplicados e inténtalo de nuevo."
|
||||
},
|
||||
{
|
||||
"id": "ent.migration.migratetoldap.user_not_found",
|
||||
@@ -4573,7 +4573,7 @@
|
||||
},
|
||||
{
|
||||
"id": "oauth.gitlab.tos.error",
|
||||
"translation": "Los Términos de Servicio de GitLab se han actualizado. Por favor, vaya a gitlab.com a aceptar y, a continuación, intente iniciar sesión en Mattermost de nuevo."
|
||||
"translation": "Los Términos de Servicio de GitLab se han actualizado. Por favor, visita {{.URL}} para aceptarlos y, a continuación, intenta iniciar sesión en Mattermost nuevamente."
|
||||
},
|
||||
{
|
||||
"id": "plugin.api.update_user_status.bad_status",
|
||||
@@ -4753,7 +4753,7 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_os_version.windows",
|
||||
"translation": "Windows 7+"
|
||||
"translation": "Windows 8.1+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_os_version.mac",
|
||||
@@ -4761,19 +4761,19 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.safari",
|
||||
"translation": "Versión 12+"
|
||||
"translation": "Versión 14.1+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.firefox",
|
||||
"translation": "Versión 78+"
|
||||
"translation": "Versión 91+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.edge",
|
||||
"translation": "Versión 44+"
|
||||
"translation": "Versión 95+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.chrome",
|
||||
"translation": "Versión 89+"
|
||||
"translation": "Versión 106+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.learn_more",
|
||||
@@ -5543,14 +5543,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Los canales de la invitación deben ser parte del equipo al cual fue invitado."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "La licencia actual no es compatible con cuentas de huéspedes"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Las cuentas de huéspedes están deshabilitadas"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Error al invalidar las invitaciones por correo electrónico."
|
||||
@@ -5945,7 +5937,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.push_notifications.session.expired",
|
||||
"translation": "Sesión caducada: Inicie sesión para continuar recibiendo notificaciones. El administrador del sistema configura las sesiones para {{.siteName}} para que caduquen cada {{.daysCount}} día (s)."
|
||||
"translation": "Sesión vencida: Inicie sesión para continuar recibiendo notificaciones. El administrador del sistema configura las sesiones para {{.siteName}} para que caduquen cada {{.hoursCount}} hora(s)."
|
||||
},
|
||||
{
|
||||
"id": "api.post.error_get_post_id.pending",
|
||||
@@ -7621,7 +7613,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.user.get_authorization_code.endpoint.app_error",
|
||||
"translation": "Error all obtener el endpoint para Documento de Descubrimiento."
|
||||
"translation": "Error al obtener el endpoint desde Documento de Descubrimiento."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed_no_card.title",
|
||||
@@ -7645,15 +7637,15 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.title",
|
||||
"translation": "Pago fallido"
|
||||
"translation": "El pago ha fallado"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.subject",
|
||||
"translation": "Se requiere una acción: El pago de Mattermost Cloud no se ha realizado"
|
||||
"translation": "Se requiere una acción: El pago de {{.Plan}} Mattermost Cloud no se ha realizado"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.info3",
|
||||
"translation": "Para asegurar una suscripción ininterrumpida a Mattermost Cloud, por favor, póngase en contacto con su institución financiera para arreglar el problema subyacente o actualizar su información de pago. Una vez actualizada la información de pago, Mattermost intentará liquidar cualquier saldo pendiente."
|
||||
"translation": "Para asegurar un acceso ininterrumpido a {{.Plan}} Mattermost Cloud, por favor, póngase en contacto con su institución financiera para resolver el problema subyacente o actualizar su información de pago. Una vez actualizada la información de pago, Mattermost intentará liquidar cualquier saldo pendiente."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.info2",
|
||||
@@ -7981,7 +7973,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.questions_footer.info",
|
||||
"translation": "Envíenos un correo electrónico en cualquier momento a "
|
||||
"translation": "¿Necesitas ayuda o tienes alguna pregunta? Envíanos un correo electrónico a "
|
||||
},
|
||||
{
|
||||
"id": "api.templates.license_up_for_renewal_title",
|
||||
@@ -8025,11 +8017,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_footer_v2",
|
||||
"translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
|
||||
"translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.cloud_welcome_email.title",
|
||||
"translation": "¡El período de prueba de 14 días de tu espacio de trabajo {{.WorkSpace}} está listo!"
|
||||
"translation": "¡Tu espacio de trabajo ya está listo para usar!"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.cloud_welcome_email.subtitle_info",
|
||||
@@ -8089,7 +8081,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.cloud_welcome_email.add_apps_sub_info",
|
||||
"translation": "Agiliza tu trabajo con herramientas como Github, Jira y Zoom. Explora todas las integraciones que tenemos en nuestro"
|
||||
"translation": "Agiliza tu trabajo con herramientas como GitHub, Jira y Zoom. Explora todas las integraciones que tenemos en nuestro"
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.unknown_action",
|
||||
@@ -9153,7 +9145,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.invite_team_and_channel_subject",
|
||||
"translation": " "
|
||||
"translation": "[{{ .SiteName }}] {{ .SenderName }} te invitó a unirte al Canal {{ .ChannelName }} en el Equipo {{ .TeamDisplayName }}"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.invite_team_and_channels_body.title",
|
||||
@@ -9237,7 +9229,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.invite_team_and_channel_body.title",
|
||||
"translation": " "
|
||||
"translation": "{{ .SenderName }} te invitó a unirte al Canal {{ .ChannelName }} en el Equipo {{ .TeamDisplayName}}"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.server_inactivity_info_bullet",
|
||||
@@ -9270,5 +9262,301 @@
|
||||
{
|
||||
"id": "api.user.authorize_oauth_user.saml_response_too_long.app_error",
|
||||
"translation": " "
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.get_subscription_delinquency_date.app_error",
|
||||
"translation": "La suscripción no es morosa"
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.get_subscription.app_error",
|
||||
"translation": "No se pudo recuperar la suscripción de la nube"
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.get_current_plan_name.app_error",
|
||||
"translation": "No se puede obtener el nombre del plan actual"
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.get_cloud_products.app_error",
|
||||
"translation": "No se pudieron obtener los productos de la nube"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_90.title",
|
||||
"translation": "Tu workspace Mattermost ha sido degradado"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_90.subtitle3",
|
||||
"translation": "Para desarchivar tus datos y mantener las características de pago, actualiza tu información de pago."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_90.subtitle2",
|
||||
"translation": "Además, tus datos pueden haber sido archivados debido a las limitaciones de Cloud Starter."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_90.subtitle1",
|
||||
"translation": "Si usas características Cloud Professional o Enterprise para operaciones importantes de negocio, ya no estarán activas y experimentarás un rendimiento degradado."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_90.subject",
|
||||
"translation": "Tu workspace Mattermost Cloud ha sido degradado"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_90.secondary_action_button",
|
||||
"translation": "Ver Planes y Precios"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_90.button",
|
||||
"translation": "Actualizar pago"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_75.title",
|
||||
"translation": "Tu workspace será degradado en 15 días"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_75.subtitle3",
|
||||
"translation": "Actualiza ahora tu información de pago, o degrada a Cloud Starter."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_75.subtitle2",
|
||||
"translation": "Tu workspace será degradado a Cloud Starter. Las características de tu {{.Plan}} serán bloqueadas y algunos de los datos de tu workspace podrían ser archivados hasta que liquides completamente tu saldo pendiente."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_75.subtitle1",
|
||||
"translation": "Este es un aviso final de que no hemos recibido el pago por tu workspace Mattermost Cloud desde {{.DelinquencyDate}}"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_75.subject",
|
||||
"translation": "Tu {{.Plan}} Mattermost será degradado en 15 días"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_75.downgrade_to_starter",
|
||||
"translation": "Degradar a Cloud Starter"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_75.button",
|
||||
"translation": "Actualizar pago"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_7.title",
|
||||
"translation": "Tu pago no se completó"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_7.subtitle2",
|
||||
"translation": "Para mantener tu plan {{.Plan}} activo, por favor contacta a tu institución financiera tan pronto como sea posible, Luego, actualiza tus detalles de pago según sea necesario."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_7.subtitle1",
|
||||
"translation": "No pudimos procesar tu pago más reciente"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_7.button",
|
||||
"translation": "Actualizar pago"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_60.title",
|
||||
"translation": "Tu workspace Mattermost será degradado en 30 días"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_60.subtitle3",
|
||||
"translation": "Actualiza tu información de pago ahora o degrada a Cloud Starter abajo."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_60.subtitle2",
|
||||
"translation": "Degradaremos tu workspace automáticamente en 30 días si no somos capaces de procesar tu pago."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_60.subtitle1",
|
||||
"translation": "Por favor actualiza pronto tu información de pago para procesar tus facturas pendientes."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_60.subject",
|
||||
"translation": "Acción requerida: El workspace será degradado en 30 días"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_60.downgrade_to_starter",
|
||||
"translation": "Degradar a Cloud Starter"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_60.button",
|
||||
"translation": "Actualizar pago"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_45.title",
|
||||
"translation": "Tu workspace será degradado pronto"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_45.subtitle3",
|
||||
"translation": "Actualiza la información de tu tarjeta de crédito ahora."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_45.subtitle2",
|
||||
"translation": "Un workspace degradado podría afectar negativamente los flujos de trabajo críticos, integraciones y otras actividades críticas de negocio realizadas en tu workspace."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_45.subtitle1",
|
||||
"translation": "No fuimos capaces de cobrar el pago para las facturas con fecha {{.DelinquencyDate}}. Tu workspace está en riesgo de ser degradado."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_45.subject",
|
||||
"translation": "Aviso: Tu {{.Plan}} Mattermost será degradado pronto"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_45.button",
|
||||
"translation": "Actualizar pago"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.title",
|
||||
"translation": "Tu workspace será degradado pronto"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.subtitle2",
|
||||
"translation": "si no se toma ninguna medida, tu workspace será degradado y los siguientes datos serán archivados:"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.subtitle1",
|
||||
"translation": "Tienes tiempo para mantener activo tu {{.Plan}} Mattermost pero necesitarás resolver los problemas con tu método de pago."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.subject",
|
||||
"translation": "Actúa para mantener las característica de tu {{.Plan}} Mattermost"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.limits_documentation",
|
||||
"translation": "Ver toda la documentación de límites."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.button",
|
||||
"translation": "Actualizar pago"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.bullet.plugins",
|
||||
"translation": "Plugins e integraciones activas"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.bullet.message_history",
|
||||
"translation": "Historial de mensajes"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.bullet.files",
|
||||
"translation": "Archivos"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.bullet.cards",
|
||||
"translation": "Tarjetas de tus Boards"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_14.title",
|
||||
"translation": "Pago no recibido"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_14.subtitle2",
|
||||
"translation": "Por favor contacta a tu institución financiera para resolver cualquier problema. Luego, actualiza los detalles de pago como sean necesarios."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_14.subtitle1",
|
||||
"translation": "No pudimos realizar el cargo a la tarjeta de crédito que tenemos registrada. Por lo cual tu workspace está en riesgo de ser degradado a Cloud Starter."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_14.subject",
|
||||
"translation": "El pago está vencido para tu {{.Plan}} Mattermost."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_14.button",
|
||||
"translation": "Actualizar pago"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.license.error",
|
||||
"translation": "Tu licencia no admite cuentas de invitado"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.disabled.error",
|
||||
"translation": "Las cuentas de invitado están deshabilitadas"
|
||||
},
|
||||
{
|
||||
"id": "api.command_marketplace.unsupported.app_error",
|
||||
"translation": "El comando marketplace no es soportado por tu dispositivo."
|
||||
},
|
||||
{
|
||||
"id": "api.command_marketplace.name",
|
||||
"translation": "marketplace"
|
||||
},
|
||||
{
|
||||
"id": "api.command_marketplace.desc",
|
||||
"translation": "Abre el Marketplace"
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.success",
|
||||
"translation": "Mattermost es una plataforma de código abierto para comunicación segura, colaboración, y orquestación de trabajo entre herramientas y equipos.\nMattermost contiene tres herramientas clave:\n\n**Channels** - Mantente conectado con tu equipo vía 1:1 y en por mensajes grupales.\n**[Playbooks](/playbooks)** - Construye y configura procesos repetibles para lograr resultados específicos y predecibles.\n**[Tableros](/boards)** - Administra proyectos y tareas en una estructura de tablero Kanban para ayudar a que tú equipo logre los hitos clave.\n\n[Ver documentación y guías]({{.HelpLink}})"
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.trial_plan_bot_message",
|
||||
"translation": "los miembros {{.UsersNum}} del espacio de trabajo {{.WorkspaceName}} han solicitado comenzar la prueba Enterprise para acceder a: "
|
||||
},
|
||||
{
|
||||
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
|
||||
"translation": "Campos faltantes requeridos para enviar el correo electrónico de morosidad."
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "el nombre de grupo ya existe como un nombre reservado"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.amazons3_timeout.app_error",
|
||||
"translation": "Valor de timeout inválido {{.Value}}. Debe ser un número positivo."
|
||||
},
|
||||
{
|
||||
"id": "ent.saml.configure.certificate_parse_error.app_error",
|
||||
"translation": "SAML no pudo cargar satisfactoriamente el Identity Provider Public Certificate, por favor contacta a tu administrador del sistema."
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_badge_count.app_error",
|
||||
"translation": "No pudimos obtener el recuento de insignias para el usuario."
|
||||
},
|
||||
{
|
||||
"id": "app.team.clear_cache.app_error",
|
||||
"translation": "Error al limpiar el caché de los miembros del equipo"
|
||||
},
|
||||
{
|
||||
"id": "app.post_reminder_dm",
|
||||
"translation": "Hola, este es un recordatorio acerca de este mensaje de @{{.Username}}: {{.SiteURL}}/{{.TeamName}}/pl/{{.PostId}}"
|
||||
},
|
||||
{
|
||||
"id": "app.post.get_top_dms_for_user_since.app_error",
|
||||
"translation": "No es posible obtener los DMs top para el usuario."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.product_mode.app_error",
|
||||
"translation": "El plugin {{.Name}} no se pudo activar en el modo producto."
|
||||
},
|
||||
{
|
||||
"id": "app.notify_admin.send_notification_post.app_error",
|
||||
"translation": "No es posible enviar la publicación de notificación."
|
||||
},
|
||||
{
|
||||
"id": "app.notify_admin.save.app_error",
|
||||
"translation": "No es posible guardar los datos de notificación."
|
||||
},
|
||||
{
|
||||
"id": "app.last_accessible_file.app_error",
|
||||
"translation": "Error al obtener el último archivo accesible"
|
||||
},
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Error durante la ejecución de un job."
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "No se puede obtener el archivo porque superó los límites del plan en la nube."
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.upgrade_plan_bot_message_single",
|
||||
"translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado una actualización del workspace para: "
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.upgrade_plan_bot_message",
|
||||
"translation": "los miembros {{.UsersNum}} del workspace {{.WorkspaceName}} han solicitado una actualización del workspace para: "
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.trial_plan_bot_message_single",
|
||||
"translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado comenzar la prueba Enterprise para acceder a: "
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5159,14 +5159,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "کانال های دعوت باید بخشی از تیم دعوت باشند."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "مجوز شما از حساب های مهمان پشتیبانی نمی کند"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "حساب های مهمان غیرفعال شده است"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "خطا در عدم اعتبار دعوت نامه های ایمیل."
|
||||
|
||||
@@ -5583,14 +5583,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Les canaux présents dans l'invitation doivent faire partie de la même équipe pour laquelle vous êtes invité."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Votre licence ne prend pas en charge les comptes invités."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Les comptes invités sont désactivés"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Une erreur s'est produite lors de l'invalidation des e-mails d'invitation."
|
||||
|
||||
56
i18n/hu.json
56
i18n/hu.json
@@ -3125,7 +3125,7 @@
|
||||
},
|
||||
{
|
||||
"id": "oauth.gitlab.tos.error",
|
||||
"translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a gitlab.com webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba."
|
||||
"translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a {{.URL}} webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba."
|
||||
},
|
||||
{
|
||||
"id": "model.websocket_client.connect_fail.app_error",
|
||||
@@ -5469,15 +5469,15 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.title",
|
||||
"translation": "Sikertelen fizetés"
|
||||
"translation": "A fizetési művelet nem sikerült"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.subject",
|
||||
"translation": "Művelet szükséges: Sikertelen fizetés a Mattermost Cloudra"
|
||||
"translation": "Művelet szükséges: Sikertelen fizetés a Mattermost {{.Plan}} csomagra"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.info3",
|
||||
"translation": "A Mattermost Cloud zavartalan előfizetésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget."
|
||||
"translation": "A Mattermost {{.Plan}} zavartalan elérésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.info2",
|
||||
@@ -5795,14 +5795,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "A meghívás csatornáinak a meghívó csapat részének kell lenniük."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Az Ön licence nem támogatja a vendég fiókokat"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "A vendég fiókok le vannak tiltva"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Hiba történt az e-mail meghívók érvénytelenítésekor."
|
||||
@@ -9354,5 +9346,45 @@
|
||||
{
|
||||
"id": "api.templates.delinquency_30.subject",
|
||||
"translation": "Lépjen most, hogy megtarthassa a Mattermost {{.Plan}} funkcióit"
|
||||
},
|
||||
{
|
||||
"id": "app.notify_admin.send_notification_post.app_error",
|
||||
"translation": "Nem sikerült elküldeni az értesítési bejegyzést."
|
||||
},
|
||||
{
|
||||
"id": "app.notify_admin.save.app_error",
|
||||
"translation": "Nem sikerült elmenteni az értesítési beállításokat."
|
||||
},
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Hiba a művelet végrehajtása közben."
|
||||
},
|
||||
{
|
||||
"id": "app.cloud.get_current_plan_name.app_error",
|
||||
"translation": "Nem sikerült lekérni a jelenlegi előfizetési csomag nevét"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.button",
|
||||
"translation": "Fizetés frissítése"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.delinquency_30.bullet.plugins",
|
||||
"translation": "Aktív bővítmények és integrációk"
|
||||
},
|
||||
{
|
||||
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
|
||||
"translation": "Hiányzó kötelező mezők a késedelmes e-mail küldéséhez."
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "Nem sikerült lekérni a fájlt, mivel az meghaladta a felhőcsomag határértékét."
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_badge_count.app_error",
|
||||
"translation": "Nem sikerült lekérni a felhasználó jelvény számlálóját."
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "csoport név létezik mint lefoglalt név"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5211,10 +5211,6 @@
|
||||
"id": "api.team.add_members.user_denied",
|
||||
"translation": "Questa squadra è gestita dai gruppi. Questo utente non appartiene a un gruppo sincronizzato con questa squadra."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Gli account ospite sono disattivati"
|
||||
},
|
||||
{
|
||||
"id": "api.team.remove_member.group_constrained.app_error",
|
||||
"translation": "Impossibile rimuovere un utente da una squadra con vincoli di gruppo."
|
||||
@@ -5643,10 +5639,6 @@
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Errore invalidando gli inviti email."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "La tua licenza non supporta gli account ospite"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "I canali dell'invito devono far parte della squadra dell'invito."
|
||||
|
||||
@@ -5579,14 +5579,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "招待チャンネルは招待チーム内のチャンネルでなくてはなりません。"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "現在のライセンスではゲストアカウントはサポートされていません"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "ゲストアカウントは無効化されています"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "電子メール招待の無効化でエラーが発生しました。"
|
||||
|
||||
46
i18n/ko.json
46
i18n/ko.json
@@ -619,7 +619,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.desc",
|
||||
"translation": "Mattermost 도움말 페이지 열기"
|
||||
"translation": "Mattermost 도움말 보기"
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.name",
|
||||
@@ -5575,14 +5575,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "초대 채널은 초대한 팀의 채널이어야 합니다."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "게스트 계정을 지원하지 않는 라이선스입니다"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "게스트 계정이 비활성화되어 있습니다"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "전자우편 초대에서 유효하지 않는 오류가 발생했습니다."
|
||||
@@ -7934,5 +7926,41 @@
|
||||
{
|
||||
"id": "api.cloud.teams_limit_reached.create",
|
||||
"translation": "팀 제한에 도달했기 때문에 팀을 만들 수 없습니다"
|
||||
},
|
||||
{
|
||||
"id": "api.command_marketplace.desc",
|
||||
"translation": "마켓플레이스 열기"
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.success",
|
||||
"translation": "Mattermost는 도구와 팀을 넘나드는 안전한 소통, 협업, 작업 조율을 위한 오픈소스 플랫폼입니다.\nMattermost는 세가지 핵심 도구를 갖고 있습니다:\n\n**채널** - 1:1 및 그룹 채팅을 통해 팀과의 소통을 유지하세요.\n**[플레이북](/playbooks)** - 반복가능한 작업들을 구성하여 구체적이고 예측 가능한 결과를 만드세요.\n**[보드](/boards)** - 팀의 핵심 마일스톤에 도달하기 위해 칸반 보드에서 작업을 만들고 프로젝트를 관리하세요.\n\n[문서 및 가이드 살펴보기]({{.HelpLink}})"
|
||||
},
|
||||
{
|
||||
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
|
||||
"translation": "연체 이메일을 보내기 위한 필수 항목이 누락되었습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.error_set_first_admin_complete_setup",
|
||||
"translation": "스토어에서 첫 번째 관리자의 설정을 저장하는 동안 오류가 발생했습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.error_get_first_admin_visit_marketplace_status",
|
||||
"translation": "스토어에서 첫 번째 관리자의 마켓플레이스 방문 상태를 검색하는 동안 오류가 발생했습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.error_get_first_admin_complete_setup",
|
||||
"translation": "스토어에서 첫 번째 관리자 설정을 검색하는 동안 오류가 발생했습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_remote.invite_summary",
|
||||
"translation": "AES-256으로 암호화된 초대를 비밀번호와 함께 외부 Mattermost 시스템 관리자에게 보냅니다. 초대를 받기 위해 `{{.Command}}` 슬래시 명령어를 사용합니다.\n\n```\n{{.Invitation}}\n```\n\n**보안 연결이 다음을 통해 Mattermost에 액세스할 수 있는지 확인하십시오** {{.SiteURL}}"
|
||||
},
|
||||
{
|
||||
"id": "api.command_marketplace.unsupported.app_error",
|
||||
"translation": "마켓플레이스 명령어는 현재 기기에서 지원하지 않습니다."
|
||||
},
|
||||
{
|
||||
"id": "api.command_marketplace.name",
|
||||
"translation": "마켓플레이스"
|
||||
}
|
||||
]
|
||||
|
||||
44
i18n/nl.json
44
i18n/nl.json
@@ -1777,7 +1777,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_change_verify_body.title",
|
||||
"translation": "Je hebt je e-mailadres bijgewerkt"
|
||||
"translation": "Je hebt je e-mailadres succesvol bijgewerkt"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_change_verify_subject",
|
||||
@@ -4325,7 +4325,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.post.is_valid.create_at.app_error",
|
||||
"translation": "Aangemaakt op moet een geldige tijd bevatten"
|
||||
"translation": "Aangemaakt op moet een geldige tijd bevatten."
|
||||
},
|
||||
{
|
||||
"id": "model.post.is_valid.file_ids.app_error",
|
||||
@@ -4781,11 +4781,11 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.edge",
|
||||
"translation": "Versie 44+"
|
||||
"translation": "Versie 95+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.chrome",
|
||||
"translation": "Versie 100+"
|
||||
"translation": "Versie 106+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.learn_more",
|
||||
@@ -5607,14 +5607,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "De kanalen van de uitgenodigde moet een kanaal dat deel uitmaakt van het team."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Je licentie ondersteunt geen gastgebruikers"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Gast accounts zijn uitgeschakeld"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Fout bij afkeuren e-mailuitnodigingen."
|
||||
@@ -9537,5 +9529,33 @@
|
||||
{
|
||||
"id": "api.templates.delinquency_90.title",
|
||||
"translation": "Jouw Mattermost-werkruimte werd gedowngraded"
|
||||
},
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Fout tijdens het uitvoeren van de opdracht."
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "groepsnaam bestaat al als een gereserveerde naam"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.product_mode.app_error",
|
||||
"translation": "Plugin {{.Name}} kan niet worden ingeschakeld in de productmodus."
|
||||
},
|
||||
{
|
||||
"id": "app.last_accessible_file.app_error",
|
||||
"translation": "Fout bij het ophalen van het laatst toegankelijke bestand"
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "Kan het bestand niet ophalen omdat dit bestand over de limiet van het cloud plan is."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.license.error",
|
||||
"translation": "Jouw licentie ondersteunt geen gastaccounts"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.disabled.error",
|
||||
"translation": "Gastaccounts zijn uitgeschakeld"
|
||||
}
|
||||
]
|
||||
|
||||
36
i18n/pl.json
36
i18n/pl.json
@@ -4761,11 +4761,11 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.edge",
|
||||
"translation": "Wersja 44+"
|
||||
"translation": "Wersja 95+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.chrome",
|
||||
"translation": "Wersja 100+"
|
||||
"translation": "Wersja 106+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.learn_more",
|
||||
@@ -5583,14 +5583,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Kanały do których chcesz zaprosić, muszą być częścią zespołu do którego chcesz zaprosić."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Twoja licencja nie obsługuje kont gości"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Konta gości są wyłączone"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Błąd unieważniania zaproszenia E-Mail."
|
||||
@@ -9542,5 +9534,29 @@
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Błąd podczas wykonywania zadania."
|
||||
},
|
||||
{
|
||||
"id": "app.last_accessible_file.app_error",
|
||||
"translation": "Błąd pobierania ostatniego dostępnego pliku"
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "Nie można pobrać pliku, ponieważ przekroczony został limit planu chmury."
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "nazwa grupy już istnieje jako nazwa zastrzeżona"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.product_mode.app_error",
|
||||
"translation": "Wtyczka {{.Name}} nie może być włączona w trybie produktu."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.license.error",
|
||||
"translation": "Twoja licencja nie wspiera kont gości"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.disabled.error",
|
||||
"translation": "Konta gości są wyłączone"
|
||||
}
|
||||
]
|
||||
|
||||
112
i18n/pt-BR.json
112
i18n/pt-BR.json
@@ -621,7 +621,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.desc",
|
||||
"translation": "Abrir a página de ajuda do Mattermost"
|
||||
"translation": "Exibir descrição de ajuda do Mattermost"
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.name",
|
||||
@@ -1173,7 +1173,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.marshal_error",
|
||||
"translation": "marshal error"
|
||||
"translation": "Falha para marshal."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.allow_oauth.redirect_callback.app_error",
|
||||
@@ -1789,7 +1789,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_footer",
|
||||
"translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações de Conta > Notificações."
|
||||
"translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações > Notificações."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_info1",
|
||||
@@ -1813,7 +1813,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.invite_body.button",
|
||||
"translation": "Entrar na Equipe"
|
||||
"translation": "Entrar agora"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.invite_body.title",
|
||||
@@ -1857,7 +1857,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.post_body.button",
|
||||
"translation": "Ir Para a Publicação"
|
||||
"translation": "Responder no Mattermost"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.reset_body.button",
|
||||
@@ -1865,7 +1865,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.reset_body.title",
|
||||
"translation": "Você solicitou uma redefinição de senha"
|
||||
"translation": "Redefina sua senha"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.reset_subject",
|
||||
@@ -4333,11 +4333,11 @@
|
||||
},
|
||||
{
|
||||
"id": "model.post.is_valid.file_ids.app_error",
|
||||
"translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a cinco arquivos no máximo. Por favor, use publicações adicionais para mais arquivos."
|
||||
"translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a 10 arquivos no máximo. Por favor, use publicações adicionais para mais arquivos."
|
||||
},
|
||||
{
|
||||
"id": "model.post.is_valid.filenames.app_error",
|
||||
"translation": "Nomes de arquivo inválidos"
|
||||
"translation": "Nomes de arquivo inválidos."
|
||||
},
|
||||
{
|
||||
"id": "model.post.is_valid.hashtags.app_error",
|
||||
@@ -4573,7 +4573,7 @@
|
||||
},
|
||||
{
|
||||
"id": "oauth.gitlab.tos.error",
|
||||
"translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até gitlab.com para aceitar os termos e depois tente se logar novamente no Mattermost."
|
||||
"translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até {{.URL}} para aceitar os termos e depois tente se logar novamente no Mattermost."
|
||||
},
|
||||
{
|
||||
"id": "plugin.api.update_user_status.bad_status",
|
||||
@@ -4745,19 +4745,19 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_os_version.windows",
|
||||
"translation": "Windows 7+"
|
||||
"translation": "Windows 8.1+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_os_version.mac",
|
||||
"translation": "macOS 10.9+"
|
||||
"translation": "macOS 10.14+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.safari",
|
||||
"translation": "Versão 12+"
|
||||
"translation": "Versão 14.1+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.firefox",
|
||||
"translation": "Versão 60+"
|
||||
"translation": "Versão 91+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.edge",
|
||||
@@ -4765,7 +4765,7 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.chrome",
|
||||
"translation": "Versão 61+"
|
||||
"translation": "Version 100+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.learn_more",
|
||||
@@ -5587,14 +5587,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Os canais do convite devem ser parte da equipe do convite."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Sua licença não suporta contas convidado"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Contas convidado estão desativadas"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Erro ao invalidar convites por email."
|
||||
@@ -5945,7 +5937,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.push_notifications.session.expired",
|
||||
"translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.daysCount}} dia(s)."
|
||||
"translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.hoursCount}} hour(s)."
|
||||
},
|
||||
{
|
||||
"id": "api.post.error_get_post_id.pending",
|
||||
@@ -6189,7 +6181,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.preference.permanent_delete_by_user.app_error",
|
||||
"translation": "Encontramos um erro enquanto excluía as preferências."
|
||||
"translation": "Encontramos um erro enquanto excluiamos as preferências."
|
||||
},
|
||||
{
|
||||
"id": "app.preference.get_category.app_error",
|
||||
@@ -6897,7 +6889,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.upload_session.is_valid.user_id.app_error",
|
||||
"translation": "Valor inválido para user_id."
|
||||
"translation": "Valor inválido para UserId"
|
||||
},
|
||||
{
|
||||
"id": "model.upload_session.is_valid.type.app_error",
|
||||
@@ -6905,15 +6897,15 @@
|
||||
},
|
||||
{
|
||||
"id": "model.upload_session.is_valid.path.app_error",
|
||||
"translation": "Valor inválido para path."
|
||||
"translation": "Valor inválido para path"
|
||||
},
|
||||
{
|
||||
"id": "model.upload_session.is_valid.id.app_error",
|
||||
"translation": "Valor inválido para id."
|
||||
"translation": "Valor inválido para id"
|
||||
},
|
||||
{
|
||||
"id": "model.upload_session.is_valid.create_at.app_error",
|
||||
"translation": "Valor inválido para create_at."
|
||||
"translation": "Valor inválido para Create_At"
|
||||
},
|
||||
{
|
||||
"id": "app.upload.upload_data.large_image.app_error",
|
||||
@@ -7601,7 +7593,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.copyright",
|
||||
"translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301"
|
||||
"translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
|
||||
},
|
||||
{
|
||||
"id": "api.roles.patch_roles.not_allowed_permission.error",
|
||||
@@ -7645,15 +7637,15 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.title",
|
||||
"translation": "Pagamento Falhou"
|
||||
"translation": "O Pagamento não foi bem sucedido"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.subject",
|
||||
"translation": "Ação necessária: Falha no pagamento para Mattermost Cloud"
|
||||
"translation": "Ação necessária: Falha no pagamento para Mattermost {{.Plan}}"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.info3",
|
||||
"translation": "Para garantir a assinatura ininterrupta do Mattermost Cloud, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente."
|
||||
"translation": "Para garantir a acesso ininterrupto do Mattermost {{.Plan}}, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.payment_failed.info2",
|
||||
@@ -8069,7 +8061,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.user.get_authorization_code.endpoint.app_error",
|
||||
"translation": "Erro ao recuperar o endpoint da Descoberta de Documento."
|
||||
"translation": "Erro ao recuperar o endpoint do Documento de Descoberta."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.welcome_body.subTitle2",
|
||||
@@ -8117,11 +8109,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.questions_footer.info",
|
||||
"translation": "Envie-nos um email a qualquer momento em "
|
||||
"translation": "Precisa de ajuda ou tem dúvidas? Envie-nos um e-mail em "
|
||||
},
|
||||
{
|
||||
"id": "api.templates.email_footer_v2",
|
||||
"translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301"
|
||||
"translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
|
||||
},
|
||||
{
|
||||
"id": "api.post.search_files.invalid_body.app_error",
|
||||
@@ -8333,7 +8325,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.context.remote_id_invalid.app_error",
|
||||
"translation": "Não foi possível encontrar o id do cluster remoto {{.RemoteId}}."
|
||||
"translation": "Não foi possível encontrar o ID de uma conexão segura {{.RemoteId}}."
|
||||
},
|
||||
{
|
||||
"id": "api.context.json_encoding.app_error",
|
||||
@@ -8357,15 +8349,15 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.remote_uninvited",
|
||||
"translation": "Remoto `{{.RemoteId}}` sem convite."
|
||||
"translation": "Conexão segura `{{.RemoteId}}` sem convite."
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.remote_id_invalid.error",
|
||||
"translation": "O id do cluster remoto é inválido: {{.Error}}"
|
||||
"translation": "O ID de conexão segura é inválido: {{.Error}}"
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.remote_id.help",
|
||||
"translation": "Id de uma instância remota existente. Veja o comando `remote` para adicionar uma instância remota."
|
||||
"translation": "ID de uma conexão segura existente. Veja o comando `remote` para adicionar uma conexão segura."
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.permission_required",
|
||||
@@ -8437,7 +8429,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.cloud_welcome_email.signin_sub_info2",
|
||||
"translation": "para a melhor experiência em PC, Mac, iOS e Android."
|
||||
"translation": "para a melhor experiência em Windows, Linux, Mac, iOS e Android."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.cloud_welcome_email.signin_sub_info",
|
||||
@@ -8469,7 +8461,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.templates.cloud_welcome_email.add_apps_sub_info",
|
||||
"translation": "Otimize seu trabalho com ferramentas como Github, Google Agenda e Chrome. Explore todas as integrações que temos"
|
||||
"translation": "Otimize seu trabalho com ferramentas como GitHub, Jira e Zoom. Explore todas as integrações que temos em nosso"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.cloud_welcome_email.add_apps_info",
|
||||
@@ -8481,11 +8473,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.update_not_unique.app_error",
|
||||
"translation": "Já existe um cluster remoto com a mesmo url."
|
||||
"translation": "Já existe uma conexão segura com a mesmo url."
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.update.app_error",
|
||||
"translation": "Encontramos um erro ao atualizar o cluster remoto."
|
||||
"translation": "Encontramos um erro ao atualizar a conexão segura."
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.service_not_enabled.app_error",
|
||||
@@ -8493,11 +8485,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.save_not_unique.app_error",
|
||||
"translation": "O cluster remoto já foi adicionado."
|
||||
"translation": "A conexão segura já foi adicionada."
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.save.app_error",
|
||||
"translation": "Encontramos um erro ao salvar o cluster remoto."
|
||||
"translation": "Encontramos um erro ao salvar a conexão segura."
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.invalid_topic.app_error",
|
||||
@@ -8509,11 +8501,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.get.app_error",
|
||||
"translation": "Encontramos um erro ao recuperar um cluster remoto."
|
||||
"translation": "Encontramos um erro ao recuperar uma conexão segura."
|
||||
},
|
||||
{
|
||||
"id": "api.remote_cluster.delete.app_error",
|
||||
"translation": "Encontramos um erro ao excluir o cluster remoto."
|
||||
"translation": "Encontramos um erro ao excluir uma conexão segura."
|
||||
},
|
||||
{
|
||||
"id": "api.job.unable_to_create_job.incorrect_job_type",
|
||||
@@ -8533,11 +8525,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.context.remote_id_missing.app_error",
|
||||
"translation": "Falta a id do cluster remoto."
|
||||
"translation": "Falta a ID de conexão sergura."
|
||||
},
|
||||
{
|
||||
"id": "api.context.remote_id_mismatch.app_error",
|
||||
"translation": "Incompatibilidade de id do cluster remoto."
|
||||
"translation": "Incompatibilidade de ID da conexão segura."
|
||||
},
|
||||
{
|
||||
"id": "api.context.invitation_expired.error",
|
||||
@@ -8549,11 +8541,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.uninvite_remote_id.help",
|
||||
"translation": "Id da instância remota para desconvidar."
|
||||
"translation": "ID de uma conexão segura para desconvidar."
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.uninvite_remote.help",
|
||||
"translation": "Cancela o convite de uma instância remota deste canal compartilhado"
|
||||
"translation": "Cancela o convite de uma conexão segura deste canal compartilhado"
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.shared_channel_unavailable",
|
||||
@@ -8569,11 +8561,11 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.remote_not_valid",
|
||||
"translation": "Deve especificar um cluster remoto válido para cancelar o convite"
|
||||
"translation": "Deve especificar uma ID de conexão segura válida para cancelar o convite"
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.remote_already_invited",
|
||||
"translation": "O cluster remoto já foi convidado."
|
||||
"translation": "A conexão segura já foi convidada."
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.not_shared_channel_unshare",
|
||||
@@ -8581,15 +8573,15 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.no_remote_invited",
|
||||
"translation": "Nenhum remoto foi convidado para este canal compartilhado."
|
||||
"translation": "Nenhuma conexão remota foi convidada a este canal."
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.name",
|
||||
"translation": "compartilhar"
|
||||
"translation": "compartilhar-canal"
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.must_specify_valid_remote",
|
||||
"translation": "Deve especificar um ID de conjunto remoto válido para convidar."
|
||||
"translation": "Deve especificar um ID de conjunto válido seguro para convidar."
|
||||
},
|
||||
{
|
||||
"id": "api.command_share.missing_action",
|
||||
@@ -8650,5 +8642,13 @@
|
||||
{
|
||||
"id": "Boards",
|
||||
"translation": "Quadros"
|
||||
},
|
||||
{
|
||||
"id": "Playbooks",
|
||||
"translation": "Playbooks"
|
||||
},
|
||||
{
|
||||
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
|
||||
"translation": "Campos faltando para envio de email."
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5587,14 +5587,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Canalele invitației trebuie să facă parte din echipa invitației."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Licența dvs. nu acceptă conturile de invitați"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Conturile oaspeților sunt dezactivate"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Eroare la invalidarea invitațiilor de e-mail."
|
||||
|
||||
@@ -5235,10 +5235,6 @@
|
||||
"id": "api.team.demote_user_to_guest.license.error",
|
||||
"translation": "Ваша лицензия не поддерживает гостевые учётные записи"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Ваша лицензия не поддерживает гостевые учётные записи"
|
||||
},
|
||||
{
|
||||
"id": "api.team.remove_member.group_constrained.app_error",
|
||||
"translation": "Невозможно удалить пользователя из управляемой группами команды."
|
||||
@@ -5675,10 +5671,6 @@
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Ошибка аннулирования e-mail приглашений."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Гостевые учётные записи отключены"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Каналы в приглашении должны быть частью команды в приглашении."
|
||||
|
||||
28
i18n/sv.json
28
i18n/sv.json
@@ -5275,14 +5275,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "De kanaler som ingår i inbjudan måste tillhöra de team som inbjudan gäller för."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Din licens tillåter inte gäståtkomst"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Gäståtkomst har inaktiverats"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Fel vid ogiltiggörande av e-postinbjudan."
|
||||
@@ -9541,5 +9533,25 @@
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Fel under utförandet av jobbet."
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "gruppnamnet finns redan som ett reserverat namn"
|
||||
},
|
||||
{
|
||||
"id": "app.last_accessible_file.app_error",
|
||||
"translation": "Fel vid hämtning av den senast tillgängliga filen"
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "Ditt abonnemang begränsar möjligheten att hämta filen."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.license.error",
|
||||
"translation": "Din licens tillåter inte gäståtkomst"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.disabled.error",
|
||||
"translation": "Gäståtkomst har inaktiverats"
|
||||
}
|
||||
]
|
||||
|
||||
44
i18n/tr.json
44
i18n/tr.json
@@ -2725,7 +2725,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.import.validate_team_import_data.name_reserved.error",
|
||||
"translation": "Takım adında ayrılmış bir sözcük var."
|
||||
"translation": "Takım adında sistem kullanımına ayrılmış bir sözcük var."
|
||||
},
|
||||
{
|
||||
"id": "app.import.validate_team_import_data.scheme_invalid.error",
|
||||
@@ -5577,7 +5577,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.ldap_groups.existing_reserved_name_error",
|
||||
"translation": "aynı adlı bir grup zaten ayrılmış bir ad olarak var"
|
||||
"translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var"
|
||||
},
|
||||
{
|
||||
"id": "api.ldap_groups.existing_user_name_error",
|
||||
@@ -5655,14 +5655,6 @@
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "E-posta çağrıları geçersiz kılınırken sorun çıktı."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Konuk hesapları kullanılmıyor"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Lisansınızda konuk hesaplarını kullanma özelliği yok"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "Çağrılan kanallar çağrı yapılan takımın bir parçası olmalıdır."
|
||||
@@ -5849,11 +5841,11 @@
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.chrome",
|
||||
"translation": "Sürüm 100+"
|
||||
"translation": "Sürüm 106+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.edge",
|
||||
"translation": "Sürüm 44+"
|
||||
"translation": "Sürüm 95+"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_browser.min_browser_version.firefox",
|
||||
@@ -9537,5 +9529,33 @@
|
||||
{
|
||||
"id": "app.user.get_badge_count.app_error",
|
||||
"translation": "Kullanıcının nişan sayısını alamadık."
|
||||
},
|
||||
{
|
||||
"id": "app.job.error",
|
||||
"translation": "Görev yürütülürken sorun çıktı."
|
||||
},
|
||||
{
|
||||
"id": "app.last_accessible_file.app_error",
|
||||
"translation": "Erişilebilen son dosya alınırken sorun çıktı"
|
||||
},
|
||||
{
|
||||
"id": "app.file.cloud.get.app_error",
|
||||
"translation": "Cloud tarifesinin sınırlarını aştığından dosya alınamadı."
|
||||
},
|
||||
{
|
||||
"id": "model.group.name.reserved_name.app_error",
|
||||
"translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.product_mode.app_error",
|
||||
"translation": "{{.Name}} uygulama eki ürün kipinde etkinleştirilemez."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.license.error",
|
||||
"translation": "Lisansınız konuk hesaplarının kullanılmasını desteklemiyor"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invite_guests_to_channels.disabled.error",
|
||||
"translation": "Konuk hesapları devre dışı bırakılmış"
|
||||
}
|
||||
]
|
||||
|
||||
20
i18n/uk.json
20
i18n/uk.json
@@ -5583,14 +5583,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "The channels of the invite must be part of the team of the invite."
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "Редакція не підтримує Elasticsearch. "
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "Guest accounts are disabled"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "Error invalidating email invites."
|
||||
@@ -6858,5 +6850,17 @@
|
||||
{
|
||||
"id": "api.admin.add_certificate.parseform.app_error",
|
||||
"translation": "Помилка синтаксісу багато-форматного запиту"
|
||||
},
|
||||
{
|
||||
"id": "api.channel.patch_channel_moderations.cache_invalidation.error",
|
||||
"translation": "Помилка валідації кешу"
|
||||
},
|
||||
{
|
||||
"id": "api.back_to_app",
|
||||
"translation": "Повернутися до {{.SiteName}}"
|
||||
},
|
||||
{
|
||||
"id": "Channels",
|
||||
"translation": "Канали"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5611,10 +5611,6 @@
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "作废邮件邀请错误。"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "您的许可证不支持访客帐号"
|
||||
},
|
||||
{
|
||||
"id": "api.team.remove_member.group_constrained.app_error",
|
||||
"translation": "无法从组受限团队移除用户。"
|
||||
@@ -5627,10 +5623,6 @@
|
||||
"id": "api.team.search_teams.pagination_not_implemented.public_team_search",
|
||||
"translation": "仅搜索公共团队不支持分页。"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "访客帐号已停用"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.remove_expired_license.subject",
|
||||
"translation": "Mattermost 企业许可证已停用。"
|
||||
|
||||
@@ -421,7 +421,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_header.update_channel.app_error",
|
||||
"translation": "更新當前頻道時錯誤。"
|
||||
"translation": "更新當前頻道名稱時錯誤。"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.channel.app_error",
|
||||
@@ -453,7 +453,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.update_channel.app_error",
|
||||
"translation": "更新當前頻道時錯誤。"
|
||||
"translation": "更新當前頻道用途時錯誤。"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_remove.channel.app_error",
|
||||
@@ -619,7 +619,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.desc",
|
||||
"translation": "開啟 Mattermost 說明頁面"
|
||||
"translation": "顯示 Mattermost 說明頁面"
|
||||
},
|
||||
{
|
||||
"id": "api.command_help.name",
|
||||
@@ -5579,14 +5579,6 @@
|
||||
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
|
||||
"translation": "邀請的頻道必須屬於邀請的團隊"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.license.error",
|
||||
"translation": "授權不支援訪客帳號"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invate_guests_to_channels.disabled.error",
|
||||
"translation": "訪客帳號已停用"
|
||||
},
|
||||
{
|
||||
"id": "api.team.invalidate_all_email_invites.app_error",
|
||||
"translation": "使電子郵件邀請無效時失敗。"
|
||||
@@ -7302,5 +7294,57 @@
|
||||
{
|
||||
"id": "api.cloud.teams_limit_reached.create",
|
||||
"translation": "無法建立團隊,已達到團隊數量上限"
|
||||
},
|
||||
{
|
||||
"id": "api.command_custom_status.success",
|
||||
"translation": "你的狀態已設為 “{{.EmojiName}} {{.StatusMessage}}”。您可以從頻道側邊欄中的狀態彈出視窗更改您的狀態。"
|
||||
},
|
||||
{
|
||||
"id": "api.command_custom_status.name",
|
||||
"translation": "狀態"
|
||||
},
|
||||
{
|
||||
"id": "api.command_custom_status.hint",
|
||||
"translation": "[:emoji_name:] [status_message] 或清除"
|
||||
},
|
||||
{
|
||||
"id": "api.command_custom_status.desc",
|
||||
"translation": "設定或清除您的狀態"
|
||||
},
|
||||
{
|
||||
"id": "api.command_custom_status.clear.success",
|
||||
"translation": "狀態已清除。"
|
||||
},
|
||||
{
|
||||
"id": "api.command_custom_status.clear.app_error",
|
||||
"translation": "清除狀態錯誤。"
|
||||
},
|
||||
{
|
||||
"id": "api.command_custom_status.app_error",
|
||||
"translation": "設定狀態錯誤。"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_purpose.update_channel.max_length",
|
||||
"translation": "輸入的文本超出字數限制。頻道用途不得超過 {{.MaxLength}} 個字數。"
|
||||
},
|
||||
{
|
||||
"id": "api.command_channel_header.update_channel.max_length",
|
||||
"translation": "輸入的文本超出字數限制。頻道標題不得超過 {{.MaxLength}} 個字數。"
|
||||
},
|
||||
{
|
||||
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
|
||||
"translation": "缺少發送拖欠電子郵件的必要欄位。"
|
||||
},
|
||||
{
|
||||
"id": "api.cloud.cws_webhook_event_missing_error",
|
||||
"translation": "Webhook 事件未處理。事件可能遺失或無效。"
|
||||
},
|
||||
{
|
||||
"id": "api.channel.create_channel.direct_channel.team_restricted_error",
|
||||
"translation": "無法在這些使用者間創建直連頻道因為他們不屬於同一個團隊。"
|
||||
},
|
||||
{
|
||||
"id": "api.admin.saml.failure_reset_authdata_to_email.app_error",
|
||||
"translation": "無法寄出重設AuthData欄位失敗的電子郵件。"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8004,8 +8004,8 @@ func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodReq
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) RequestCloudTrial(email *StartCloudTrialRequest) (*Subscription, *Response, error) {
|
||||
payload, err := json.Marshal(email)
|
||||
func (c *Client4) RequestCloudTrial(cloudTrialRequest *StartCloudTrialRequest) (*Subscription, *Response, error) {
|
||||
payload, err := json.Marshal(cloudTrialRequest)
|
||||
if err != nil {
|
||||
return nil, nil, NewAppError("RequestCloudTrial", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
@@ -110,11 +110,12 @@ type ValidateBusinessEmailResponse struct {
|
||||
|
||||
// CloudCustomerInfo represents editable info of a customer.
|
||||
type CloudCustomerInfo struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
ContactFirstName string `json:"contact_first_name,omitempty"`
|
||||
ContactLastName string `json:"contact_last_name,omitempty"`
|
||||
NumEmployees int `json:"num_employees"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
ContactFirstName string `json:"contact_first_name,omitempty"`
|
||||
ContactLastName string `json:"contact_last_name,omitempty"`
|
||||
NumEmployees int `json:"num_employees"`
|
||||
MonthlySubscriptionIntentWireTransfer string `json:"monthly_subscription_intent_wire_transfer"`
|
||||
}
|
||||
|
||||
// Address model represents a customer's address.
|
||||
|
||||
@@ -2747,6 +2747,16 @@ func (s *CloudSettings) SetDefaults() {
|
||||
}
|
||||
}
|
||||
|
||||
type ProductSettings struct {
|
||||
EnablePublicSharedBoards *bool
|
||||
}
|
||||
|
||||
func (s *ProductSettings) SetDefaults() {
|
||||
if s.EnablePublicSharedBoards == nil {
|
||||
s.EnablePublicSharedBoards = NewBool(false)
|
||||
}
|
||||
}
|
||||
|
||||
type PluginState struct {
|
||||
Enable bool
|
||||
}
|
||||
@@ -3136,6 +3146,7 @@ type Config struct {
|
||||
DataRetentionSettings DataRetentionSettings
|
||||
MessageExportSettings MessageExportSettings
|
||||
JobSettings JobSettings
|
||||
ProductSettings ProductSettings
|
||||
PluginSettings PluginSettings
|
||||
DisplaySettings DisplaySettings
|
||||
GuestAccountsSettings GuestAccountsSettings
|
||||
@@ -3235,6 +3246,7 @@ func (o *Config) SetDefaults() {
|
||||
o.ThemeSettings.SetDefaults()
|
||||
o.ClusterSettings.SetDefaults()
|
||||
o.PluginSettings.SetDefaults(o.LogSettings)
|
||||
o.ProductSettings.SetDefaults()
|
||||
o.AnalyticsSettings.SetDefaults()
|
||||
o.ComplianceSettings.SetDefaults()
|
||||
o.LocalizationSettings.SetDefaults()
|
||||
|
||||
@@ -76,6 +76,8 @@ type FeatureFlags struct {
|
||||
SendWelcomePost bool
|
||||
|
||||
PostPriority bool
|
||||
|
||||
PeopleProduct bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -104,6 +106,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.PlanUpgradeButtonText = "upgrade"
|
||||
f.SendWelcomePost = true
|
||||
f.PostPriority = false
|
||||
f.PeopleProduct = false
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) Plugins() map[string]string {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -260,6 +261,23 @@ func StartOfDayForTimeRange(timeRange string, location *time.Location) *time.Tim
|
||||
return &resultTime
|
||||
}
|
||||
|
||||
// GetStartOfDayForTimeRange gets the unix start time in milliseconds from the given time range.
|
||||
// Time range can be one of: "today", "7_day", or "28_day".
|
||||
func GetStartOfDayForTimeRange(timeRange string, location *time.Location) (*time.Time, *AppError) {
|
||||
now := time.Now().In(location)
|
||||
resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
|
||||
switch timeRange {
|
||||
case TimeRangeToday:
|
||||
case TimeRange7Day:
|
||||
resultTime = resultTime.Add(time.Hour * time.Duration(-144))
|
||||
case TimeRange28Day:
|
||||
resultTime = resultTime.Add(time.Hour * time.Duration(-648))
|
||||
default:
|
||||
return nil, NewAppError("GetStartOfDayForTimeRange", "model.insights.get_start_of_day_for_time_range.time_range.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
return &resultTime, nil
|
||||
}
|
||||
|
||||
// GetTopReactionListWithPagination adds a rank to each item in the given list of TopReaction and checks if there is
|
||||
// another page that can be fetched based on the given limit and offset. The given list of TopReaction is assumed to be
|
||||
// sorted by Count. Returns a TopReactionList.
|
||||
|
||||
@@ -38,4 +38,5 @@ const (
|
||||
MigrationKeyAddPlaybooksPermissions = "playbooks_permissions"
|
||||
MigrationKeyAddCustomUserGroupsPermissions = "custom_groups_permissions"
|
||||
MigrationKeyAddPlayboosksManageRolesPermissions = "playbooks_manage_roles"
|
||||
MigrationKeyAddProductsBoardsPermissions = "products_boards"
|
||||
)
|
||||
|
||||
@@ -354,6 +354,9 @@ var PermissionRunManageProperties *Permission
|
||||
var PermissionRunManageMembers *Permission
|
||||
var PermissionRunView *Permission
|
||||
|
||||
var PermissionSysconsoleReadProductsBoards *Permission
|
||||
var PermissionSysconsoleWriteProductsBoards *Permission
|
||||
|
||||
// General permission that encompasses all system admin functions
|
||||
// in the future this could be broken up to allow access to some
|
||||
// admin functions but not others
|
||||
@@ -2070,6 +2073,19 @@ func initializePermissions() {
|
||||
PermissionScopeRun,
|
||||
}
|
||||
|
||||
PermissionSysconsoleReadProductsBoards = &Permission{
|
||||
"sysconsole_read_products_boards",
|
||||
"",
|
||||
"",
|
||||
PermissionScopeSystem,
|
||||
}
|
||||
PermissionSysconsoleWriteProductsBoards = &Permission{
|
||||
"sysconsole_write_products_boards",
|
||||
"",
|
||||
"",
|
||||
PermissionScopeSystem,
|
||||
}
|
||||
|
||||
SysconsoleReadPermissions = []*Permission{
|
||||
PermissionSysconsoleReadAboutEditionAndLicense,
|
||||
PermissionSysconsoleReadBilling,
|
||||
@@ -2125,6 +2141,7 @@ func initializePermissions() {
|
||||
PermissionSysconsoleReadExperimentalFeatures,
|
||||
PermissionSysconsoleReadExperimentalFeatureFlags,
|
||||
PermissionSysconsoleReadExperimentalBleve,
|
||||
PermissionSysconsoleReadProductsBoards,
|
||||
}
|
||||
|
||||
SysconsoleWritePermissions = []*Permission{
|
||||
@@ -2182,6 +2199,7 @@ func initializePermissions() {
|
||||
PermissionSysconsoleWriteExperimentalFeatures,
|
||||
PermissionSysconsoleWriteExperimentalFeatureFlags,
|
||||
PermissionSysconsoleWriteExperimentalBleve,
|
||||
PermissionSysconsoleWriteProductsBoards,
|
||||
}
|
||||
|
||||
SystemScopedPermissionsMinusSysconsole := []*Permission{
|
||||
|
||||
@@ -261,6 +261,7 @@ func init() {
|
||||
PermissionSysconsoleReadExperimentalFeatures.Id,
|
||||
PermissionSysconsoleReadExperimentalFeatureFlags.Id,
|
||||
PermissionSysconsoleReadExperimentalBleve.Id,
|
||||
PermissionSysconsoleReadProductsBoards.Id,
|
||||
}
|
||||
|
||||
SystemManagerDefaultPermissions = []string{
|
||||
@@ -339,6 +340,8 @@ func init() {
|
||||
PermissionSysconsoleWriteIntegrationsBotAccounts.Id,
|
||||
PermissionSysconsoleWriteIntegrationsGif.Id,
|
||||
PermissionSysconsoleWriteIntegrationsCors.Id,
|
||||
PermissionSysconsoleReadProductsBoards.Id,
|
||||
PermissionSysconsoleWriteProductsBoards.Id,
|
||||
}
|
||||
|
||||
SystemCustomGroupAdminDefaultPermissions = []string{
|
||||
|
||||
@@ -39,6 +39,7 @@ type PostService interface {
|
||||
//
|
||||
// The service shall be registered via app.PermissionKey service key.
|
||||
type PermissionService interface {
|
||||
HasPermissionTo(userID string, permission *model.Permission) bool
|
||||
HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool
|
||||
HasPermissionToChannel(askingUserID string, channelID string, permission *model.Permission) bool
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ const (
|
||||
TrackConfigBleve = "config_bleve"
|
||||
TrackConfigExport = "config_export"
|
||||
TrackFeatureFlags = "config_feature_flags"
|
||||
TrackConfigProducts = "products"
|
||||
TrackPermissionsGeneral = "permissions_general"
|
||||
TrackPermissionsSystemScheme = "permissions_system_scheme"
|
||||
TrackPermissionsTeamSchemes = "permissions_team_schemes"
|
||||
@@ -827,6 +828,10 @@ func (ts *TelemetryService) trackConfig() {
|
||||
"retention_days": *cfg.ExportSettings.RetentionDays,
|
||||
})
|
||||
|
||||
ts.SendTelemetry(TrackConfigProducts, map[string]any{
|
||||
"enable_public_shared_boards": *cfg.ProductSettings.EnablePublicSharedBoards,
|
||||
})
|
||||
|
||||
// Convert feature flags to map[string]any for sending
|
||||
flags := cfg.FeatureFlags.ToMap()
|
||||
interfaceFlags := make(map[string]any)
|
||||
|
||||
@@ -37,7 +37,7 @@ func (s *SearchUserStore) deleteUserIndex(user *model.User) {
|
||||
func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchOptions) ([]*model.User, error) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsSearchEnabled() {
|
||||
listOfAllowedChannels, nErr := s.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
|
||||
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, "", options.ViewRestrictions)
|
||||
if nErr != nil {
|
||||
mlog.Warn("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
||||
continue
|
||||
@@ -148,7 +148,7 @@ func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine
|
||||
return autocomplete, nil
|
||||
}
|
||||
|
||||
// getListOfAllowedChannelsForTeam return the list of allowed channels to search user based on the
|
||||
// getListOfAllowedChannels return the list of allowed channels to search user based on the
|
||||
// next scenarios:
|
||||
// - If there isn't view restrictions (team or channel) and no team id to filter them, then all
|
||||
// channels are allowed (nil return)
|
||||
@@ -159,7 +159,7 @@ func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine
|
||||
// - If we receive channels restrictions we get:
|
||||
// - If we don't have team id, we get those restricted channels (guest accounts and quick search)
|
||||
// - If we have a team id then we only return those restricted channels that belongs to that team
|
||||
func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
|
||||
func (s *SearchUserStore) getListOfAllowedChannels(teamId, channelId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
|
||||
var listOfAllowedChannels []string
|
||||
if viewRestrictions == nil && teamId == "" {
|
||||
// nil return without error means all channels are allowed
|
||||
@@ -174,6 +174,20 @@ func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRes
|
||||
for _, channel := range channels {
|
||||
listOfAllowedChannels = append(listOfAllowedChannels, channel.Id)
|
||||
}
|
||||
|
||||
if channelId != "" {
|
||||
ch, err := s.rootStore.Channel().Get(channelId, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get channel with id: %s", channelId)
|
||||
}
|
||||
// Check if DM/GM channel, and add to the list.
|
||||
// This is because GetTeamChannels does not return DM/GM channels.
|
||||
// And since the channelId is passed from the API layer, it is already
|
||||
// auth checked to confirm that the user has permission.
|
||||
if ch.IsGroupOrDirect() {
|
||||
listOfAllowedChannels = append(listOfAllowedChannels, channelId)
|
||||
}
|
||||
}
|
||||
return listOfAllowedChannels, nil
|
||||
}
|
||||
|
||||
@@ -196,7 +210,7 @@ func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRes
|
||||
func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsAutocompletionEnabled() {
|
||||
listOfAllowedChannels, nErr := s.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
|
||||
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, channelId, options.ViewRestrictions)
|
||||
if nErr != nil {
|
||||
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
||||
continue
|
||||
@@ -205,6 +219,7 @@ func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term str
|
||||
return &model.UserAutocompleteInChannel{}, nil
|
||||
}
|
||||
options.ListOfAllowedChannels = listOfAllowedChannels
|
||||
|
||||
autocomplete, nErr := s.autocompleteUsersInChannelByEngine(engine, teamId, channelId, term, options)
|
||||
if nErr != nil {
|
||||
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
||||
|
||||
@@ -742,6 +742,8 @@ func (us SqlUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*m
|
||||
query = query.Where("u.DeleteAt = 0")
|
||||
}
|
||||
|
||||
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, us.DriverName() == model.DatabaseDriverPostgres)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get_profiles_in_channel_tosql")
|
||||
@@ -1803,7 +1805,16 @@ func (us SqlUserStore) GetUsersBatchForIndexing(startTime int64, startFileID str
|
||||
`).
|
||||
From("ChannelMembers cm").
|
||||
Join("Channels c ON cm.ChannelId = c.Id").
|
||||
Where(sq.Eq{"c.Type": model.ChannelTypeOpen, "cm.UserId": userIds}).
|
||||
Where(sq.And{
|
||||
sq.Eq{
|
||||
"cm.UserId": userIds,
|
||||
},
|
||||
sq.Or{
|
||||
sq.Eq{"c.Type": model.ChannelTypeOpen},
|
||||
sq.Eq{"c.Type": model.ChannelTypeDirect},
|
||||
sq.Eq{"c.Type": model.ChannelTypeGroup},
|
||||
},
|
||||
}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql2")
|
||||
|
||||
@@ -1003,6 +1003,36 @@ func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*model.User{sanitized(u1)}, users)
|
||||
})
|
||||
|
||||
t.Run("Filter by channel members and channel admins", func(t *testing.T) {
|
||||
// save admin for c1
|
||||
user2Admin, err := ss.User().Save(&model.User{
|
||||
Email: MakeEmail(),
|
||||
Username: "bbb" + model.NewId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(user2Admin.Id)) }()
|
||||
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user2Admin.Id}, -1)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
|
||||
ChannelId: c1.Id,
|
||||
UserId: user2Admin.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
ExplicitRoles: "channel_admin",
|
||||
})
|
||||
require.NoError(t, nErr)
|
||||
ss.Channel().UpdateMembersRole(c1.Id, []string{user2Admin.Id})
|
||||
|
||||
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
|
||||
InChannelId: c1.Id,
|
||||
ChannelRoles: []string{model.ChannelAdminRoleId},
|
||||
Page: 0,
|
||||
PerPage: 5,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user2Admin.Id, users[0].Id)
|
||||
})
|
||||
}
|
||||
|
||||
func testUserStoreGetProfilesInChannelByAdmin(t *testing.T, ss store.Store, s SqlStore) {
|
||||
@@ -4882,10 +4912,35 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, ss store.Store) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cDM := &model.Channel{
|
||||
Name: model.NewId() + "__" + model.NewId(),
|
||||
Type: model.ChannelTypeDirect,
|
||||
}
|
||||
cm1 := &model.ChannelMember{
|
||||
UserId: u3.Id,
|
||||
ChannelId: cDM.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
}
|
||||
cm2 := &model.ChannelMember{
|
||||
UserId: u2.Id,
|
||||
ChannelId: cDM.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
}
|
||||
cDM, nErr = ss.Channel().SaveDirectChannel(cDM, cm1, cm2)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
// Getting all users
|
||||
res1List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 100)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res1List, 3)
|
||||
for _, user := range res1List {
|
||||
switch user.Id {
|
||||
case u2.Id:
|
||||
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub1.Id, cPub2.Id, cDM.Id})
|
||||
case u3.Id:
|
||||
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub2.Id, cDM.Id})
|
||||
}
|
||||
}
|
||||
|
||||
// Testing pagination
|
||||
res2List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 1)
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
<mj-include path="./partials/header.mjml" />
|
||||
<mj-raw>{{if .Props.Message}}</mj-raw>
|
||||
<mj-raw>{{range .Props.Posts}}<div class="postCard"></mj-raw>
|
||||
<mj-include path="./partials/card.mjml" />
|
||||
<mj-section css-class="messageCard" padding="0px">
|
||||
<mj-group>
|
||||
<mj-include path="./partials/message_avatar_col.mjml" />
|
||||
<mj-include path="./partials/sender_info_col.mjml" />
|
||||
<mj-include path="./partials/message_button.mjml" />
|
||||
</mj-group>
|
||||
</mj-section>
|
||||
<mj-raw></div>{{end}}</mj-raw>
|
||||
<mj-raw>{{else}}</mj-raw>
|
||||
<mj-section padding="0px 0px 40px 0px">
|
||||
|
||||
@@ -356,7 +356,160 @@
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.messageAttachments * {
|
||||
font-family: Open Sans, sans-serif !important;
|
||||
}
|
||||
|
||||
.messageAttachmentContent {
|
||||
padding: 0px;
|
||||
border: 1px solid rgba(63, 67, 80, 0.16);
|
||||
margin-bottom: 20px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.messageAttachmentContent>table,
|
||||
.attachment__body {
|
||||
font-family: Open Sans, sans-serif;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #3F4350;
|
||||
}
|
||||
|
||||
.messageAttachmentContent .attachment__author-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 5px;
|
||||
border-radius: 50px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.attachment__author-name {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.messageAttachmentContent p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.attachment__title {
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.attachment__image {
|
||||
max-height: 300px;
|
||||
border: 1px solid transparent;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.attachment__thumb-image {
|
||||
max-width: 100%;
|
||||
max-height: 75px;
|
||||
}
|
||||
|
||||
.attachment__thumb-container {
|
||||
max-width: 80px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.attachment__wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.attachment__body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.messageAttachmentContent>table.attachment__footer-container {
|
||||
color: #a3a3a3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attachment__footer-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.attachment__footer-container {
|
||||
color: #a3a3a3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.messageAttachment_title {
|
||||
padding-top: 1em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pretext h1 {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.pretext h2 {
|
||||
font-size: 25px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.pretext h3 {
|
||||
font-size: 22px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.pretext h4 {
|
||||
font-size: 19px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.pretext h5 {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.pretext h6 {
|
||||
font-size: 1em;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.pretext>* {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.messageAttachments {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.messageAttachments h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
background-color: rgba(63, 67, 80, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.messageAttachments a {
|
||||
background-color: unset !important;
|
||||
border: none !important;
|
||||
padding: unset !important;
|
||||
}
|
||||
</style>
|
||||
<!-- this empty mj attribute tag is required by MJML to compile successfully -->
|
||||
</head>
|
||||
|
||||
<body style="word-spacing:normal;">
|
||||
@@ -507,7 +660,96 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td><td style="vertical-align:top;width:552px;" ><![endif]-->
|
||||
<!--[if mso | IE]></td><![endif]-->
|
||||
{{if .MessageAttachments}}
|
||||
<div class="messageAttachments" style="margin-top: 22px;">
|
||||
{{range .MessageAttachments}}
|
||||
<div class="messageAttachment" style="vertical-align: top; font-family: Open Sans, sans-serif;" width="100%">
|
||||
<div class="pretext" style="text-align: left; font-size: 14px; line-height: 20px; color: #3F4350; padding: 0px; font-family: Open Sans, sans-serif;">
|
||||
{{.Pretext}}
|
||||
</div>
|
||||
<div class="messageAttachmentContent" style="border: 1px solid rgba(63, 67, 80, 0.16); margin-bottom: 20px; border-radius: 0 4px 4px 0; border-left: 4px solid {{.Color}}; padding: 12px; font-family: Open Sans, sans-serif;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="text-align: left; line-height: 20px; color: #3F4350; vertical-align: top; font-size: 14px; font-family: Open Sans, sans-serif;" width="100%" valign="top" align="left">
|
||||
<tbody style="font-family: Open Sans, sans-serif;">
|
||||
{{if or .AuthorIcon .AuthorName}}
|
||||
<tr style="font-family: Open Sans, sans-serif;">
|
||||
<td style="font-family: Open Sans, sans-serif;">
|
||||
{{if .AuthorLink}}<a href="{{.AuthorLink}}" style="color: #1C58D9; font-family: Open Sans, sans-serif; text-decoration: none; background-color: unset; border: none; padding: unset;">{{end}}
|
||||
{{if .AuthorIcon}}<img class="attachment__author-icon" alt="attachment author icon" src="{{.AuthorIcon}}" style="width: 14px; height: 14px; margin-right: 5px; border-radius: 50px; vertical-align: middle; font-family: Open Sans, sans-serif;" width="14" height="14">{{end}}
|
||||
{{if .AuthorName}}<span class="attachment__author-name" style="opacity: 0.6; font-family: Open Sans, sans-serif;">{{.AuthorName}}</span>{{end}}
|
||||
{{if .AuthorLink}}</a>{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if .Title}}
|
||||
<tr style="font-family: Open Sans, sans-serif;">
|
||||
<td style="font-family: Open Sans, sans-serif;">
|
||||
<h1 class="attachment__title" style="padding: 0; margin: 5px 0; font-size: 14px; line-height: 18px; font-weight: 500; font-family: Open Sans, sans-serif;">
|
||||
{{if .TitleLink}}<a href="{{.TitleLink}}" style="color: #1C58D9; font-family: Open Sans, sans-serif; text-decoration: none; background-color: unset; border: none; padding: unset;">{{end}}
|
||||
{{.Title}}
|
||||
{{if .Title}}</a>{{end}}
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="attachment__wrapper" style="width: 100%; display: flex; flex-direction: row; gap: 12px; font-family: Open Sans, sans-serif;">
|
||||
<div class="attachment__body" style="text-align: left; font-size: 14px; line-height: 20px; color: #3F4350; flex: 1; font-family: Open Sans, sans-serif;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top; font-size: 14px; color: #3F4350; font-family: Open Sans, sans-serif;" width="100%" valign="top">
|
||||
<tbody style="font-family: Open Sans, sans-serif;">
|
||||
<tr style="font-family: Open Sans, sans-serif;">
|
||||
<td vertical-align="middle" class="messageButton" style="font-family: Open Sans, sans-serif;">
|
||||
<div style="font-family: Open Sans, sans-serif;">{{.Text}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{if .ImageURL}}
|
||||
<tr style="font-family: Open Sans, sans-serif;">
|
||||
<td style="font-family: Open Sans, sans-serif;">
|
||||
<img class="attachment__image" src="{{.ImageURL}}" style="max-height: 300px; border: 1px solid transparent; margin-bottom: 1em; font-family: Open Sans, sans-serif;">
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top; font-size: 14px; table-layout: fixed; margin-bottom: 0.7em; font-family: Open Sans, sans-serif;" width="100%" valign="top">
|
||||
{{range .FieldRows}}
|
||||
<tr class="messageAttachment_row" style="font-family: Open Sans, sans-serif;">
|
||||
{{ $length := len .Cells }}
|
||||
{{range .Cells}}
|
||||
<td class="messageAttachment_field" {{ if eq $length 1 }}colspan="2" {{end}} style="font-family: Open Sans, sans-serif;">
|
||||
<div class="messageAttachment_title" style="padding-top: 1em; font-weight: 600; margin-bottom: 4px; font-family: Open Sans, sans-serif;">{{.Title}}</div>
|
||||
<div style="font-family: Open Sans, sans-serif;">{{.Value}}</div>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
{{if .ThumbURL}}
|
||||
<div class="attachment__thumb-container" style="max-width: 80px; width: max-content; font-family: Open Sans, sans-serif;">
|
||||
<img class="attachment__thumb-image" src="{{.ThumbURL}}" style="max-width: 100%; max-height: 75px; font-family: Open Sans, sans-serif;">
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .Footer}}
|
||||
<table class="attachment__footer-container" border="0" cellpadding="0" cellspacing="0" role="presentation" style="text-align: left; line-height: 20px; color: #a3a3a3; vertical-align: top; font-size: 14px; font-family: Open Sans, sans-serif;" width="100%" valign="top" align="left">
|
||||
<tbody style="font-family: Open Sans, sans-serif;">
|
||||
<tr style="font-family: Open Sans, sans-serif;">
|
||||
<td style="font-family: Open Sans, sans-serif;">
|
||||
{{if .FooterIcon}}<img class="attachment__footer-icon" src="{{.FooterIcon}}" style="width: 16px; height: 16px; vertical-align: middle; font-family: Open Sans, sans-serif;" width="16" height="16">{{end}}
|
||||
<span style="font-size: 12px; font-family: Open Sans, sans-serif;">{{.Footer}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<!--[if mso | IE]><td style="vertical-align:top;width:552px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="./partials/style.mjml" />
|
||||
<mj-include path="partials/message_attachment_styles.mjml" />
|
||||
</mj-head>
|
||||
<mj-body css-class="emailBody">
|
||||
<mj-wrapper mj-class="email">
|
||||
<mj-include path="./partials/logo.mjml" />
|
||||
<mj-include path="./partials/header.mjml" />
|
||||
<mj-raw>{{range .Props.Posts}}<div class="postCard"></mj-raw>
|
||||
<mj-include path="./partials/card.mjml" />
|
||||
<mj-section css-class="messageCard" padding="0px">
|
||||
<mj-group>
|
||||
<mj-include path="./partials/message_avatar_col.mjml" />
|
||||
<mj-include path="./partials/sender_info_col.mjml" />
|
||||
<mj-include path="partials/message_attachment.html" type="html" />
|
||||
<mj-include path="./partials/message_button.mjml" />
|
||||
</mj-group>
|
||||
</mj-section>
|
||||
<mj-raw></div>{{end}}</mj-raw>
|
||||
<mj-section padding="16px 0px 40px 0px">
|
||||
<mj-column>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<mj-section css-class="messageCard" padding="0px">
|
||||
<mj-group>
|
||||
<mj-column css-class="messageAvatarCol">
|
||||
<mj-image css-class="messageAvatar" src="cid:{{.SenderPhoto}}" padding="0px" />
|
||||
</mj-column>
|
||||
<mj-column css-class="senderInfoCol" width=90% >
|
||||
<mj-raw>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="postNameAndTime">
|
||||
<div class="senderName">{{.SenderName}}</div>
|
||||
{{if .Time}}
|
||||
<div class="time">{{.Time}}</div>
|
||||
{{end}}
|
||||
{{if .ChannelName}}
|
||||
<div class="channelBg">
|
||||
{{if .ShowChannelIcon}}
|
||||
<div class="channelLogo"><img src="{{$.Props.SiteURL}}/static/images/channel_icon.png" width=10px height=10px></img></div>
|
||||
{{end}}
|
||||
<div class="channelName">
|
||||
{{if .OtherChannelMembersCount}}
|
||||
<span class="gmChannelCount">{{.OtherChannelMembersCount}}</span>
|
||||
{{end}}
|
||||
{{.ChannelName}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</mj-raw>
|
||||
<mj-text css-class="senderMessage" padding="0px">
|
||||
{{.Message}}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width=100%>
|
||||
<mj-raw>{{if .MessageURL}}</mj-raw>
|
||||
<mj-button href="{{.MessageURL}}" padding="16px 0px 0px 0px" css-class="messageButton">{{$.Props.MessageButton}}</mj-button>
|
||||
<mj-raw>{{end}}</mj-raw>
|
||||
</mj-column>
|
||||
</mj-group>
|
||||
</mj-section>
|
||||
146
templates/partials/message_attachment.css
Обычный файл
146
templates/partials/message_attachment.css
Обычный файл
@@ -0,0 +1,146 @@
|
||||
.messageAttachments * {
|
||||
font-family: Open Sans, sans-serif !important;
|
||||
}
|
||||
|
||||
.messageAttachmentContent {
|
||||
padding: 0px;
|
||||
border: 1px solid rgba(63, 67, 80, 0.16);
|
||||
margin-bottom: 20px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.messageAttachmentContent > table, .attachment__body {
|
||||
font-family: Open Sans, sans-serif;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #3F4350;
|
||||
}
|
||||
|
||||
.messageAttachmentContent .attachment__author-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 5px;
|
||||
border-radius: 50px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.attachment__author-name {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.messageAttachmentContent p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.attachment__title {
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.attachment__image {
|
||||
max-height: 300px;
|
||||
border: 1px solid transparent;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
|
||||
.attachment__thumb-image {
|
||||
max-width: 100%;
|
||||
max-height: 75px;
|
||||
}
|
||||
|
||||
.attachment__thumb-container {
|
||||
max-width: 80px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.attachment__wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.attachment__body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.messageAttachmentContent > table.attachment__footer-container {
|
||||
color: #a3a3a3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attachment__footer-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.attachment__footer-container {
|
||||
color: #a3a3a3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.messageAttachment_title {
|
||||
padding-top: 1em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pretext h1 {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.pretext h2 {
|
||||
font-size: 25px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.pretext h3 {
|
||||
font-size: 22px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.pretext h4 {
|
||||
font-size: 19px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.pretext h5 {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.pretext h6 {
|
||||
font-size: 1em;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.pretext > * {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.messageAttachments {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.messageAttachments h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
background-color: rgba(63, 67, 80, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.messageAttachments a {
|
||||
background-color: unset !important;
|
||||
border: none !important;
|
||||
padding: unset !important;
|
||||
}
|
||||
95
templates/partials/message_attachment.html
Обычный файл
95
templates/partials/message_attachment.html
Обычный файл
@@ -0,0 +1,95 @@
|
||||
{{if .MessageAttachments}}
|
||||
<div class="messageAttachments">
|
||||
{{range .MessageAttachments}}
|
||||
<div class="messageAttachment" style="vertical-align:top;" width="100%">
|
||||
<div class="pretext" style="font-family: Open Sans, sans-serif; text-align: left; font-size: 14px; line-height: 20px; color: #3F4350; padding: 0px;">
|
||||
{{.Pretext}}
|
||||
</div>
|
||||
<div class="messageAttachmentContent" style="border-left: 4px solid {{.Color}}; padding: 12px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px" width="100%">
|
||||
<tbody>
|
||||
{{if or .AuthorIcon .AuthorName}}
|
||||
<tr>
|
||||
<td>
|
||||
{{if .AuthorLink}}<a href="{{.AuthorLink}}">{{end}}
|
||||
{{if .AuthorIcon}}<img class="attachment__author-icon" alt="attachment author icon" src="{{.AuthorIcon}}"/>{{end}}
|
||||
{{if .AuthorName}}<span class="attachment__author-name">{{.AuthorName}}</span>{{end}}
|
||||
{{if .AuthorLink}}</a>{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if .Title}}
|
||||
<tr>
|
||||
<td>
|
||||
<h1 class="attachment__title">
|
||||
{{if .TitleLink}}<a href="{{.TitleLink}}">{{end}}
|
||||
{{.Title}}
|
||||
{{if .Title}}</a>{{end}}
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="attachment__wrapper">
|
||||
<div class="attachment__body">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px; color: #3F4350;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td vertical-align="middle" class="messageButton">
|
||||
<div>{{.Text}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{if .ImageURL}}
|
||||
<tr>
|
||||
<td>
|
||||
<img class="attachment__image" src="{{.ImageURL}}"/>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px; table-layout: fixed; margin-bottom: 0.7em;" width="100%">
|
||||
{{range .FieldRows}}
|
||||
<tr class="messageAttachment_row">
|
||||
{{ $length := len .Cells }}
|
||||
{{range .Cells}}
|
||||
<td
|
||||
class="messageAttachment_field"
|
||||
{{ if eq $length 1 }}colspan="2"{{end}}
|
||||
>
|
||||
<div class="messageAttachment_title">{{.Title}}</div>
|
||||
<div>{{.Value}}</div>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{if .ThumbURL}}
|
||||
<div class="attachment__thumb-container">
|
||||
<img class="attachment__thumb-image" src="{{.ThumbURL}}"/>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Footer}}
|
||||
<table class="attachment__footer-container" border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{if .FooterIcon}}<img class="attachment__footer-icon" src="{{.FooterIcon}}" style="vertical-align: middle;"/>{{end}}
|
||||
<span style="font-size: 12px;">{{.Footer}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
5
templates/partials/message_attachment_styles.mjml
Обычный файл
5
templates/partials/message_attachment_styles.mjml
Обычный файл
@@ -0,0 +1,5 @@
|
||||
<!-- this empty mj attribute tag is required by MJML to compile successfully -->
|
||||
<mj-attributes/>
|
||||
|
||||
<mj-include type="css" path="message_attachment.css"/>
|
||||
<mj-include type="css" css-inline="inline" path="message_attachment.css"/>
|
||||
3
templates/partials/message_avatar_col.mjml
Обычный файл
3
templates/partials/message_avatar_col.mjml
Обычный файл
@@ -0,0 +1,3 @@
|
||||
<mj-column css-class="messageAvatarCol">
|
||||
<mj-image css-class="messageAvatar" src="cid:{{.SenderPhoto}}" padding="0px" />
|
||||
</mj-column>
|
||||
5
templates/partials/message_button.mjml
Обычный файл
5
templates/partials/message_button.mjml
Обычный файл
@@ -0,0 +1,5 @@
|
||||
<mj-column width=100%>
|
||||
<mj-raw>{{if .MessageURL}}</mj-raw>
|
||||
<mj-button href="{{.MessageURL}}" padding="16px 0px 0px 0px" css-class="messageButton">{{$.Props.MessageButton}}</mj-button>
|
||||
<mj-raw>{{end}}</mj-raw>
|
||||
</mj-column>
|
||||
30
templates/partials/sender_info_col.mjml
Обычный файл
30
templates/partials/sender_info_col.mjml
Обычный файл
@@ -0,0 +1,30 @@
|
||||
<mj-column css-class="senderInfoCol" width=90% >
|
||||
<mj-raw>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="postNameAndTime">
|
||||
<div class="senderName">{{.SenderName}}</div>
|
||||
{{if .Time}}
|
||||
<div class="time">{{.Time}}</div>
|
||||
{{end}}
|
||||
{{if .ChannelName}}
|
||||
<div class="channelBg">
|
||||
{{if .ShowChannelIcon}}
|
||||
<div class="channelLogo"><img src="{{$.Props.SiteURL}}/static/images/channel_icon.png" width=10px height=10px></img></div>
|
||||
{{end}}
|
||||
<div class="channelName">
|
||||
{{if .OtherChannelMembersCount}}
|
||||
<span class="gmChannelCount">{{.OtherChannelMembersCount}}</span>
|
||||
{{end}}
|
||||
{{.ChannelName}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</mj-raw>
|
||||
<mj-text css-class="senderMessage" padding="0px">
|
||||
{{.Message}}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
@@ -69,6 +69,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
|
||||
systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", model.MigrationKeyAddPlayboosksManageRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)
|
||||
systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()
|
||||
systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil)
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user