1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-27 08:31:20 +02:00

Person Property Type (#406)

* Added getWorkspaceUsers API

* Integrated workspace user API in UI

* Integrated workspace user API in UI

* Added toto for implementation slot

* Implemenmted getWorkspaceUSers to get data from Focalboard DB

* Updated store mocks

* Made select styles a shared constant

* Removed unwanted diffs

* Removed unwanted diffs

* Updated snapshots for new property type

* Added user store test

* Added missing copyright notice

* Returning error if no users found to retain original behavior

* Minor fixes and added tests

* Minor fixes

* Used React context for workspace users

* Used useContext hook, and added additional user ID -> user context to avoid that computation by all componnets

* Mergerd both workspace user contextx

* Minor review fix
This commit is contained in:
Harshil Sharma 2021-06-04 18:53:15 +05:30 committed by GitHub
parent b45c1e7fd0
commit 90f6389745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1684 additions and 553 deletions

View File

@ -71,6 +71,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
apiv1.HandleFunc("/workspaces/{workspaceID}", a.sessionRequired(a.handleGetWorkspace)).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/regenerate_signup_token", a.sessionRequired(a.handlePostWorkspaceRegenerateSignupToken)).Methods("POST")
apiv1.HandleFunc("/workspaces/{workspaceID}/users", a.sessionRequired(a.getWorkspaceUsers)).Methods("GET")
// User APIs
apiv1.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET")
@ -1133,6 +1134,59 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) getWorkspaceUsers(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID}/users getWorkspaceUsers
//
// Returns workspace users
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
ctx := r.Context()
session := ctx.Value("session").(*model.Session)
if !a.app().DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) {
a.errorResponse(w, http.StatusForbidden, "Access denied to workspace", errors.New("Access denied to workspace"))
return
}
users, err := a.app().GetWorkspaceUsers(workspaceID)
if err != nil {
a.errorResponse(w, http.StatusInternalServerError, "", err)
return
}
data, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
// Response helpers
func (a *API) errorResponse(w http.ResponseWriter, code int, message string, sourceError error) {

7
server/app/user.go Normal file
View File

@ -0,0 +1,7 @@
package app
import "github.com/mattermost/focalboard/server/model"
func (a *App) GetWorkspaceUsers(workspaceID string) ([]*model.User, error) {
return a.store.GetUsersByWorkspace(workspaceID)
}

View File

@ -27,6 +27,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.7.3 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20210324051608-47abb6519492 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

View File

@ -1183,6 +1183,7 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1381,6 +1382,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

View File

@ -36,133 +36,133 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder {
}
// CleanUpSessions mocks base method.
func (m *MockStore) CleanUpSessions(arg0 int64) error {
func (m *MockStore) CleanUpSessions(expireTime int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CleanUpSessions", arg0)
ret := m.ctrl.Call(m, "CleanUpSessions", expireTime)
ret0, _ := ret[0].(error)
return ret0
}
// CleanUpSessions indicates an expected call of CleanUpSessions.
func (mr *MockStoreMockRecorder) CleanUpSessions(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) CleanUpSessions(expireTime interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpSessions", reflect.TypeOf((*MockStore)(nil).CleanUpSessions), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpSessions", reflect.TypeOf((*MockStore)(nil).CleanUpSessions), expireTime)
}
// CreateSession mocks base method.
func (m *MockStore) CreateSession(arg0 *model.Session) error {
func (m *MockStore) CreateSession(session *model.Session) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSession", arg0)
ret := m.ctrl.Call(m, "CreateSession", session)
ret0, _ := ret[0].(error)
return ret0
}
// CreateSession indicates an expected call of CreateSession.
func (mr *MockStoreMockRecorder) CreateSession(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) CreateSession(session interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockStore)(nil).CreateSession), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockStore)(nil).CreateSession), session)
}
// CreateUser mocks base method.
func (m *MockStore) CreateUser(arg0 *model.User) error {
func (m *MockStore) CreateUser(user *model.User) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUser", arg0)
ret := m.ctrl.Call(m, "CreateUser", user)
ret0, _ := ret[0].(error)
return ret0
}
// CreateUser indicates an expected call of CreateUser.
func (mr *MockStoreMockRecorder) CreateUser(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) CreateUser(user interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), user)
}
// DeleteBlock mocks base method.
func (m *MockStore) DeleteBlock(arg0 store.Container, arg1, arg2 string) error {
func (m *MockStore) DeleteBlock(c store.Container, blockID, modifiedBy string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteBlock", arg0, arg1, arg2)
ret := m.ctrl.Call(m, "DeleteBlock", c, blockID, modifiedBy)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteBlock indicates an expected call of DeleteBlock.
func (mr *MockStoreMockRecorder) DeleteBlock(arg0, arg1, arg2 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) DeleteBlock(c, blockID, modifiedBy interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlock", reflect.TypeOf((*MockStore)(nil).DeleteBlock), arg0, arg1, arg2)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlock", reflect.TypeOf((*MockStore)(nil).DeleteBlock), c, blockID, modifiedBy)
}
// DeleteSession mocks base method.
func (m *MockStore) DeleteSession(arg0 string) error {
func (m *MockStore) DeleteSession(sessionId string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteSession", arg0)
ret := m.ctrl.Call(m, "DeleteSession", sessionId)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteSession indicates an expected call of DeleteSession.
func (mr *MockStoreMockRecorder) DeleteSession(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) DeleteSession(sessionId interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSession", reflect.TypeOf((*MockStore)(nil).DeleteSession), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSession", reflect.TypeOf((*MockStore)(nil).DeleteSession), sessionId)
}
// GetActiveUserCount mocks base method.
func (m *MockStore) GetActiveUserCount(arg0 int64) (int, error) {
func (m *MockStore) GetActiveUserCount(updatedSecondsAgo int64) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetActiveUserCount", arg0)
ret := m.ctrl.Call(m, "GetActiveUserCount", updatedSecondsAgo)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetActiveUserCount indicates an expected call of GetActiveUserCount.
func (mr *MockStoreMockRecorder) GetActiveUserCount(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetActiveUserCount(updatedSecondsAgo interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), updatedSecondsAgo)
}
// GetAllBlocks mocks base method.
func (m *MockStore) GetAllBlocks(arg0 store.Container) ([]model.Block, error) {
func (m *MockStore) GetAllBlocks(c store.Container) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllBlocks", arg0)
ret := m.ctrl.Call(m, "GetAllBlocks", c)
ret0, _ := ret[0].([]model.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllBlocks indicates an expected call of GetAllBlocks.
func (mr *MockStoreMockRecorder) GetAllBlocks(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetAllBlocks(c interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBlocks", reflect.TypeOf((*MockStore)(nil).GetAllBlocks), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBlocks", reflect.TypeOf((*MockStore)(nil).GetAllBlocks), c)
}
// GetBlocksWithParent mocks base method.
func (m *MockStore) GetBlocksWithParent(arg0 store.Container, arg1 string) ([]model.Block, error) {
func (m *MockStore) GetBlocksWithParent(c store.Container, parentID string) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksWithParent", arg0, arg1)
ret := m.ctrl.Call(m, "GetBlocksWithParent", c, parentID)
ret0, _ := ret[0].([]model.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksWithParent indicates an expected call of GetBlocksWithParent.
func (mr *MockStoreMockRecorder) GetBlocksWithParent(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetBlocksWithParent(c, parentID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParent", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParent), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParent", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParent), c, parentID)
}
// GetBlocksWithParentAndType mocks base method.
func (m *MockStore) GetBlocksWithParentAndType(arg0 store.Container, arg1, arg2 string) ([]model.Block, error) {
func (m *MockStore) GetBlocksWithParentAndType(c store.Container, parentID, blockType string) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksWithParentAndType", arg0, arg1, arg2)
ret := m.ctrl.Call(m, "GetBlocksWithParentAndType", c, parentID, blockType)
ret0, _ := ret[0].([]model.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksWithParentAndType indicates an expected call of GetBlocksWithParentAndType.
func (mr *MockStoreMockRecorder) GetBlocksWithParentAndType(arg0, arg1, arg2 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetBlocksWithParentAndType(c, parentID, blockType interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParentAndType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParentAndType), arg0, arg1, arg2)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParentAndType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParentAndType), c, parentID, blockType)
}
// GetBlocksWithRootID mocks base method.
@ -181,33 +181,33 @@ func (mr *MockStoreMockRecorder) GetBlocksWithRootID(arg0, arg1 interface{}) *go
}
// GetBlocksWithType mocks base method.
func (m *MockStore) GetBlocksWithType(arg0 store.Container, arg1 string) ([]model.Block, error) {
func (m *MockStore) GetBlocksWithType(c store.Container, blockType string) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksWithType", arg0, arg1)
ret := m.ctrl.Call(m, "GetBlocksWithType", c, blockType)
ret0, _ := ret[0].([]model.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksWithType indicates an expected call of GetBlocksWithType.
func (mr *MockStoreMockRecorder) GetBlocksWithType(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetBlocksWithType(c, blockType interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithType), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithType), c, blockType)
}
// GetParentID mocks base method.
func (m *MockStore) GetParentID(arg0 store.Container, arg1 string) (string, error) {
func (m *MockStore) GetParentID(c store.Container, blockID string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetParentID", arg0, arg1)
ret := m.ctrl.Call(m, "GetParentID", c, blockID)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetParentID indicates an expected call of GetParentID.
func (mr *MockStoreMockRecorder) GetParentID(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetParentID(c, blockID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParentID", reflect.TypeOf((*MockStore)(nil).GetParentID), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParentID", reflect.TypeOf((*MockStore)(nil).GetParentID), c, blockID)
}
// GetRegisteredUserCount mocks base method.
@ -226,78 +226,78 @@ func (mr *MockStoreMockRecorder) GetRegisteredUserCount() *gomock.Call {
}
// GetRootID mocks base method.
func (m *MockStore) GetRootID(arg0 store.Container, arg1 string) (string, error) {
func (m *MockStore) GetRootID(c store.Container, blockID string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRootID", arg0, arg1)
ret := m.ctrl.Call(m, "GetRootID", c, blockID)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetRootID indicates an expected call of GetRootID.
func (mr *MockStoreMockRecorder) GetRootID(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetRootID(c, blockID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRootID", reflect.TypeOf((*MockStore)(nil).GetRootID), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRootID", reflect.TypeOf((*MockStore)(nil).GetRootID), c, blockID)
}
// GetSession mocks base method.
func (m *MockStore) GetSession(arg0 string, arg1 int64) (*model.Session, error) {
func (m *MockStore) GetSession(token string, expireTime int64) (*model.Session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSession", arg0, arg1)
ret := m.ctrl.Call(m, "GetSession", token, expireTime)
ret0, _ := ret[0].(*model.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSession indicates an expected call of GetSession.
func (mr *MockStoreMockRecorder) GetSession(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetSession(token, expireTime interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockStore)(nil).GetSession), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockStore)(nil).GetSession), token, expireTime)
}
// GetSharing mocks base method.
func (m *MockStore) GetSharing(arg0 store.Container, arg1 string) (*model.Sharing, error) {
func (m *MockStore) GetSharing(c store.Container, rootID string) (*model.Sharing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSharing", arg0, arg1)
ret := m.ctrl.Call(m, "GetSharing", c, rootID)
ret0, _ := ret[0].(*model.Sharing)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSharing indicates an expected call of GetSharing.
func (mr *MockStoreMockRecorder) GetSharing(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetSharing(c, rootID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSharing", reflect.TypeOf((*MockStore)(nil).GetSharing), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSharing", reflect.TypeOf((*MockStore)(nil).GetSharing), c, rootID)
}
// GetSubTree2 mocks base method.
func (m *MockStore) GetSubTree2(arg0 store.Container, arg1 string) ([]model.Block, error) {
func (m *MockStore) GetSubTree2(c store.Container, blockID string) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSubTree2", arg0, arg1)
ret := m.ctrl.Call(m, "GetSubTree2", c, blockID)
ret0, _ := ret[0].([]model.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSubTree2 indicates an expected call of GetSubTree2.
func (mr *MockStoreMockRecorder) GetSubTree2(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetSubTree2(c, blockID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubTree2", reflect.TypeOf((*MockStore)(nil).GetSubTree2), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubTree2", reflect.TypeOf((*MockStore)(nil).GetSubTree2), c, blockID)
}
// GetSubTree3 mocks base method.
func (m *MockStore) GetSubTree3(arg0 store.Container, arg1 string) ([]model.Block, error) {
func (m *MockStore) GetSubTree3(c store.Container, blockID string) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSubTree3", arg0, arg1)
ret := m.ctrl.Call(m, "GetSubTree3", c, blockID)
ret0, _ := ret[0].([]model.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSubTree3 indicates an expected call of GetSubTree3.
func (mr *MockStoreMockRecorder) GetSubTree3(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetSubTree3(c, blockID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubTree3", reflect.TypeOf((*MockStore)(nil).GetSubTree3), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubTree3", reflect.TypeOf((*MockStore)(nil).GetSubTree3), c, blockID)
}
// GetSystemSettings mocks base method.
@ -316,63 +316,78 @@ func (mr *MockStoreMockRecorder) GetSystemSettings() *gomock.Call {
}
// GetUserByEmail mocks base method.
func (m *MockStore) GetUserByEmail(arg0 string) (*model.User, error) {
func (m *MockStore) GetUserByEmail(email string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByEmail", arg0)
ret := m.ctrl.Call(m, "GetUserByEmail", email)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByEmail indicates an expected call of GetUserByEmail.
func (mr *MockStoreMockRecorder) GetUserByEmail(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetUserByEmail(email interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockStore)(nil).GetUserByEmail), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockStore)(nil).GetUserByEmail), email)
}
// GetUserById mocks base method.
func (m *MockStore) GetUserById(arg0 string) (*model.User, error) {
func (m *MockStore) GetUserById(userID string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserById", arg0)
ret := m.ctrl.Call(m, "GetUserById", userID)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserById indicates an expected call of GetUserById.
func (mr *MockStoreMockRecorder) GetUserById(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetUserById(userID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserById", reflect.TypeOf((*MockStore)(nil).GetUserById), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserById", reflect.TypeOf((*MockStore)(nil).GetUserById), userID)
}
// GetUserByUsername mocks base method.
func (m *MockStore) GetUserByUsername(arg0 string) (*model.User, error) {
func (m *MockStore) GetUserByUsername(username string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByUsername", arg0)
ret := m.ctrl.Call(m, "GetUserByUsername", username)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByUsername indicates an expected call of GetUserByUsername.
func (mr *MockStoreMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetUserByUsername(username interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockStore)(nil).GetUserByUsername), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockStore)(nil).GetUserByUsername), username)
}
// GetUsersByWorkspace mocks base method.
func (m *MockStore) GetUsersByWorkspace(workspaceID string) ([]*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUsersByWorkspace", workspaceID)
ret0, _ := ret[0].([]*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUsersByWorkspace indicates an expected call of GetUsersByWorkspace.
func (mr *MockStoreMockRecorder) GetUsersByWorkspace(workspaceID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByWorkspace", reflect.TypeOf((*MockStore)(nil).GetUsersByWorkspace), workspaceID)
}
// GetWorkspace mocks base method.
func (m *MockStore) GetWorkspace(arg0 string) (*model.Workspace, error) {
func (m *MockStore) GetWorkspace(ID string) (*model.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspace", arg0)
ret := m.ctrl.Call(m, "GetWorkspace", ID)
ret0, _ := ret[0].(*model.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspace indicates an expected call of GetWorkspace.
func (mr *MockStoreMockRecorder) GetWorkspace(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetWorkspace(ID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspace", reflect.TypeOf((*MockStore)(nil).GetWorkspace), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspace", reflect.TypeOf((*MockStore)(nil).GetWorkspace), ID)
}
// HasWorkspaceAccess mocks base method.
@ -391,45 +406,45 @@ func (mr *MockStoreMockRecorder) HasWorkspaceAccess(arg0, arg1 interface{}) *gom
}
// InsertBlock mocks base method.
func (m *MockStore) InsertBlock(arg0 store.Container, arg1 model.Block) error {
func (m *MockStore) InsertBlock(c store.Container, block model.Block) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertBlock", arg0, arg1)
ret := m.ctrl.Call(m, "InsertBlock", c, block)
ret0, _ := ret[0].(error)
return ret0
}
// InsertBlock indicates an expected call of InsertBlock.
func (mr *MockStoreMockRecorder) InsertBlock(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) InsertBlock(c, block interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlock", reflect.TypeOf((*MockStore)(nil).InsertBlock), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlock", reflect.TypeOf((*MockStore)(nil).InsertBlock), c, block)
}
// RefreshSession mocks base method.
func (m *MockStore) RefreshSession(arg0 *model.Session) error {
func (m *MockStore) RefreshSession(session *model.Session) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RefreshSession", arg0)
ret := m.ctrl.Call(m, "RefreshSession", session)
ret0, _ := ret[0].(error)
return ret0
}
// RefreshSession indicates an expected call of RefreshSession.
func (mr *MockStoreMockRecorder) RefreshSession(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) RefreshSession(session interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshSession", reflect.TypeOf((*MockStore)(nil).RefreshSession), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshSession", reflect.TypeOf((*MockStore)(nil).RefreshSession), session)
}
// SetSystemSetting mocks base method.
func (m *MockStore) SetSystemSetting(arg0, arg1 string) error {
func (m *MockStore) SetSystemSetting(key, value string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetSystemSetting", arg0, arg1)
ret := m.ctrl.Call(m, "SetSystemSetting", key, value)
ret0, _ := ret[0].(error)
return ret0
}
// SetSystemSetting indicates an expected call of SetSystemSetting.
func (mr *MockStoreMockRecorder) SetSystemSetting(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) SetSystemSetting(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSystemSetting", reflect.TypeOf((*MockStore)(nil).SetSystemSetting), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSystemSetting", reflect.TypeOf((*MockStore)(nil).SetSystemSetting), key, value)
}
// Shutdown mocks base method.
@ -447,99 +462,99 @@ func (mr *MockStoreMockRecorder) Shutdown() *gomock.Call {
}
// UpdateSession mocks base method.
func (m *MockStore) UpdateSession(arg0 *model.Session) error {
func (m *MockStore) UpdateSession(session *model.Session) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateSession", arg0)
ret := m.ctrl.Call(m, "UpdateSession", session)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateSession indicates an expected call of UpdateSession.
func (mr *MockStoreMockRecorder) UpdateSession(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) UpdateSession(session interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSession", reflect.TypeOf((*MockStore)(nil).UpdateSession), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSession", reflect.TypeOf((*MockStore)(nil).UpdateSession), session)
}
// UpdateUser mocks base method.
func (m *MockStore) UpdateUser(arg0 *model.User) error {
func (m *MockStore) UpdateUser(user *model.User) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", arg0)
ret := m.ctrl.Call(m, "UpdateUser", user)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockStoreMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) UpdateUser(user interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), user)
}
// UpdateUserPassword mocks base method.
func (m *MockStore) UpdateUserPassword(arg0, arg1 string) error {
func (m *MockStore) UpdateUserPassword(username, password string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUserPassword", arg0, arg1)
ret := m.ctrl.Call(m, "UpdateUserPassword", username, password)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateUserPassword indicates an expected call of UpdateUserPassword.
func (mr *MockStoreMockRecorder) UpdateUserPassword(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) UpdateUserPassword(username, password interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPassword", reflect.TypeOf((*MockStore)(nil).UpdateUserPassword), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPassword", reflect.TypeOf((*MockStore)(nil).UpdateUserPassword), username, password)
}
// UpdateUserPasswordByID mocks base method.
func (m *MockStore) UpdateUserPasswordByID(arg0, arg1 string) error {
func (m *MockStore) UpdateUserPasswordByID(userID, password string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUserPasswordByID", arg0, arg1)
ret := m.ctrl.Call(m, "UpdateUserPasswordByID", userID, password)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateUserPasswordByID indicates an expected call of UpdateUserPasswordByID.
func (mr *MockStoreMockRecorder) UpdateUserPasswordByID(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) UpdateUserPasswordByID(userID, password interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPasswordByID", reflect.TypeOf((*MockStore)(nil).UpdateUserPasswordByID), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPasswordByID", reflect.TypeOf((*MockStore)(nil).UpdateUserPasswordByID), userID, password)
}
// UpsertSharing mocks base method.
func (m *MockStore) UpsertSharing(arg0 store.Container, arg1 model.Sharing) error {
func (m *MockStore) UpsertSharing(c store.Container, sharing model.Sharing) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertSharing", arg0, arg1)
ret := m.ctrl.Call(m, "UpsertSharing", c, sharing)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertSharing indicates an expected call of UpsertSharing.
func (mr *MockStoreMockRecorder) UpsertSharing(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) UpsertSharing(c, sharing interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertSharing", reflect.TypeOf((*MockStore)(nil).UpsertSharing), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertSharing", reflect.TypeOf((*MockStore)(nil).UpsertSharing), c, sharing)
}
// UpsertWorkspaceSettings mocks base method.
func (m *MockStore) UpsertWorkspaceSettings(arg0 model.Workspace) error {
func (m *MockStore) UpsertWorkspaceSettings(workspace model.Workspace) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertWorkspaceSettings", arg0)
ret := m.ctrl.Call(m, "UpsertWorkspaceSettings", workspace)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertWorkspaceSettings indicates an expected call of UpsertWorkspaceSettings.
func (mr *MockStoreMockRecorder) UpsertWorkspaceSettings(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) UpsertWorkspaceSettings(workspace interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSettings", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSettings), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSettings", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSettings), workspace)
}
// UpsertWorkspaceSignupToken mocks base method.
func (m *MockStore) UpsertWorkspaceSignupToken(arg0 model.Workspace) error {
func (m *MockStore) UpsertWorkspaceSignupToken(workspace model.Workspace) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertWorkspaceSignupToken", arg0)
ret := m.ctrl.Call(m, "UpsertWorkspaceSignupToken", workspace)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertWorkspaceSignupToken indicates an expected call of UpsertWorkspaceSignupToken.
func (mr *MockStoreMockRecorder) UpsertWorkspaceSignupToken(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) UpsertWorkspaceSignupToken(workspace interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSignupToken", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSignupToken), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSignupToken", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSignupToken), workspace)
}

View File

@ -39,4 +39,5 @@ func TestBlocksStore(t *testing.T) {
t.Run("BlocksStore", func(t *testing.T) { storetests.StoreTestBlocksStore(t, SetupTests) })
t.Run("SharingStore", func(t *testing.T) { storetests.StoreTestSharingStore(t, SetupTests) })
t.Run("SystemStore", func(t *testing.T) { storetests.StoreTestSystemStore(t, SetupTests) })
t.Run("UserStore", func(t *testing.T) { storetests.StoreTestUserStore(t, SetupTests) })
}

View File

@ -1,8 +1,10 @@
package sqlstore
import (
"database/sql"
"encoding/json"
"errors"
"log"
"time"
"github.com/mattermost/focalboard/server/model"
@ -27,26 +29,52 @@ func (s *SQLStore) GetRegisteredUserCount() (int, error) {
}
func (s *SQLStore) getUserByCondition(condition sq.Eq) (*model.User, error) {
users, err := s.getUsersByCondition(condition)
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, nil
}
return users[0], nil
}
func (s *SQLStore) getUsersByCondition(condition sq.Eq) ([]*model.User, error) {
query := s.getQueryBuilder().
Select("id", "username", "email", "password", "mfa_secret", "auth_service", "auth_data", "props", "create_at", "update_at", "delete_at").
Select(
"id",
"username",
"email",
"password",
"mfa_secret",
"auth_service",
"auth_data",
"props",
"create_at",
"update_at",
"delete_at",
).
From(s.tablePrefix + "users").
Where(sq.Eq{"delete_at": 0}).
Where(condition)
row := query.QueryRow()
user := model.User{}
rows, err := query.Query()
if err != nil {
log.Printf("getUsersByCondition ERROR: %v", err)
return nil, err
}
var propsBytes []byte
err := row.Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.MfaSecret, &user.AuthService, &user.AuthData, &propsBytes, &user.CreateAt, &user.UpdateAt, &user.DeleteAt)
users, err := s.usersFromRows(rows)
if err != nil {
return nil, err
}
err = json.Unmarshal(propsBytes, &user.Props)
if err != nil {
return nil, err
if len(users) == 0 {
return nil, sql.ErrNoRows
}
return &user, nil
return users, nil
}
func (s *SQLStore) GetUserById(userID string) (*model.User, error) {
@ -158,3 +186,44 @@ func (s *SQLStore) UpdateUserPasswordByID(userID, password string) error {
return nil
}
func (s *SQLStore) GetUsersByWorkspace(workspaceID string) ([]*model.User, error) {
return s.getUsersByCondition(nil)
}
func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
defer rows.Close()
users := []*model.User{}
for rows.Next() {
var user model.User
var propsBytes []byte
err := rows.Scan(
&user.ID,
&user.Username,
&user.Email,
&user.Password,
&user.MfaSecret,
&user.AuthService,
&user.AuthData,
&propsBytes,
&user.CreateAt,
&user.UpdateAt,
&user.DeleteAt,
)
if err != nil {
return nil, err
}
err = json.Unmarshal(propsBytes, &user.Props)
if err != nil {
return nil, err
}
users = append(users, &user)
}
return users, nil
}

View File

@ -36,6 +36,7 @@ type Store interface {
UpdateUser(user *model.User) error
UpdateUserPassword(username, password string) error
UpdateUserPasswordByID(userID, password string) error
GetUsersByWorkspace(workspaceID string) ([]*model.User, error)
GetActiveUserCount(updatedSecondsAgo int64) (int, error)
GetSession(token string, expireTime int64) (*model.Session, error)

View File

@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetests
import (
"database/sql"
"time"
"github.com/google/uuid"
"github.com/mattermost/focalboard/server/model"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/focalboard/server/services/store"
)
func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
t.Run("SetGetSystemSettings", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetWorkspaceUsers(t, store)
})
}
func testGetWorkspaceUsers(t *testing.T, store store.Store) {
t.Run("GetWorkspaceUSers", func(t *testing.T) {
users, err := store.GetUsersByWorkspace("workspace_1")
require.Equal(t, 0, len(users))
require.Equal(t, sql.ErrNoRows, err)
userID := uuid.New().String()
err = store.CreateUser(&model.User{
ID: userID,
Username: "darth.vader",
})
require.Nil(t, err)
defer store.UpdateUser(&model.User{
ID: userID,
DeleteAt: time.Now().Unix(),
})
users, err = store.GetUsersByWorkspace("workspace_1")
require.Equal(t, 1, len(users))
require.Equal(t, "darth.vader", users[0].Username)
require.Nil(t, err)
})
}

View File

@ -1 +1 @@
5.0.1
5.1.1

File diff suppressed because one or more lines are too long

View File

@ -266,6 +266,10 @@ definitions:
description: Token required to register new users
type: string
x-go-name: SignupToken
title:
description: Title of the workspace
type: string
x-go-name: Title
updateAt:
description: Updated time
format: int64
@ -724,6 +728,31 @@ paths:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/api/v1/workspaces/{workspaceID}/users:
get:
description: Returns workspace users
operationId: getWorkspaceUsers
parameters:
- description: Workspace ID
in: path
name: workspaceID
required: true
type: string
produces:
- application/json
responses:
"200":
description: success
schema:
items:
$ref: '#/definitions/User'
type: array
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/workspaces/{workspaceID}/{rootID}/{fileID}:
get:
description: Returns the contents of an uploaded file
@ -764,7 +793,8 @@ schemes:
- https
securityDefinitions:
BearerAuth:
description: 'Pass session token using Bearer authentication, e.g. set header "Authorization: Bearer <session token>"'
description: 'Pass session token using Bearer authentication, e.g. set header
"Authorization: Bearer <session token>"'
in: header
name: Authorization
type: apiKey

View File

@ -36,6 +36,7 @@
"@formatjs/ts-transformer": "^3.2.1",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.9",
"@types/emoji-mart": "^3.0.4",
"@types/jest": "^26.0.21",
"@types/marked": "^2.0.0",
@ -653,9 +654,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
"integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
@ -1540,15 +1541,6 @@
"node": ">=10"
}
},
"node_modules/@testing-library/dom/node_modules/@babel/runtime": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
"integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "5.11.10",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.10.tgz",
@ -1596,13 +1588,20 @@
"node": ">=10"
}
},
"node_modules/@testing-library/react/node_modules/@babel/runtime": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
"integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
"node_modules/@testing-library/user-event": {
"version": "13.1.9",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.1.9.tgz",
"integrity": "sha512-NZr0zL2TMOs2qk+dNlqrAdbaRW5dAmYwd1yuQ4r7HpkVEOj0MWuUjDWwKhcLd/atdBy8ZSMHSKp+kXSQe47ezg==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
"@babel/runtime": "^7.12.5"
},
"engines": {
"node": ">=10",
"npm": ">=6"
},
"peerDependencies": {
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@types/aria-query": {
@ -12879,14 +12878,6 @@
"react-transition-group": "^4.3.0"
}
},
"node_modules/react-select/node_modules/@babel/runtime": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
"integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
}
},
"node_modules/react-simplemde-editor": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-4.1.3.tgz",
@ -16879,9 +16870,9 @@
}
},
"@babel/runtime": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
"integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
@ -17685,17 +17676,6 @@
"dom-accessibility-api": "^0.5.4",
"lz-string": "^1.4.4",
"pretty-format": "^26.6.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
"integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"@testing-library/jest-dom": {
@ -17734,17 +17714,15 @@
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^7.28.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
"integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"@testing-library/user-event": {
"version": "13.1.9",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.1.9.tgz",
"integrity": "sha512-NZr0zL2TMOs2qk+dNlqrAdbaRW5dAmYwd1yuQ4r7HpkVEOj0MWuUjDWwKhcLd/atdBy8ZSMHSKp+kXSQe47ezg==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5"
}
},
"@types/aria-query": {
@ -27014,16 +26992,6 @@
"prop-types": "^15.6.0",
"react-input-autosize": "^3.0.0",
"react-transition-group": "^4.3.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
"integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"react-simplemde-editor": {

View File

@ -20,7 +20,8 @@
"cypress:run:firefox": "cypress run --browser firefox",
"cypress:run:edge": "cypress run --browser edge",
"cypress:run:electron": "cypress run --browser electron",
"cypress:open": "cypress open"
"cypress:open": "cypress open",
"updatesnapshots": "jest --updateSnapshot"
},
"dependencies": {
"cypress": "^6.8.0",
@ -70,6 +71,7 @@
"@formatjs/ts-transformer": "^3.2.1",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.9",
"@types/emoji-mart": "^3.0.4",
"@types/jest": "^26.0.21",
"@types/marked": "^2.0.0",

View File

@ -27,7 +27,6 @@ const Comment: FC<Props> = (props: Props) => {
const html = Utils.htmlFromMarkdown(comment.title)
const [username, setUsername] = useState('')
useEffect(() => {
UserCache.shared.getUser(userId).then((user) => {
if (user) {

View File

@ -0,0 +1,185 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/properties/user not readonly 1`] = `
<div>
<div
class="UserProperty css-2b097c-container"
>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-pwgt1w-Control"
>
<div
class=" css-kpfmlq-ValueContainer"
>
<div
class=" css-1wa3eu0-placeholder"
>
Select...
</div>
<div
class="css-1shkodo-Input"
>
<div
class=""
style="display: inline-block;"
>
<input
aria-autocomplete="list"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
id="react-select-2-input"
spellcheck="false"
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
<div
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
/>
</div>
</div>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
>
<span
class=" css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class=" css-19sxey8-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/properties/user readonly view 1`] = `
<div>
<div
class="UserProperty octo-propertyvalue"
>
user-id-1
</div>
</div>
`;
exports[`components/properties/user user dropdown open 1`] = `
<div>
<div
class="UserProperty css-2b097c-container"
>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
>
<span
id="aria-selection"
/>
<span
id="aria-context"
>
Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
</span>
</span>
<div
class=" css-pwgt1w-Control"
>
<div
class=" css-kpfmlq-ValueContainer"
>
<div
class=" css-1wa3eu0-placeholder"
>
Select...
</div>
<div
class="css-1shkodo-Input"
>
<div
class=""
style="display: inline-block;"
>
<input
aria-autocomplete="list"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
id="react-select-3-input"
spellcheck="false"
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
<div
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
/>
</div>
</div>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
>
<span
class=" css-43ykx9-indicatorSeparator"
/>
<div
aria-hidden="true"
class=" css-hl9mox-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
<div
class=" css-7p3rb2-menu"
>
<div
class=" css-1m3ubgr-MenuList"
>
<div
class=" css-gg45go-NoOptionsMessage"
>
No options
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,4 @@
.UserProperty {
margin-right: 20px;
min-width: 120px;
}

View File

@ -0,0 +1,116 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {IntlProvider} from 'react-intl'
import {render, wait} from '@testing-library/react'
import {act} from 'react-dom/test-utils'
import userEvent from '@testing-library/user-event'
import UserProperty from './user'
const wrapIntl = (children: any) => <IntlProvider locale='en'>{children}</IntlProvider>
const fetchMock = require('fetch-mock-jest')
describe('components/properties/user', () => {
beforeAll(() => {
fetchMock.get('http://localhost/api/v1/workspaces/0/users', JSON.stringify([
{
id: 'user-id-1',
username: 'username-1',
email: 'user-1@example.com',
props: {},
create_at: 1621315184,
update_at: 1621315184,
delete_at: 0,
},
{
id: 'user-id-2',
username: 'username-2',
email: 'user-2@example.com',
props: {},
create_at: 1621315184,
update_at: 1621315184,
delete_at: 0,
},
]),
)
})
afterAll(() => {
fetchMock.mockClear()
})
test('not readonly', async () => {
const component = wrapIntl(
<UserProperty
value={'user-id-1'}
readonly={false}
onChange={() => {
}}
/>,
)
let container
act(() => {
const renderResult = render(component)
container = renderResult.container
})
await wait()
expect(container).toMatchSnapshot()
})
test('readonly view', async () => {
const component = wrapIntl(
<UserProperty
value={'user-id-1'}
readonly={true}
onChange={() => {
}}
/>,
)
let container
act(() => {
const renderResult = render(component)
container = renderResult.container
})
await wait()
expect(container).toMatchSnapshot()
})
test('user dropdown open', async () => {
const component = wrapIntl(
<UserProperty
value={'user-id-1'}
readonly={false}
onChange={() => {
}}
/>,
)
let container: Element | DocumentFragment = {} as Element
act(() => {
const renderResult = render(component)
container = renderResult.container
})
await wait()
if (container) {
// this is the actual element where the click event triggers
// opening of the dropdown
const userProperty = container.querySelector('.UserProperty > div > div:nth-child(1) > div:nth-child(2) > div > input')
expect(userProperty).not.toBeNull()
act(() => {
userEvent.click(userProperty as Element)
})
expect(container).toMatchSnapshot()
} else {
throw new Error('container should have been initialized')
}
})
})

View File

@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useContext} from 'react'
import Select from 'react-select'
import {IUser, WorkspaceUsersContext, WorkspaceUsersContextData} from '../../../user'
import './user.scss'
import {getSelectBaseStyle} from '../../../theme'
type Props = {
value: string,
readonly: boolean,
onChange: (value: string) => void,
}
const UserProperty = (props: Props): JSX.Element => {
const workspaceUsers = useContext<WorkspaceUsersContextData>(WorkspaceUsersContext)
if (props.readonly) {
return (<div className='UserProperty octo-propertyvalue'>{workspaceUsers.usersById.get(props.value)?.username || props.value}</div>)
}
return (
<Select
options={workspaceUsers.users}
isSearchable={true}
isClearable={true}
backspaceRemovesValue={true}
className={'UserProperty'}
styles={getSelectBaseStyle()}
getOptionLabel={(o: IUser) => o.username}
getOptionValue={(a: IUser) => a.id}
value={workspaceUsers.usersById.get(props.value) || null}
onChange={(item, action) => {
if (action.action === 'select-option') {
props.onChange(item?.id || '')
} else if (action.action === 'clear') {
props.onChange('')
}
}}
/>
)
}
export default UserProperty

View File

@ -18,6 +18,7 @@ import Label from '../widgets/label'
import EditableDayPicker from '../widgets/editableDayPicker'
import Switch from '../widgets/switch'
import UserProperty from './properties/user/user'
import MultiSelectProperty from './properties/multiSelect'
import URLProperty from './properties/link/link'
@ -133,19 +134,15 @@ const PropertyValueElement = (props:Props): JSX.Element => {
}
/>
)
} else if (propertyTemplate.type === 'url') {
} else if (propertyTemplate.type === 'person') {
return (
<URLProperty
value={value as string}
onChange={setValue}
onSave={() => mutator.changePropertyValue(card, propertyTemplate.id, value)}
onCancel={() => setValue(propertyValue)}
validator={(newValue) => validateProp(propertyTemplate.type, newValue)}
<UserProperty
value={propertyValue as string}
readonly={readOnly}
onChange={(newValue) => mutator.changePropertyValue(card, propertyTemplate.id, newValue)}
/>
)
}
if (propertyTemplate.type === 'date') {
} else if (propertyTemplate.type === 'date') {
if (readOnly) {
return <div className='octo-propertyvalue'>{displayValue}</div>
}
@ -156,6 +153,16 @@ const PropertyValueElement = (props:Props): JSX.Element => {
onChange={(newValue) => mutator.changePropertyValue(card, propertyTemplate.id, newValue)}
/>
)
} else if (propertyTemplate.type === 'url') {
return (
<URLProperty
value={value as string}
onChange={setValue}
onSave={() => mutator.changePropertyValue(card, propertyTemplate.id, value)}
onCancel={() => setValue(propertyValue)}
validator={(newValue) => validateProp(propertyTemplate.type, newValue)}
/>
)
}
if (propertyTemplate.type === 'checkbox') {

View File

@ -359,6 +359,15 @@ class OctoClient {
const blob = await response.blob()
return URL.createObjectURL(blob)
}
async getWorkspaceUsers(): Promise<Array<IUser>> {
const path = this.workspacePath() + '/users'
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as IUser[]
}
}
const octoClient = new OctoClient()

View File

@ -16,6 +16,7 @@ import {Utils} from '../utils'
import {BoardTree, MutableBoardTree} from '../viewModel/boardTree'
import {MutableWorkspaceTree, WorkspaceTree} from '../viewModel/workspaceTree'
import './boardPage.scss'
import {IUser, WorkspaceUsersContext, WorkspaceUsersContextData} from '../user'
type Props = RouteComponentProps<{workspaceId?: string}> & {
readonly?: boolean
@ -29,6 +30,7 @@ type State = {
workspaceTree: WorkspaceTree
boardTree?: BoardTree
syncFailed?: boolean
workspaceUsers: WorkspaceUsersContextData
}
class BoardPage extends React.Component<Props, State> {
@ -57,8 +59,13 @@ class BoardPage extends React.Component<Props, State> {
boardId,
viewId,
workspaceTree: new MutableWorkspaceTree(),
workspaceUsers: {
users: new Array<IUser>(),
usersById: new Map<string, IUser>(),
},
}
this.setWorkspaceUsers()
Utils.log(`BoardPage. boardId: ${boardId}`)
}
@ -88,6 +95,28 @@ class BoardPage extends React.Component<Props, State> {
document.title = 'OCTO'
}
}
if (this.state.workspace?.id !== prevState.workspace?.id) {
this.setWorkspaceUsers()
}
}
async setWorkspaceUsers() {
const workspaceUsers = await octoClient.getWorkspaceUsers()
// storing workspaceUsersById in state to avoid re-computation in each render cycle
this.setState({
workspaceUsers: {
users: workspaceUsers,
usersById: this.getIdToWorkspaceUsers(workspaceUsers),
},
})
}
getIdToWorkspaceUsers(users: Array<IUser>): Map<string, IUser> {
return users.reduce((acc: Map<string, IUser>, user: IUser) => {
acc.set(user.id, user)
return acc
}, new Map())
}
private undoRedoHandler = async (keyName: string, e: KeyboardEvent) => {
@ -158,27 +187,29 @@ class BoardPage extends React.Component<Props, State> {
}
return (
<div className='BoardPage'>
<HotKeys
keyName='shift+ctrl+z,shift+cmd+z,ctrl+z,cmd+z'
onKeyDown={this.undoRedoHandler}
/>
<Workspace
workspace={workspace}
workspaceTree={workspaceTree}
boardTree={this.state.boardTree}
showView={(id, boardId) => {
this.showView(id, boardId)
}}
showBoard={(id) => {
this.showBoard(id)
}}
setSearchText={(text) => {
this.setSearchText(text)
}}
readonly={this.props.readonly || false}
/>
</div>
<WorkspaceUsersContext.Provider value={this.state.workspaceUsers}>
<div className='BoardPage'>
<HotKeys
keyName='shift+ctrl+z,shift+cmd+z,ctrl+z,cmd+z'
onKeyDown={this.undoRedoHandler}
/>
<Workspace
workspace={workspace}
workspaceTree={workspaceTree}
boardTree={this.state.boardTree}
showView={(id, boardId) => {
this.showView(id, boardId)
}}
showBoard={(id) => {
this.showBoard(id)
}}
setSearchText={(text) => {
this.setSearchText(text)
}}
readonly={this.props.readonly || false}
/>
</div>
</WorkspaceUsersContext.Provider>
)
}

View File

@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {CSSObject} from '@emotion/serialize'
export type Theme = {
mainBg: string,
mainFg: string,
@ -156,3 +158,67 @@ export function initThemes(): void {
}
loadTheme()
}
export function getSelectBaseStyle() {
return {
dropdownIndicator: (provided: CSSObject): CSSObject => ({
...provided,
display: 'none !important',
}),
indicatorSeparator: (provided: CSSObject): CSSObject => ({
...provided,
display: 'none',
}),
loadingIndicator: (provided: CSSObject): CSSObject => ({
...provided,
display: 'none',
}),
clearIndicator: (provided: CSSObject): CSSObject => ({
...provided,
display: 'none',
}),
menu: (provided: CSSObject): CSSObject => ({
...provided,
width: 'unset',
background: 'rgb(var(--main-bg))',
}),
option: (provided: CSSObject, state: { isFocused: boolean }): CSSObject => ({
...provided,
background: state.isFocused ? 'rgba(var(--main-fg), 0.1)' : 'rgb(var(--main-bg))',
color: state.isFocused ? 'rgb(var(--main-fg))' : 'rgb(var(--main-fg))',
padding: '2px 8px',
}),
control: (): CSSObject => ({
border: 0,
width: '100%',
margin: '4px 0 0 0',
// display: 'flex',
// marginTop: 0,
}),
valueContainer: (provided: CSSObject): CSSObject => ({
...provided,
padding: '0 5px',
overflow: 'unset',
// height: '20px',
}),
singleValue: (provided: CSSObject): CSSObject => ({
...provided,
color: 'rgb(var(--main-fg))',
overflow: 'unset',
maxWidth: 'calc(100% - 20px)',
}),
input: (provided: CSSObject): CSSObject => ({
...provided,
paddingBottom: 0,
paddingTop: 0,
marginBottom: 0,
marginTop: 0,
}),
menuList: (provided: CSSObject): CSSObject => ({
...provided,
overflowY: 'unset',
}),
}
}

View File

@ -5,6 +5,11 @@ import React from 'react'
const UserContext = React.createContext(undefined as IUser|undefined)
const WorkspaceUsersContext = React.createContext({
users: new Array<IUser>(),
usersById: new Map<string, IUser>(),
})
interface IUser {
id: string,
username: string,
@ -14,4 +19,9 @@ interface IUser {
updateAt: number,
}
export {IUser, UserContext}
type WorkspaceUsersContextData = {
users: Array<IUser>
usersById: Map<string, IUser>
}
export {IUser, UserContext, WorkspaceUsersContext, WorkspaceUsersContextData}

View File

@ -187,6 +187,21 @@ exports[`widgets/PropertyMenu should match snapshot 1`] = `
class="noicon"
/>
</div>
<div
class="MenuOption TextOption menu-option"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Person
</div>
<div
class="noicon"
/>
</div>
<div
class="MenuOption TextOption menu-option"
>

View File

@ -129,6 +129,11 @@ const PropertyMenu = React.memo((props: Props) => {
name={typeDisplayName(intl, 'date')}
onClick={() => props.onTypeChanged('date')}
/>
<Menu.Text
id='person'
name={typeDisplayName(intl, 'person')}
onClick={() => props.onTypeChanged('person')}
/>
<Menu.Text
id='checkbox'
name={typeDisplayName(intl, 'checkbox')}

View File

@ -2,13 +2,16 @@
// See LICENSE.txt for license information.
import React from 'react'
import {useIntl} from 'react-intl'
import {ActionMeta, ValueType, FormatOptionLabelMeta} from 'react-select'
import {ActionMeta, FormatOptionLabelMeta, ValueType} from 'react-select'
import CreatableSelect from 'react-select/creatable'
import {CSSObject} from '@emotion/serialize'
import {IPropertyOption} from '../blocks/board'
import {Constants} from '../constants'
import {getSelectBaseStyle} from '../theme'
import Menu from './menu'
import MenuWrapper from './menuWrapper'
import IconButton from './buttons/iconButton'
@ -90,70 +93,47 @@ const ValueSelectorLabel = React.memo((props: LabelProps): JSX.Element => {
)
})
const valueSelectorStyle = {
...getSelectBaseStyle(),
option: (provided: CSSObject, state: {isFocused: boolean}): CSSObject => ({
...provided,
background: state.isFocused ? 'rgba(var(--main-fg), 0.1)' : 'rgb(var(--main-bg))',
color: state.isFocused ? 'rgb(var(--main-fg))' : 'rgb(var(--main-fg))',
padding: '8px',
}),
control: (): CSSObject => ({
border: 0,
width: '100%',
margin: '0',
}),
valueContainer: (provided: CSSObject): CSSObject => ({
...provided,
padding: '0 8px',
overflow: 'unset',
}),
multiValue: (provided: CSSObject): CSSObject => ({
...provided,
margin: 0,
padding: 0,
backgroundColor: 'transparent',
}),
multiValueLabel: (provided: CSSObject): CSSObject => ({
...provided,
display: 'flex',
paddingLeft: 0,
padding: 0,
}),
multiValueRemove: (): CSSObject => ({
display: 'none',
}),
}
function ValueSelector(props: Props): JSX.Element {
return (
<CreatableSelect
isMulti={props.isMulti}
isClearable={true}
styles={{
indicatorsContainer: (provided: CSSObject): CSSObject => ({
...provided,
display: 'none',
}),
menu: (provided: CSSObject): CSSObject => ({
...provided,
width: 'unset',
background: 'rgb(var(--main-bg))',
}),
option: (provided: CSSObject, state: {isFocused: boolean}): CSSObject => ({
...provided,
background: state.isFocused ? 'rgba(var(--main-fg), 0.1)' : 'rgb(var(--main-bg))',
color: state.isFocused ? 'rgb(var(--main-fg))' : 'rgb(var(--main-fg))',
padding: '8px',
}),
control: (): CSSObject => ({
border: 0,
width: '100%',
margin: '0',
}),
valueContainer: (provided: CSSObject): CSSObject => ({
...provided,
padding: '0 8px',
overflow: 'unset',
}),
singleValue: (provided: CSSObject): CSSObject => ({
...provided,
color: 'rgb(var(--main-fg))',
overflow: 'unset',
maxWidth: 'calc(100% - 20px)',
}),
input: (provided: CSSObject): CSSObject => ({
...provided,
paddingBottom: 0,
paddingTop: 0,
marginBottom: 0,
marginTop: 0,
}),
menuList: (provided: CSSObject): CSSObject => ({
...provided,
overflowY: 'unset',
}),
multiValue: (provided: CSSObject): CSSObject => ({
...provided,
margin: 0,
padding: 0,
backgroundColor: 'transparent',
}),
multiValueLabel: (provided: CSSObject): CSSObject => ({
...provided,
display: 'flex',
paddingLeft: 0,
padding: 0,
}),
multiValueRemove: (): CSSObject => ({
display: 'none',
}),
}}
styles={valueSelectorStyle}
formatOptionLabel={(option: IPropertyOption, meta: FormatOptionLabelMeta<IPropertyOption, true | false>) => (
<ValueSelectorLabel
option={option}