diff --git a/server/api/api.go b/server/api/api.go index 44bb148c3..c35ed69cd 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -2188,7 +2188,7 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) { auditRec.AddMeta("teamID", teamID) // retrieve boards list - boards, err := a.app.GetTemplateBoards(teamID) + boards, err := a.app.GetTemplateBoards(teamID, userID) if err != nil { a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) return diff --git a/server/app/boards.go b/server/app/boards.go index 8500d4d51..6baba12de 100644 --- a/server/app/boards.go +++ b/server/app/boards.go @@ -150,8 +150,8 @@ func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, er return a.store.GetBoardsForUserAndTeam(userID, teamID) } -func (a *App) GetTemplateBoards(teamID string) ([]*model.Board, error) { - return a.store.GetTemplateBoards(teamID) +func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) { + return a.store.GetTemplateBoards(teamID, userID) } func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) { diff --git a/server/app/onboarding.go b/server/app/onboarding.go index 369e32227..03aff7169 100644 --- a/server/app/onboarding.go +++ b/server/app/onboarding.go @@ -46,14 +46,14 @@ func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, strin } func (a *App) getOnboardingBoardID() (string, error) { - boards, err := a.store.GetTemplateBoards(globalTeamID) + boards, err := a.store.GetTemplateBoards(globalTeamID, "") if err != nil { return "", err } var onboardingBoardID string for _, block := range boards { - if block.Title == WelcomeBoardTitle { + if block.Title == WelcomeBoardTitle && block.TeamID == globalTeamID { onboardingBoardID = block.ID break } diff --git a/server/app/onboarding_test.go b/server/app/onboarding_test.go index 1388add44..ca57855c0 100644 --- a/server/app/onboarding_test.go +++ b/server/app/onboarding_test.go @@ -25,7 +25,7 @@ func TestPrepareOnboardingTour(t *testing.T) { IsTemplate: true, } - th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) + th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil) th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil) @@ -60,7 +60,7 @@ func TestCreateWelcomeBoard(t *testing.T) { TeamID: "0", IsTemplate: true, } - th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) + th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false). Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil) th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil) @@ -72,7 +72,7 @@ func TestCreateWelcomeBoard(t *testing.T) { t.Run("template doesn't contain a board", func(t *testing.T) { teamID := testTeamID - th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil) + th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{}, nil) boardID, err := th.App.createWelcomeBoard("user_id_1", teamID) assert.Error(t, err) assert.Empty(t, boardID) @@ -86,7 +86,7 @@ func TestCreateWelcomeBoard(t *testing.T) { TeamID: teamID, IsTemplate: true, } - th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) + th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") assert.Error(t, err) assert.Empty(t, boardID) @@ -104,7 +104,7 @@ func TestGetOnboardingBoardID(t *testing.T) { TeamID: "0", IsTemplate: true, } - th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) + th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) onboardingBoardID, err := th.App.getOnboardingBoardID() assert.NoError(t, err) @@ -112,7 +112,7 @@ func TestGetOnboardingBoardID(t *testing.T) { }) t.Run("no blocks found", func(t *testing.T) { - th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil) + th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{}, nil) onboardingBoardID, err := th.App.getOnboardingBoardID() assert.Error(t, err) @@ -126,7 +126,7 @@ func TestGetOnboardingBoardID(t *testing.T) { TeamID: "0", IsTemplate: true, } - th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) + th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) onboardingBoardID, err := th.App.getOnboardingBoardID() assert.Error(t, err) diff --git a/server/app/templates.go b/server/app/templates.go index 2b05827de..96c1e7453 100644 --- a/server/app/templates.go +++ b/server/app/templates.go @@ -23,7 +23,7 @@ func (a *App) InitTemplates() error { // initializeTemplates imports default templates if the boards table is empty. func (a *App) initializeTemplates() (bool, error) { - boards, err := a.store.GetTemplateBoards(globalTeamID) + boards, err := a.store.GetTemplateBoards(globalTeamID, "") if err != nil { return false, fmt.Errorf("cannot initialize templates: %w", err) } diff --git a/server/app/templates_test.go b/server/app/templates_test.go index 37defa865..4092f366c 100644 --- a/server/app/templates_test.go +++ b/server/app/templates_test.go @@ -38,7 +38,7 @@ func TestApp_initializeTemplates(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - th.Store.EXPECT().GetTemplateBoards(globalTeamID).Return([]*model.Board{}, nil) + th.Store.EXPECT().GetTemplateBoards(globalTeamID, "").Return([]*model.Board{}, nil) th.Store.EXPECT().RemoveDefaultTemplates([]*model.Board{}).Return(nil) th.Store.EXPECT().CreateBoardsAndBlocks(gomock.Any(), gomock.Any()).AnyTimes().Return(boardsAndBlocks, nil) th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil) @@ -54,7 +54,7 @@ func TestApp_initializeTemplates(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() - th.Store.EXPECT().GetTemplateBoards(globalTeamID).Return([]*model.Board{board}, nil) + th.Store.EXPECT().GetTemplateBoards(globalTeamID, "").Return([]*model.Board{board}, nil) done, err := th.App.initializeTemplates() require.NoError(t, err, "initializeTemplates should not error") diff --git a/server/auth/auth_test.go b/server/auth/auth_test.go index 04b6b0218..4103edba5 100644 --- a/server/auth/auth_test.go +++ b/server/auth/auth_test.go @@ -42,7 +42,7 @@ func setupTestHelper(t *testing.T) *TestHelper { newAuth := New(&cfg, mockStore, localpermissions.New(mockPermissions, logger)) // called during default template setup for every test - mockStore.EXPECT().GetTemplateBoards(gomock.Any()).AnyTimes() + mockStore.EXPECT().GetTemplateBoards("0", "").AnyTimes() mockStore.EXPECT().RemoveDefaultTemplates(gomock.Any()).AnyTimes() mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any()).AnyTimes() diff --git a/server/services/store/mockstore/mockstore.go b/server/services/store/mockstore/mockstore.go index 6bb48ae64..462b401a7 100644 --- a/server/services/store/mockstore/mockstore.go +++ b/server/services/store/mockstore/mockstore.go @@ -881,18 +881,18 @@ func (mr *MockStoreMockRecorder) GetTeamsForUser(arg0 interface{}) *gomock.Call } // GetTemplateBoards mocks base method. -func (m *MockStore) GetTemplateBoards(arg0 string) ([]*model.Board, error) { +func (m *MockStore) GetTemplateBoards(arg0, arg1 string) ([]*model.Board, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTemplateBoards", arg0) + ret := m.ctrl.Call(m, "GetTemplateBoards", arg0, arg1) ret0, _ := ret[0].([]*model.Board) ret1, _ := ret[1].(error) return ret0, ret1 } // GetTemplateBoards indicates an expected call of GetTemplateBoards. -func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0, arg1) } // GetUserByEmail mocks base method. diff --git a/server/services/store/sqlstore/public_methods.go b/server/services/store/sqlstore/public_methods.go index 0a334f7d1..a3f30591b 100644 --- a/server/services/store/sqlstore/public_methods.go +++ b/server/services/store/sqlstore/public_methods.go @@ -435,8 +435,8 @@ func (s *SQLStore) GetTeamsForUser(userID string) ([]*model.Team, error) { } -func (s *SQLStore) GetTemplateBoards(teamID string) ([]*model.Board, error) { - return s.getTemplateBoards(s.db, teamID) +func (s *SQLStore) GetTemplateBoards(teamID string, userID string) ([]*model.Board, error) { + return s.getTemplateBoards(s.db, teamID, userID) } diff --git a/server/services/store/sqlstore/templates.go b/server/services/store/sqlstore/templates.go index a58ba15b4..f23b0fa4c 100644 --- a/server/services/store/sqlstore/templates.go +++ b/server/services/store/sqlstore/templates.go @@ -54,13 +54,24 @@ func (s *SQLStore) removeDefaultTemplates(db sq.BaseRunner, boards []*model.Boar return nil } -// getDefaultTemplateBoards fetches all template blocks . -func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID string) ([]*model.Board, error) { +// getTemplateBoards fetches all template boards . +func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID, userID string) ([]*model.Board, error) { query := s.getQueryBuilder(db). Select(boardFields("")...). - From(s.tablePrefix + "boards"). - Where(sq.Eq{"coalesce(team_id, '0')": teamID}). - Where(sq.Eq{"is_template": true}) + From(s.tablePrefix+"boards as b"). + LeftJoin(s.tablePrefix+"board_members as bm on b.id = bm.board_id and bm.user_id = ?", userID). + Where(sq.Eq{"is_template": true}). + Where(sq.Eq{"b.team_id": teamID}). + Where(sq.Or{ + // this is to include public templates even if there is not board_member entry + sq.And{ + sq.Eq{"bm.board_id": nil}, + sq.Eq{"b.type": model.BoardTypeOpen}, + }, + sq.And{ + sq.NotEq{"bm.board_id": nil}, + }, + }) rows, err := query.Query() if err != nil { @@ -69,5 +80,10 @@ func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID string) ([]*model. } defer s.CloseRows(rows) - return s.boardsFromRows(rows) + userTemplates, err := s.boardsFromRows(rows) + if err != nil { + return nil, err + } + + return userTemplates, nil } diff --git a/server/services/store/store.go b/server/services/store/store.go index 1dce5b066..666a4c32f 100644 --- a/server/services/store/store.go +++ b/server/services/store/store.go @@ -129,7 +129,7 @@ type Store interface { GetNextNotificationHint(remove bool) (*model.NotificationHint, error) RemoveDefaultTemplates(boards []*model.Board) error - GetTemplateBoards(teamID string) ([]*model.Board, error) + GetTemplateBoards(teamID, userID string) ([]*model.Board, error) DBType() string diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 32f39a4f2..072a4bd73 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -181,6 +181,7 @@ "ShareBoard.tokenRegenrated": "Token regenerated", "ShareBoard.userPermissionsRemoveMemberText": "Remove member", "ShareBoard.userPermissionsYouText": "(You)", + "ShareTemplate.Title": "Share Template", "Sidebar.about": "About Focalboard", "Sidebar.add-board": "+ Add board", "Sidebar.changePassword": "Change password", diff --git a/webapp/src/components/shareBoard/shareBoard.scss b/webapp/src/components/shareBoard/shareBoard.scss index 325752e87..692864107 100644 --- a/webapp/src/components/shareBoard/shareBoard.scss +++ b/webapp/src/components/shareBoard/shareBoard.scss @@ -212,5 +212,16 @@ text-decoration: underline; } } + + .Menu { + position: fixed; + left: 55%; + right: calc(45% - 240px); + + .menu-contents { + min-width: 240px; + max-width: 240px; + } + } } } diff --git a/webapp/src/components/shareBoard/shareBoard.tsx b/webapp/src/components/shareBoard/shareBoard.tsx index c3d89ef02..76dfc35a4 100644 --- a/webapp/src/components/shareBoard/shareBoard.tsx +++ b/webapp/src/components/shareBoard/shareBoard.tsx @@ -9,7 +9,7 @@ import Select from 'react-select/async' import {CSSObject} from '@emotion/serialize' import {useAppSelector} from '../../store/hooks' -import {getCurrentBoardId, getCurrentBoardMembers} from '../../store/boards' +import {getCurrentBoard, getCurrentBoardMembers} from '../../store/boards' import {getMe, getBoardUsersList} from '../../store/users' import {Utils, IDType} from '../../utils' @@ -95,7 +95,8 @@ export default function ShareBoardDialog(props: Props): JSX.Element { // members of the current board const members = useAppSelector<{[key: string]: BoardMember}>(getCurrentBoardMembers) - const boardId = useAppSelector(getCurrentBoardId) + const board = useAppSelector(getCurrentBoard) + const boardId = board.id const boardUsers = useAppSelector(getBoardUsersList) const me = useAppSelector(getMe) @@ -239,7 +240,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element { )) } - const toolbar = ( + const shareBoardTitle = ( ) + const shareTemplateTitle = ( + + + + ) + + const toolbar = board.isTemplate ? shareTemplateTitle : shareBoardTitle + return ( - {props.enableSharedBoards && ( + {props.enableSharedBoards && !board.isTemplate && (
)} - {(props.enableSharedBoards && publish) && + {(props.enableSharedBoards && publish && !board.isTemplate) && (
@@ -401,7 +413,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element { )} - {!publish && ( + {!publish && !board.isTemplate && (