~netlandish/links

d3d59a18baeb9100b1a5674ab3cc9d890e668077 — Peter Sanchez 4 months ago 4b9a73a
Adding default org and link permissions
M api/graph/generated.go => api/graph/generated.go +80 -3
@@ 301,7 301,8 @@ type ComplexityRoot struct {
	}

	OrganizationSettings struct {
		Billing func(childComplexity int) int
		Billing     func(childComplexity int) int
		DefaultPerm func(childComplexity int) int
	}

	OrganizationStats struct {


@@ 1843,6 1844,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.OrganizationSettings.Billing(childComplexity), true

	case "OrganizationSettings.defaultPerm":
		if e.complexity.OrganizationSettings.DefaultPerm == nil {
			break
		}

		return e.complexity.OrganizationSettings.DefaultPerm(childComplexity), true

	case "OrganizationStats.links":
		if e.complexity.OrganizationStats.Links == nil {
			break


@@ 13563,6 13571,8 @@ func (ec *executionContext) fieldContext_Organization_settings(ctx context.Conte
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "defaultPerm":
				return ec.fieldContext_OrganizationSettings_defaultPerm(ctx, field)
			case "billing":
				return ec.fieldContext_OrganizationSettings_billing(ctx, field)
			}


@@ 13923,6 13933,50 @@ func (ec *executionContext) fieldContext_OrganizationCursor_pageInfo(ctx context
	return fc, nil
}

func (ec *executionContext) _OrganizationSettings_defaultPerm(ctx context.Context, field graphql.CollectedField, obj *models.OrganizationSettings) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_OrganizationSettings_defaultPerm(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.DefaultPerm, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(int)
	fc.Result = res
	return ec.marshalNInt2int(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_OrganizationSettings_defaultPerm(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "OrganizationSettings",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type Int does not have child fields")
		},
	}
	return fc, nil
}

func (ec *executionContext) _OrganizationSettings_billing(ctx context.Context, field graphql.CollectedField, obj *models.OrganizationSettings) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_OrganizationSettings_billing(ctx, field)
	if err != nil {


@@ 22606,7 22660,7 @@ func (ec *executionContext) unmarshalInputOrganizationInput(ctx context.Context,
		asMap[k] = v
	}

	fieldsInOrder := [...]string{"name", "orgUsername", "image"}
	fieldsInOrder := [...]string{"name", "orgUsername", "image", "defaultPerm"}
	for _, k := range fieldsInOrder {
		v, ok := asMap[k]
		if !ok {


@@ 22637,6 22691,14 @@ func (ec *executionContext) unmarshalInputOrganizationInput(ctx context.Context,
			if err != nil {
				return it, err
			}
		case "defaultPerm":
			var err error

			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("defaultPerm"))
			it.DefaultPerm, err = ec.unmarshalNInt2int(ctx, v)
			if err != nil {
				return it, err
			}
		}
	}



@@ 23250,7 23312,7 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co
		asMap[k] = v
	}

	fieldsInOrder := [...]string{"currentSlug", "name", "slug", "image", "isActive", "deleteImg"}
	fieldsInOrder := [...]string{"currentSlug", "name", "slug", "image", "isActive", "deleteImg", "defaultPerm"}
	for _, k := range fieldsInOrder {
		v, ok := asMap[k]
		if !ok {


@@ 23305,6 23367,14 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co
			if err != nil {
				return it, err
			}
		case "defaultPerm":
			var err error

			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("defaultPerm"))
			it.DefaultPerm, err = ec.unmarshalOInt2áš–int(ctx, v)
			if err != nil {
				return it, err
			}
		}
	}



@@ 25167,6 25237,13 @@ func (ec *executionContext) _OrganizationSettings(ctx context.Context, sel ast.S
		switch field.Name {
		case "__typename":
			out.Values[i] = graphql.MarshalString("OrganizationSettings")
		case "defaultPerm":

			out.Values[i] = ec._OrganizationSettings_defaultPerm(ctx, field, obj)

			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "billing":

			out.Values[i] = ec._OrganizationSettings_billing(ctx, field, obj)

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +2 -0
@@ 294,6 294,7 @@ type OrganizationInput struct {
	Name        string          `json:"name"`
	OrgUsername string          `json:"orgUsername"`
	Image       *graphql.Upload `json:"image,omitempty"`
	DefaultPerm int             `json:"defaultPerm"`
}

type OrganizationStats struct {


@@ 425,6 426,7 @@ type UpdateOrganizationInput struct {
	Image       *graphql.Upload `json:"image,omitempty"`
	IsActive    *bool           `json:"isActive,omitempty"`
	DeleteImg   *bool           `json:"deleteImg,omitempty"`
	DefaultPerm *int            `json:"defaultPerm,omitempty"`
}

type UpdateUserInput struct {

M api/graph/schema.graphqls => api/graph/schema.graphqls +3 -1
@@ 97,6 97,7 @@ type BillingSettings {
}

type OrganizationSettings {
    defaultPerm: Int!
    billing: BillingSettings
}



@@ 589,7 590,7 @@ input OrganizationInput {
    name: String!
    orgUsername: String!
    image: Upload

    defaultPerm: Int!
}

input UpdateOrganizationInput {


@@ 599,6 600,7 @@ input UpdateOrganizationInput {
    image: Upload
    isActive: Boolean
    deleteImg: Boolean
    defaultPerm: Int
}

input RegisterInput {

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +16 -0
@@ 82,6 82,10 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, input model.Orga
	validator.Expect(len(input.OrgUsername) < 150, lt.Translate("Org username may not exceed 150 characters")).
		WithField("orgUsername").
		WithCode(valid.ErrValidationCode)
	validator.Expect(input.DefaultPerm >= models.OrgLinkVisibilityPublic &&
		input.DefaultPerm <= models.OrgLinkVisibilityPrivate, lt.Translate("Invalid default permission value")).
		WithField("defaultPerm").
		WithCode(valid.ErrValidationCode)

	if !validator.Ok() {
		return nil, nil


@@ 138,6 142,7 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, input model.Orga
		Name:     input.Name,
		Slug:     slug,
		IsActive: true,
		Settings: models.OrganizationSettings{DefaultPerm: input.DefaultPerm},
	}

	if input.Image != nil {


@@ 1569,6 1574,17 @@ func (r *mutationResolver) UpdateOrganization(ctx context.Context, input *model.
		org.IsActive = *input.IsActive
	}

	if input.DefaultPerm != nil {
		if *input.DefaultPerm < models.OrgLinkVisibilityPublic ||
			*input.DefaultPerm > models.OrgLinkVisibilityPrivate {
			validator.Error(lt.Translate("Invalid default permission value")).
				WithField("defaultPerm").
				WithCode(valid.ErrValidationCode)
			return nil, nil
		}
		org.Settings.DefaultPerm = *input.DefaultPerm
	}

	err = org.Store(ctx)
	if err != nil {
		return nil, err

M core/inputs.go => core/inputs.go +4 -0
@@ 35,6 35,7 @@ func (p *PlaygroundForm) Validate(c echo.Context) error {
type OrganizationForm struct {
	Name        string `form:"name" validate:"required"`
	OrgUsername string `form:"org_username" validate:"required"`
	DefaultPerm int    `form:"default_perm" validate:"number,gte=0"`
}

// Validate ...


@@ 43,6 44,7 @@ func (o *OrganizationForm) Validate(c echo.Context) error {
		FailFast(false).
		String("name", &o.Name).
		String("org_username", &o.OrgUsername).
		Int("default_perm", &o.DefaultPerm).
		BindErrors()
	if errs != nil {
		return validate.GetInputErrors(errs)


@@ 55,6 57,7 @@ type UpdateOrganizationForm struct {
	Slug        string `form:"slug" validate:"required"`
	Enabled     bool   `from:"is_enabled"`
	DeleteImage bool   `form:"delete"`
	DefaultPerm int    `form:"default_perm" validate:"number,gte=0"`
}

// Validate ...


@@ 64,6 67,7 @@ func (uo *UpdateOrganizationForm) Validate(c echo.Context) error {
		String("slug", &uo.Slug).
		Bool("is_enabled", &uo.Enabled).
		Bool("delete", &uo.DeleteImage).
		Int("default_perm", &uo.DefaultPerm).
		BindErrors()
	if errs != nil {
		return validate.GetInputErrors(errs)

M core/routes.go => core/routes.go +26 -3
@@ 728,11 728,18 @@ func (s *Service) OrgCreate(c echo.Context) error {
	pd.Data["image"] = lt.Translate("Image")
	pd.Data["org_username"] = lt.Translate("Org Username")
	pd.Data["save"] = lt.Translate("Save")
	pd.Data["visibility"] = lt.Translate("Default Bookmark Visibility")
	pd.Data["you_have_exceeded"] = lt.Translate(
		"Sorry, you have exceeded the amount of free accounts available. Please update your current free account to create one more")

	form := &OrganizationForm{}
	visibilityOpt := map[int]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}
	gmap := gobwebs.Map{
		"pd":             pd,
		"visibilityOpt":  visibilityOpt,
		"settingSection": true,
		"navFlag":        "settings",
		"navSubFlag":     "orgAdd",


@@ 754,13 761,19 @@ func (s *Service) OrgCreate(c echo.Context) error {
			Org models.Organization `json:"addBusinessOrganization"`
		}
		op := gqlclient.NewOperation(
			`mutation AddOrganization($name: String!, $image: Upload, $username: String!) {
				addOrganization(input: {name: $name, image: $image, orgUsername: $username}) {
			`mutation AddOrganization($name: String!, $image: Upload, $username: String!, $perm: Int!) {
				addOrganization(input: {
				    name: $name,
					image: $image,
					orgUsername: $username,
					defaultPerm: $perm,
				}) {
					id
				}
			}`)
		op.Var("name", form.Name)
		op.Var("username", form.OrgUsername)
		op.Var("perm", form.DefaultPerm)

		image, err := c.FormFile("image")
		if err != nil {


@@ 868,8 881,13 @@ func (s *Service) OrgUpdate(c echo.Context) error {
	pd.Data["save"] = lt.Translate("Save")
	pd.Data["back"] = lt.Translate("Back")
	pd.Data["cancel"] = lt.Translate("Cancel")
	pd.Data["visibility"] = lt.Translate("Default Bookmark Visibility")

	form := &UpdateOrganizationForm{}
	visibilityOpt := map[int]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}

	var isNormal bool
	if org.OrgType == models.OrgTypeNormal {


@@ 877,6 895,7 @@ func (s *Service) OrgUpdate(c echo.Context) error {
	}
	gmap := gobwebs.Map{
		"pd":             pd,
		"visibilityOpt":  visibilityOpt,
		"org":            org,
		"settingSection": true,
		"navFlag":        "settings",


@@ 900,7 919,7 @@ func (s *Service) OrgUpdate(c echo.Context) error {
		}
		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation UpdateOrganization($currentSlug: String!, $name: String!,
			`mutation UpdateOrganization($currentSlug: String!, $name: String!, $perm: Int,
										 $slug: String!, $image: Upload, $deleteImg: Boolean,
										 $isActive: Boolean) {
				updateOrganization(input: {


@@ 910,6 929,7 @@ func (s *Service) OrgUpdate(c echo.Context) error {
					image: $image,
					deleteImg: $deleteImg,
					isActive: $isActive,
					defaultPerm: $perm,
				}) {
					id
				}


@@ 918,6 938,8 @@ func (s *Service) OrgUpdate(c echo.Context) error {
		op.Var("slug", form.Slug)
		op.Var("deleteImg", form.DeleteImage)
		op.Var("currentSlug", org.Slug)
		op.Var("perm", form.DefaultPerm)

		if isNormal {
			op.Var("isActive", form.Enabled)
		}


@@ 955,6 977,7 @@ func (s *Service) OrgUpdate(c echo.Context) error {

	form.Slug = org.Slug
	form.Enabled = org.IsActive
	form.DefaultPerm = org.Settings.DefaultPerm

	gmap["form"] = form
	return links.Render(c, http.StatusOK, "org_edit.html", gmap)

M helpers.go => helpers.go +4 -1
@@ 53,13 53,15 @@ var tzKey = "user.tz"
// and parse them into InputErrors to be displayed in the html form
// if the error code is `ErrNotFoundCode` the handler will return 404
func ParseInputErrors(c echo.Context, graphError *gqlclient.Error, fMap gobwebs.Map) error {
	inputErrs := validate.NewInputErrors()
	if len(graphError.Extensions) == 0 {
		return graphError
	}
	var ext ErrorExtension
	err := json.Unmarshal(graphError.Extensions, &ext)
	if err != nil {
		// Need to address invalid query errors here as they show as
		// json parsing errors when it's valid json. Ie,
		// {"code":"GRAPHQL_VALIDATION_FAILED"}
		return err
	}



@@ 67,6 69,7 @@ func ParseInputErrors(c echo.Context, graphError *gqlclient.Error, fMap gobwebs.
		return echo.NotFoundHandler(c)
	}

	inputErrs := validate.NewInputErrors()
	if ext.Code == valid.ErrValidationGlobalCode {
		inputErrs["_global_"] = []string{graphError.Message}
		return inputErrs

M models/models.go => models/models.go +0 -10
@@ 10,16 10,6 @@ import (
	"netlandish.com/x/gobwebs/accounts"
)

// Billing status, used for org billing setting/metadata
// and plan type
const (
	BillingStatusFree int = iota
	BillingStatusPersonal
	BillingStatusBusiness
	BillingStatusOpenSource
	BillingStatusSponsored
)

// MEMBERINVITATIONCONF is sent when a new member is invited to join an org
// REGISTRATIONINVITECONF is sent when a super user send a invitation for registration


M models/organization.go => models/organization.go +16 -5
@@ 16,18 16,29 @@ import (
	"netlandish.com/x/gobwebs/timezone"
)

type BillingSettings struct {
	Status int `json:"status"`
}

const (
	OrgTypeUser int = iota
	OrgTypeNormal
)

// Billing status, used for org billing setting/metadata
// and plan type
const (
	BillingStatusFree int = iota
	BillingStatusPersonal
	BillingStatusBusiness
	BillingStatusOpenSource
	BillingStatusSponsored
)

type BillingSettings struct {
	Status int `json:"status"` // BillingStatus<X>
}

// OrganizationSettings ...
type OrganizationSettings struct {
	Billing BillingSettings `json:"billing"`
	DefaultPerm int             `json:"default_perm"` // default: OrgLinkVisibilityPublic
	Billing     BillingSettings `json:"billing"`
}

// Value ...

M slack/commands.go => slack/commands.go +37 -13
@@ 221,6 221,7 @@ func linkAdd(c echo.Context, url, teamID, slackUser, text string) (*SlackCommand
		return nil, err
	}

	lt := localizer.GetSessionLocalizer(c)
	opts = &database.FilterOptions{
		Filter: sq.And{
			sq.Eq{"slack_conn_id": slackConn.ID},


@@ 229,7 230,6 @@ func linkAdd(c echo.Context, url, teamID, slackUser, text string) (*SlackCommand
		Limit: 1,
	}

	lt := localizer.GetSessionLocalizer(c)
	slackUsers, err := models.GetSlackUsers(ctx, opts)
	if err != nil {
		return nil, err


@@ 243,7 243,18 @@ func linkAdd(c echo.Context, url, teamID, slackUser, text string) (*SlackCommand
	if err != nil {
		return nil, err
	}
	ctx = auth.Context(ctx, user)

	opts = &database.FilterOptions{
		Filter: sq.Eq{"o.id": slackConn.OrgID},
	}
	orgs, err := models.GetOrganizations(ctx, opts)
	if err != nil {
		return nil, err
	}
	if len(orgs) == 0 {
		return nil, fmt.Errorf("No valid organization found")
	}

	// index 0 is url
	// index 1 is title
	input := strings.Split(text, " ")


@@ 251,29 262,42 @@ func linkAdd(c echo.Context, url, teamID, slackUser, text string) (*SlackCommand
		Link models.OrgLink `json:"addLink"`
	}

	ctx = auth.Context(ctx, user)
	var result GraphQLResponse
	op := gqlclient.NewOperation(
		`mutation AddLink($title: String!, $url: String!, $visibility: Int!, $slug: String!) {
						addLink(input: {
									title: $title,
									url: $url,
									visibility: $visibility,
									orgSlug: $slug}) {
							id
						}
					}`)
		`mutation AddLink($title: String!, $url: String!, $visibility: Int!, 
							  $unread: Boolean!, $starred: Boolean!,
							  $archive: Boolean! $slug: String!) {
					addLink(input: {
								title: $title,
								url: $url,
								visibility: $visibility,
								unread: $unread,
								starred: $starred,
								archive: $archive,
								orgSlug: $slug}) {
						hash
					}
				}`)
	op.Var("title", strings.TrimSpace(input[1]))
	op.Var("url", strings.TrimSpace(input[0]))
	op.Var("slug", slackConn.OrgSlug)
	op.Var("visibility", models.OrgLinkVisibilityPublic)
	op.Var("visibility", orgs[0].Settings.DefaultPerm)
	op.Var("starred", false)
	op.Var("archive", false)
	op.Var("unread", true)
	err = links.Execute(ctx, op, &result)
	if err != nil {
		return nil, err
	}

	lurl := links.GetLinksDomainURL(c)
	lurl.Path = c.Echo().Reverse("core:link_detail", result.Link.Hash)

	block := SlackBlock{}
	block.Type = "section"
	block.Text.Type = "mrkdwn"
	block.Text.Text = lt.Translate("Your link was successfully created")
	block.Text.Text = lt.Translate("Your link was successfully saved. Details here: %s", lurl.String())
	commandResp := &SlackCommandResponse{
		Text:   "Add Link",
		Blocks: []SlackBlock{block},

M templates/org_create.html => templates/org_create.html +11 -0
@@ 30,6 30,17 @@
          {{ end }}
      </div>
      <div>
          <label for="default_perm">{{.pd.Data.visibility}}</label>
          <select name="default_perm">
          {{range $key, $value := .visibilityOpt}}
            <option value="{{$key}}"{{if eq $key $.form.DefaultPerm}}selected{{end}}>{{$value}}</option>
          {{end}}
          </select>
          {{ with .errors.DefaultPerm }}
          <p class="error">{{ . }}</p>
          {{ end }}
      </div>
      <div>
          <label for="image">{{.pd.Data.image}}</label>
          <input type="file" name="image" />
          {{ with .errors.Image }}

M templates/org_edit.html => templates/org_edit.html +11 -0
@@ 30,6 30,17 @@
        </div>
    {{ end }}
    <div>
        <label for="default_perm">{{.pd.Data.visibility}}</label>
        <select name="default_perm">
        {{range $key, $value := .visibilityOpt}}
          <option value="{{$key}}"{{if eq $key $.form.DefaultPerm}}selected{{end}}>{{$value}}</option>
        {{end}}
        </select>
        {{ with .errors.DefaultPerm }}
        <p class="error">{{ . }}</p>
        {{ end }}
    </div>
    <div>
      <label for="image">{{.pd.Data.image}}</label>
      {{ if .org.Image }}
      <img class="mb-2" width="100" heigh="100" src="{{mediaURL .org.Image}}"/>