Skip to content

Commit

Permalink
Workspaces settings/preferences page (#138)
Browse files Browse the repository at this point in the history
* add settings and integrations page

* add tabs

* clean up settings

* add timezone and website fields for workspaces

* fix timezone

* use react-hook-form

* add api to update a workspace

* frontend changes to support logo upload

* allow for logo upload

* add logo if existing

* use logo everywhere

* start working on preferences

* add api for preferences

* connect api

* store and update workspaces communication preferences

* fix tabs

* fix compilation

* add db tests and create preferences on signup

* add workspace updating tests

* add fetching preferences tests

* add more http tests
  • Loading branch information
adelowo authored Jan 29, 2025
1 parent 33b3099 commit 1f75f60
Show file tree
Hide file tree
Showing 52 changed files with 2,536 additions and 67 deletions.
8 changes: 5 additions & 3 deletions cmd/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func addHTTPCommand(c *cobra.Command, cfg *config.Config) {
contactlistRepo := postgres.NewContactListRepository(db)
deckRepo := postgres.NewDeckRepository(db)
shareRepo := postgres.NewShareRepository(db)
preferenceRepo := postgres.NewPreferenceRepository(db)

googleAuthProvider := socialauth.NewGoogle(*cfg)

Expand Down Expand Up @@ -242,9 +243,10 @@ func addHTTPCommand(c *cobra.Command, cfg *config.Config) {
srv, cleanupSrv := server.New(logger,
util.DeRef(cfg), db,
tokenManager, googleAuthProvider,
userRepo, workspaceRepo, planRepo, contactRepo, updateRepo,
contactlistRepo, deckRepo, shareRepo,
mid, gulterHandler, queueHandler, redisCache)
userRepo, workspaceRepo, planRepo, contactRepo,
updateRepo, contactlistRepo, deckRepo, shareRepo,
preferenceRepo, mid, gulterHandler, queueHandler,
redisCache)

go func() {
if err := srv.ListenAndServe(); err != nil {
Expand Down
1 change: 1 addition & 0 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ package malak
//go:generate mockgen -source=deck.go -destination=mocks/deck.go -package=malak_mocks
//go:generate mockgen -source=uuid.go -destination=mocks/uuid.go -package=malak_mocks
//go:generate mockgen -source=share.go -destination=mocks/share.go -package=malak_mocks
//go:generate mockgen -source=preferences.go -destination=mocks/preferences.go -package=malak_mocks
49 changes: 49 additions & 0 deletions image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package malak

import (
"errors"
"net/http"
"net/url"
"strings"
"time"

"github.com/ayinke-llc/hermes"
)

var imageClient = &http.Client{
Timeout: time.Second * 3,
}

func IsImageFromURL(s string) (bool, error) {
if hermes.IsStringEmpty(s) {
return false, errors.New("please provide the url")
}

u, err := url.Parse(s)
if err != nil {
return false, err
}

resp, err := imageClient.Get(u.String())
if err != nil {
return false, err
}

defer resp.Body.Close()

// Read the first 512 bytes of the response body
buffer := make([]byte, 512)
_, err = resp.Body.Read(buffer)
if err != nil {
return false, err
}

// Detect the MIME type using the first 512 bytes
mimeType := http.DetectContentType(buffer)

if strings.HasPrefix(mimeType, "image/") {
return true, nil
}

return false, errors.New("url does not contain an image")
}
52 changes: 52 additions & 0 deletions image_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package malak

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestIsImageFromURL(t *testing.T) {

tt := []struct {
endpoint string
hasError bool
name string
}{
{
name: "no url provided",
hasError: true,
endpoint: "",
},
{
name: "bad url",
hasError: true,
endpoint: "http://localhost:44000",
},
{
name: "google.com",
hasError: true,
endpoint: "https://google.com",
},
{
name: "unsplash",
hasError: false,
endpoint: "https://images.unsplash.com/photo-1737467023078-a694673d7cb3",
},
}

for _, tc := range tt {

t.Run(tc.name, func(t *testing.T) {
r, err := IsImageFromURL(tc.endpoint)

if tc.hasError {
require.Error(t, err)
return
}

require.True(t, r)
require.NoError(t, err)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE workspaces DROP COLUMN website;
ALTER TABLE workspaces DROP COLUMN timezone;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE workspaces ADD COLUMN website TEXT DEFAULT '' NOT NULL;
ALTER TABLE workspaces ADD COLUMN timezone TEXT DEFAULT 'UTC' NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE workspaces DROP COLUMN logo_url;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE workspaces ADD COLUMN logo_url TEXT DEFAULT '' NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE preferences;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE preferences (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
workspace_id uuid NOT NULL REFERENCES workspaces(id),
billing jsonb NOT NULL DEFAULT '{}'::jsonb,
communication jsonb NOT NULL DEFAULT '{}'::jsonb,

created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE
);
40 changes: 40 additions & 0 deletions internal/datastore/postgres/preferences.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package postgres

import (
"context"

"github.com/ayinke-llc/malak"
"github.com/uptrace/bun"
)

type preferenceRepo struct {
inner *bun.DB
}

func NewPreferenceRepository(inner *bun.DB) malak.PreferenceRepository {
return &preferenceRepo{
inner: inner,
}
}

func (w *preferenceRepo) Update(ctx context.Context,
preferences *malak.Preference,
) error {
_, err := w.inner.NewUpdate().
Model(preferences).
Where("id = ?", preferences.ID).
Exec(ctx)
return err
}

func (w *preferenceRepo) Get(ctx context.Context,
workspace *malak.Workspace,
) (*malak.Preference, error) {
preferences := &malak.Preference{}

err := w.inner.NewSelect().
Where("workspace_id = ?", workspace.ID).
Model(preferences).Scan(ctx)

return preferences, err
}
114 changes: 114 additions & 0 deletions internal/datastore/postgres/preferences_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package postgres

import (
"context"
"testing"

"github.com/ayinke-llc/malak"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)

func TestPreferences_Get(t *testing.T) {
// create and fetch
t.Run("create and fetch", func(t *testing.T) {

client, teardownFunc := setupDatabase(t)
defer teardownFunc()

prefRepo := NewPreferenceRepository(client)

repo := NewWorkspaceRepository(client)

userRepo := NewUserRepository(client)

planRepo := NewPlanRepository(client)

// user from the fixtures
user, err := userRepo.Get(context.Background(), &malak.FindUserOptions{
Email: "[email protected]",
})
require.NoError(t, err)

plan, err := planRepo.Get(context.Background(), &malak.FetchPlanOptions{
Reference: "prod_QmtErtydaJZymT",
})
require.NoError(t, err)

opts := &malak.CreateWorkspaceOptions{
User: user,
Workspace: malak.NewWorkspace("oops", user, plan, malak.GenerateReference(malak.EntityTypeWorkspace)),
}

require.NoError(t, repo.Create(context.Background(), opts))

pref, err := prefRepo.Get(context.Background(), opts.Workspace)
require.NoError(t, err)

require.True(t, pref.Communication.EnableMarketing)
require.True(t, pref.Communication.EnableProductUpdates)
})

t.Run("no exists", func(t *testing.T) {

client, teardownFunc := setupDatabase(t)
defer teardownFunc()

prefRepo := NewPreferenceRepository(client)

_, err := prefRepo.Get(context.Background(), &malak.Workspace{
ID: uuid.New(),
})
require.Error(t, err)
})
}

func TestPreferences_Update(t *testing.T) {

client, teardownFunc := setupDatabase(t)
defer teardownFunc()

prefRepo := NewPreferenceRepository(client)

repo := NewWorkspaceRepository(client)

userRepo := NewUserRepository(client)

planRepo := NewPlanRepository(client)

// user from the fixtures
user, err := userRepo.Get(context.Background(), &malak.FindUserOptions{
Email: "[email protected]",
})
require.NoError(t, err)

plan, err := planRepo.Get(context.Background(), &malak.FetchPlanOptions{
Reference: "prod_QmtErtydaJZymT",
})
require.NoError(t, err)

opts := &malak.CreateWorkspaceOptions{
User: user,
Workspace: malak.NewWorkspace("oops", user, plan, malak.GenerateReference(malak.EntityTypeWorkspace)),
}

require.NoError(t, repo.Create(context.Background(), opts))

pref, err := prefRepo.Get(context.Background(), opts.Workspace)
require.NoError(t, err)

require.True(t, pref.Communication.EnableMarketing)
require.True(t, pref.Communication.EnableProductUpdates)

//////////
// Update the pref now

pref.Communication.EnableMarketing = false
require.NoError(t, prefRepo.Update(context.Background(), pref))

newPref, err := prefRepo.Get(context.Background(), opts.Workspace)
require.NoError(t, err)

require.False(t, newPref.Communication.EnableMarketing)
require.True(t, newPref.Communication.EnableProductUpdates)
}
7 changes: 7 additions & 0 deletions internal/datastore/postgres/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ func (o *workspaceRepo) Create(ctx context.Context,
return err
}

_, err = tx.NewInsert().
Model(malak.NewPreference(opts.Workspace)).
Exec(ctx)
if err != nil {
return err
}

if len(opts.User.Roles) == 0 {
opts.User.Metadata.CurrentWorkspace = opts.Workspace.ID
if _, err := tx.NewUpdate().
Expand Down
71 changes: 71 additions & 0 deletions mocks/preferences.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1f75f60

Please sign in to comment.