[MM-51854] Onboarding Role selection screen (#23121)

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Julien Tant
2023-05-08 13:57:41 -07:00
коммит произвёл GitHub
родитель 657c0024f9
Коммит d40689466d
101 изменённых файлов: 5022 добавлений и 285 удалений

Просмотреть файл

@@ -11,6 +11,8 @@ import (
"github.com/mattermost/mattermost-server/server/v8/model"
)
const WorkTemplateContextOnboarding = "onboarding"
func (api *API) InitWorkTemplate() {
api.BaseRoutes.WorkTemplates.Handle("/categories", api.APISessionRequired(getWorkTemplateCategories)).Methods("GET")
api.BaseRoutes.WorkTemplates.Handle("/categories/{category}/templates", api.APISessionRequired(getWorkTemplates)).Methods("GET")
@@ -62,7 +64,13 @@ func getWorkTemplates(c *Context, w http.ResponseWriter, r *http.Request) {
}
t := c.AppContext.GetT()
workTemplates, appErr := c.App.GetWorkTemplates(c.Params.Category, c.App.Config().FeatureFlags.ToMap(), t)
context := r.URL.Query().Get("context")
isOnboarding := false
if context == WorkTemplateContextOnboarding {
isOnboarding = true
}
workTemplates, appErr := c.App.GetWorkTemplates(c.Params.Category, c.App.Config().FeatureFlags.ToMap(), isOnboarding, t)
if appErr != nil {
c.Err = appErr
return

Просмотреть файл

@@ -861,7 +861,7 @@ type AppIface interface {
GetWarnMetricsBot() (*model.Bot, *model.AppError)
GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError)
GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError)
GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError)
GetWorkTemplates(category string, featureFlags map[string]string, includeOnboardingTemplates bool, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError)
HTTPService() httpservice.HTTPService
Handle404(w http.ResponseWriter, r *http.Request)
HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError)

Просмотреть файл

@@ -41,11 +41,20 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo
Value: request.Organization,
})
if err != nil {
// don't block onboarding because of that.
a.Log().Error("failed to save organization name", mlog.Err(err))
}
}
if request.Role != "" {
err := a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.SystemFirstAdminRole,
Value: request.Role,
})
if err != nil {
a.Log().Error("failed to save first admin role", mlog.Err(err))
}
}
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
if pluginsEnvironment == nil {
return a.markAdminOnboardingComplete(c)
@@ -54,7 +63,6 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo
pluginContext := pluginContext(c)
for _, pluginID := range request.InstallPlugins {
go func(id string) {
installRequest := &model.InstallMarketplacePluginRequest{
Id: id,

Просмотреть файл

@@ -28,3 +28,21 @@ func TestOnboardingSavesOrganizationName(t *testing.T) {
require.NoError(t, storeErr)
require.Equal(t, "Mattermost In Tests", sys.Value)
}
func TestOnboardingFirstAdminRole(t *testing.T) {
th := Setup(t)
defer th.TearDown()
err := th.App.CompleteOnboarding(&request.Context{}, &mm_model.CompleteOnboardingRequest{
Organization: "myorg",
Role: "engineering",
})
require.Nil(t, err)
defer func() {
th.App.Srv().Store().System().PermanentDeleteByName(mm_model.SystemFirstAdminRole)
}()
sys, storeErr := th.App.Srv().Store().System().GetByName(mm_model.SystemFirstAdminRole)
require.NoError(t, storeErr)
require.Equal(t, "engineering", sys.Value)
}

Просмотреть файл

@@ -11382,7 +11382,7 @@ func (a *OpenTracingAppLayer) GetWorkTemplateCategories(t i18n.TranslateFunc) ([
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map[string]string, includeOnboardingTemplates bool, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplates")
@@ -11394,7 +11394,7 @@ func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWorkTemplates(category, featureFlags, t)
resultVar0, resultVar1 := a.app.GetWorkTemplates(category, featureFlags, includeOnboardingTemplates, t)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))

Просмотреть файл

@@ -11,6 +11,7 @@ import (
"strings"
pbclient "github.com/mattermost/mattermost-server/server/v8/playbooks/client"
"github.com/mattermost/mattermost-server/server/v8/plugin"
fb_model "github.com/mattermost/mattermost-server/server/v8/boards/model"
@@ -250,6 +251,26 @@ func (e *appWorkTemplateExecutor) InstallPlugin(
if err := e.app.EnablePlugin(pluginID); err != nil {
return fmt.Errorf("unable to enable plugin: %w", err)
}
hooks, err := e.app.ch.HooksForPluginOrProduct(pluginID)
if err != nil {
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
return nil
}
event := model.OnInstallEvent{
UserId: c.Session().UserId,
}
if err = hooks.OnInstall(&plugin.Context{
RequestId: c.RequestId(),
SessionId: c.Session().Id,
IPAddress: c.IPAddress(),
AcceptLanguage: c.AcceptLanguage(),
UserAgent: c.UserAgent(),
}, event); err != nil {
mlog.Error("Plugin OnInstall hook failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
}
return nil
}

Просмотреть файл

@@ -29,8 +29,8 @@ func (a *App) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemp
return modelCategories, nil
}
func (a *App) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
templates, err := worktemplates.ListByCategory(category)
func (a *App) GetWorkTemplates(category string, featureFlags map[string]string, includeOnboardingTemplates bool, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
templates, err := worktemplates.ListByCategory(category, includeOnboardingTemplates)
if err != nil {
return nil, model.NewAppError("GetWorkTemplates", "app.worktemplates.get_templates.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}

Просмотреть файл

@@ -90,6 +90,12 @@ func TestGetWorkTemplatesByCategory(t *testing.T) {
},
},
},
{
ID: "test-template-4",
Category: firstCat.ID,
UseCase: "test use case 4",
OnboardingOnly: true,
},
{ // this one should not be returned because of the category
ID: "test-template-4",
Category: "cat-test2",
@@ -97,18 +103,30 @@ func TestGetWorkTemplatesByCategory(t *testing.T) {
},
}
// Act
worktemplates, appErr := th.App.GetWorkTemplates(firstCat.ID, ff, wtTranslationFunc)
t.Run("not onboarding", func(t *testing.T) {
// Act
worktemplates, appErr := th.App.GetWorkTemplates(firstCat.ID, ff, false, wtTranslationFunc)
// Assert
assert.Nil(appErr)
assert.Len(worktemplates, 2)
// assert the correct work templates have been returned
assert.Equal("test-template", worktemplates[0].ID)
assert.Equal("test-template-3", worktemplates[1].ID)
// assert the descriptions have been translated
assert.Equal("Translated test-template-channel-description", worktemplates[0].Description.Channel.Message)
assert.Equal("default message picked for unknown", worktemplates[1].Description.Channel.Message)
// Assert
assert.Nil(appErr)
assert.Len(worktemplates, 2)
// assert the correct work templates have been returned
assert.Equal("test-template", worktemplates[0].ID)
assert.Equal("test-template-3", worktemplates[1].ID)
// assert the descriptions have been translated
assert.Equal("Translated test-template-channel-description", worktemplates[0].Description.Channel.Message)
assert.Equal("default message picked for unknown", worktemplates[1].Description.Channel.Message)
})
t.Run("onboarding", func(t *testing.T) {
// Act
worktemplates, appErr := th.App.GetWorkTemplates(firstCat.ID, ff, true, wtTranslationFunc)
// Assert
assert.Nil(appErr)
assert.Len(worktemplates, 3)
assert.Equal("test-template-4", worktemplates[2].ID)
})
}
// helpers

Просмотреть файл

@@ -2,7 +2,17 @@
name: worktemplate.category.product_teams
- id: devops
name: worktemplate.category.devops
- id: companywide
name: worktemplate.category.companywide
- id: leadership
name: worktemplate.category.leadership
- id: engineering
name: worktemplate.category.engineering
- id: project_management
name: worktemplate.category.project_management
- id: marketing
name: worktemplate.category.marketing
- id: design
name: worktemplate.category.design
- id: qa
name: worktemplate.category.qa
- id: other
name: worktemplate.category.other

Просмотреть файл

@@ -43,6 +43,8 @@ func main() {
log.Fatal(errors.Wrap(err, "failed to read categories.yaml"))
}
illustrations := []string{}
h := md5.New()
cats := []WorkTemplateCategoryWithMD5{} // meow
@@ -53,6 +55,7 @@ func main() {
// validate categories
categoryIds := map[string]struct{}{}
lastCategory := ""
for id := range cats {
cat := cats[id]
@@ -67,12 +70,16 @@ func main() {
if cat.Name == "" {
log.Fatal(errors.New("category name cannot be empty"))
}
lastCategory = cat.ID
categoryIds[cat.ID] = struct{}{}
h.Write([]byte(cat.ID))
cats[id].MD5 = fmt.Sprintf("%x", h.Sum(nil))
h.Reset()
}
if lastCategory != "other" {
log.Fatal(errors.New("category 'other' must exist AND be the last category"))
}
dat, err = getFileContent("templates.yaml")
if err != nil {
@@ -105,6 +112,33 @@ func main() {
MD5: fmt.Sprintf("%x", h.Sum(nil)),
})
h.Reset()
// add illustrations to the list
illustrations = append(illustrations, t.Illustration)
if t.Description.Channel != nil && t.Description.Channel.Illustration != "" {
illustrations = append(illustrations, t.Description.Channel.Illustration)
}
if t.Description.Board != nil && t.Description.Board.Illustration != "" {
illustrations = append(illustrations, t.Description.Board.Illustration)
}
if t.Description.Integration != nil && t.Description.Integration.Illustration != "" {
illustrations = append(illustrations, t.Description.Integration.Illustration)
}
if t.Description.Playbook != nil && t.Description.Playbook.Illustration != "" {
illustrations = append(illustrations, t.Description.Playbook.Illustration)
}
for i := range t.Content {
if t.Content[i].Channel != nil && t.Content[i].Channel.Illustration != "" {
illustrations = append(illustrations, t.Content[i].Channel.Illustration)
}
if t.Content[i].Board != nil && t.Content[i].Board.Illustration != "" {
illustrations = append(illustrations, t.Content[i].Board.Illustration)
}
if t.Content[i].Playbook != nil && t.Content[i].Playbook.Illustration != "" {
illustrations = append(illustrations, t.Content[i].Playbook.Illustration)
}
}
}
code := bytes.NewBuffer(nil)
@@ -146,6 +180,15 @@ func main() {
translationHelper(t.Description.Integration)
translationHelper(t.Description.Playbook)
}
fmt.Println("Missing illustrations:")
for _, illustration := range illustrations {
// check if file exists
illusPath := path.Join("../../../../webapp/channels/src/images", illustration[8:])
if _, err := os.Stat(illusPath); os.IsNotExist(err) {
fmt.Println("\t" + illusPath)
}
}
}
var translationHelperTemplate = `{

Просмотреть файл

@@ -46,6 +46,7 @@ var wt{{.MD5}} = &WorkTemplate{
UseCase: "{{.UseCase}}",
Illustration: "{{.Illustration}}",
Visibility: "{{.Visibility}}",
OnboardingOnly: {{.OnboardingOnly}},
{{if .FeatureFlag}}FeatureFlag: &FeatureFlag{
Name: "{{.FeatureFlag.Name}}",
Value: "{{.FeatureFlag.Value}}",

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -18,14 +18,15 @@ type WorkTemplateCategory struct {
}
type WorkTemplate struct {
ID string `yaml:"id"`
Category string `yaml:"category"`
UseCase string `yaml:"useCase"`
Illustration string `yaml:"illustration"`
Visibility string `yaml:"visibility"`
FeatureFlag *FeatureFlag `yaml:"featureFlag,omitempty"`
Description Description `yaml:"description"`
Content []Content `yaml:"content"`
ID string `yaml:"id"`
Category string `yaml:"category"`
UseCase string `yaml:"useCase"`
Illustration string `yaml:"illustration"`
Visibility string `yaml:"visibility"`
OnboardingOnly bool `yaml:"onboardingOnly"`
FeatureFlag *FeatureFlag `yaml:"featureFlag,omitempty"`
Description Description `yaml:"description"`
Content []Content `yaml:"content"`
}
func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemplate {
@@ -201,17 +202,19 @@ func (wt WorkTemplate) Validate(categoryIds map[string]struct{}) error {
}
}
if hasChannel && wt.Description.Channel == nil {
return errors.New("description.channel is required")
}
if hasBoard && wt.Description.Board == nil {
return errors.New("description.board is required")
}
if hasPlaybook && wt.Description.Playbook == nil {
return errors.New("description.playbook is required")
}
if hasIntegration && wt.Description.Integration == nil {
return errors.New("description.integration is required")
if !wt.OnboardingOnly {
if hasChannel && wt.Description.Channel == nil {
return errors.New("description.channel is required")
}
if hasBoard && wt.Description.Board == nil {
return errors.New("description.board is required")
}
if hasPlaybook && wt.Description.Playbook == nil {
return errors.New("description.playbook is required")
}
if hasIntegration && wt.Description.Integration == nil {
return errors.New("description.integration is required")
}
}
for _, channel := range mustHaveChannels {

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -25,10 +25,14 @@ func ListCategories() ([]*WorkTemplateCategory, error) {
return OrderedWorkTemplateCategories, nil
}
func ListByCategory(category string) ([]*WorkTemplate, error) {
func ListByCategory(category string, includeOnboardingTemplate bool) ([]*WorkTemplate, error) {
wts := []*WorkTemplate{}
for i := range OrderedWorkTemplates {
if OrderedWorkTemplates[i].Category == category {
// do not include work template with onboarding only flag if includeOnboardingTemplate is false
if !includeOnboardingTemplate && OrderedWorkTemplates[i].OnboardingOnly {
continue
}
wts = append(wts, OrderedWorkTemplates[i])
}
}