mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-24 13:43:12 +02:00
Merge Onboarding feature branch into main (#2406)
* Persistent user config (#2133) * Added user config API * Add unit tests * lint fix * Fixed webapp tests * Fixed webapp tests * Updated props in store after updating * Minor fixes * Removed redundent data from audit logs * Onboarding Tour (#2287) * Created private board * Roughly displayed tour * Synced with Dhama's changes * WIP * Trying to add GIF * Added 3 tour steps * WIP * WIP * WIP * checked in missed file * Synced with feature branch * WIp * Adde skip tour option * Fixed image loading for on-prem * Made tour work on presonal server: * Adde missed file * Adding telemetry * Adding telemetry * Added tour tip telemetry * Fixed pulsating dot styling for personal server * reverted personal config * Added reset tour button * Displayed share tour tip of feature is enabled * Lint fixes * Fixed webapp tests * Fixed webapp tests * Completed webapp tests * Completed webapp tests * Webapp lint fixes * Added server tests * Testing cypress skip tour fix * Fixed Cypress tests * Added share board tour step * Added share board tour step * webapp lint fixes * Updated logic to pick welcome board * Updated tests: * lint fixes * Updating UI changes * Fixed a bug causing card tour to re-appear * FIxed minor issue * FIxed bug where card tour didn't start in clickingh on card * Fixed tests * Make update user props use string instead of interface * Fixed a value type * Updating gif size * Updating resolution breakpoint * Updating tutorial tip * Updating view selector * Refactored tour components * Misc fixes * minor refactoring * GH-2258: allow date range to overflow (#2268) * allow date range to overflow * Fixed issue with date overflowing into neighbouring column Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Update readme with accurate Linux standalone app build instructions (#2351) * Bump follow-redirects from 1.14.7 to 1.14.8 in /experiments/webext (#2339) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Switch component style fixed: selector specificity increased by adding additional class. (#2179) * Adding sever side undelete endpoint (#2222) * Adding sever side undelete endpoint * Removing long lines golangci-lint errors * Fixing linter errors * Fixing a test problem * Fixing tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Removing transactions from sqlite backend (#2361) * Removing transactions from sqlite backend * Skipping tests in sqlite because the lack of transactions * Generating the mocks * Fixing golangci-lint * Fixing problem opening the tour tooltip on card open * Fixing texts missmatch * Adding the Product Tour entry in the user settings menu * Fixing some tests * Fixing tests Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com> * Restored package json * Restored package json Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kamre <eremchenko@gmail.com> Co-authored-by: Jesús Espino <jespinog@gmail.com>
This commit is contained in:
parent
cd6be86a36
commit
ab3bf6312c
@ -105,7 +105,7 @@ func (p *Plugin) OnActivate() error {
|
||||
return fmt.Errorf("error initializing the DB: %w", err)
|
||||
}
|
||||
if cfg.AuthMode == server.MattermostAuthMod {
|
||||
layeredStore, err2 := mattermostauthlayer.New(cfg.DBType, sqlDB, db, logger)
|
||||
layeredStore, err2 := mattermostauthlayer.New(cfg.DBType, sqlDB, db, logger, p.API)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("error initializing the DB: %w", err2)
|
||||
}
|
||||
@ -329,10 +329,10 @@ func defaultLoggingConfig() string {
|
||||
"Filename": "focalboard_errors.log",
|
||||
"MaxAgeDays": 0,
|
||||
"MaxBackups": 5,
|
||||
"MaxSizeMB": 10
|
||||
"MaxSizeMB": 10
|
||||
},
|
||||
"MaxQueueSize": 1000
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
|
4128
mattermost-plugin/webapp/package-lock.json
generated
4128
mattermost-plugin/webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
||||
import React, {useEffect} from 'react'
|
||||
import {Store, Action} from 'redux'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {createBrowserHistory, History} from 'history'
|
||||
|
||||
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder'
|
||||
|
||||
@ -13,10 +14,9 @@ windowAny.baseURL = '/plugins/focalboard'
|
||||
windowAny.frontendBaseURL = '/boards'
|
||||
windowAny.isFocalboardPlugin = true
|
||||
|
||||
import {ClientConfig} from 'mattermost-redux/types/config'
|
||||
|
||||
import App from '../../../webapp/src/app'
|
||||
import store from '../../../webapp/src/store'
|
||||
import {Utils} from '../../../webapp/src/utils'
|
||||
import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader'
|
||||
import FocalboardIcon from '../../../webapp/src/widgets/icons/logo'
|
||||
import {setMattermostTheme} from '../../../webapp/src/theme'
|
||||
@ -67,6 +67,46 @@ type Props = {
|
||||
webSocketClient: MMWebSocketClient
|
||||
}
|
||||
|
||||
function customHistory() {
|
||||
const history = createBrowserHistory({basename: Utils.getFrontendBaseURL()})
|
||||
|
||||
if (Utils.isDesktop()) {
|
||||
window.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.origin !== windowAny.location.origin) {
|
||||
return
|
||||
}
|
||||
|
||||
const pathName = event.data.message?.pathName
|
||||
if (!pathName || !pathName.startsWith(windowAny.frontendBaseURL)) {
|
||||
return
|
||||
}
|
||||
|
||||
Utils.log(`Navigating Boards to ${pathName}`)
|
||||
history.replace(pathName.replace(windowAny.frontendBaseURL, ''))
|
||||
})
|
||||
}
|
||||
return {
|
||||
...history,
|
||||
push: (path: string, state?: unknown) => {
|
||||
if (Utils.isDesktop()) {
|
||||
windowAny.postMessage(
|
||||
{
|
||||
type: 'browser-history-push',
|
||||
message: {
|
||||
path: `${windowAny.frontendBaseURL}${path}`,
|
||||
},
|
||||
},
|
||||
windowAny.location.origin,
|
||||
)
|
||||
} else {
|
||||
history.push(path, state as Record<string, never>)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let browserHistory: History<unknown>
|
||||
|
||||
const MainApp = (props: Props) => {
|
||||
wsClient.initPlugin(manifest.id, manifest.version, props.webSocketClient)
|
||||
|
||||
@ -91,7 +131,7 @@ const MainApp = (props: Props) => {
|
||||
<ErrorBoundary>
|
||||
<ReduxProvider store={store}>
|
||||
<div id='focalboard-app'>
|
||||
<App/>
|
||||
<App history={browserHistory}/>
|
||||
</div>
|
||||
<div id='focalboard-root-portal'/>
|
||||
</ReduxProvider>
|
||||
@ -102,7 +142,7 @@ const MainApp = (props: Props) => {
|
||||
const HeaderComponent = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<GlobalHeader/>
|
||||
<GlobalHeader history={browserHistory}/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
@ -117,6 +157,7 @@ export default class Plugin {
|
||||
const subpath = siteURL ? getSubpath(siteURL) : ''
|
||||
windowAny.frontendBaseURL = subpath + windowAny.frontendBaseURL
|
||||
windowAny.baseURL = subpath + windowAny.baseURL
|
||||
browserHistory = customHistory()
|
||||
|
||||
this.registry = registry
|
||||
|
||||
|
@ -111,7 +111,7 @@ module.exports = {
|
||||
exclude: [/node_modules/],
|
||||
},
|
||||
{
|
||||
test: /\.(png|eot|tiff|svg|woff2|woff|ttf|jpg)$/,
|
||||
test: /\.(png|eot|tiff|svg|woff2|woff|ttf|jpg|gif)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
|
@ -87,6 +87,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv1.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET")
|
||||
apiv1.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET")
|
||||
apiv1.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST")
|
||||
apiv1.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut)
|
||||
|
||||
apiv1.HandleFunc("/login", a.handleLogin).Methods("POST")
|
||||
apiv1.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST")
|
||||
@ -107,6 +108,9 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE")
|
||||
apiv1.HandleFunc("/workspaces/{workspaceID}/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET")
|
||||
|
||||
// onboarding tour endpoints
|
||||
apiv1.HandleFunc("/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost)
|
||||
|
||||
// archives
|
||||
apiv1.HandleFunc("/workspaces/{workspaceID}/archive/export", a.sessionRequired(a.handleArchiveExport)).Methods("GET")
|
||||
apiv1.HandleFunc("/workspaces/{workspaceID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
|
||||
@ -333,25 +337,6 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func stampModificationMetadata(r *http.Request, blocks []model.Block, auditRec *audit.Record) {
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
userID := session.UserID
|
||||
if userID == model.SingleUser {
|
||||
userID = ""
|
||||
}
|
||||
|
||||
now := utils.GetMillis()
|
||||
for i := range blocks {
|
||||
blocks[i].ModifiedBy = userID
|
||||
blocks[i].UpdateAt = now
|
||||
|
||||
if auditRec != nil {
|
||||
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks updateBlocks
|
||||
//
|
||||
@ -436,10 +421,11 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
|
||||
stampModificationMetadata(r, blocks, auditRec)
|
||||
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
userID := session.UserID
|
||||
|
||||
model.StampModificationMetadata(userID, blocks, auditRec)
|
||||
|
||||
// this query param exists when creating template from board, or board from template
|
||||
sourceBoardID := r.URL.Query().Get("sourceBoardID")
|
||||
@ -470,6 +456,80 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation PATCH /api/v1/users/{userID}/config updateUserConfig
|
||||
//
|
||||
// Updates user config
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: userID
|
||||
// in: path
|
||||
// description: User ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: Body
|
||||
// in: body
|
||||
// description: User config patch to apply
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UserPropPatch"
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse
|
||||
|
||||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
var patch *model.UserPropPatch
|
||||
err = json.Unmarshal(requestBody, &patch)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userID"]
|
||||
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "updateUserConfig", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
|
||||
// a user can update only own config
|
||||
if userID != session.UserID {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
updatedConfig, err := a.app.UpdateUserConfig(userID, *patch)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(updatedConfig)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /api/v1/users/{userID} getUser
|
||||
//
|
||||
@ -1695,6 +1755,47 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /api/v1/onboard onboard
|
||||
//
|
||||
// Onboards a user on Boards.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// "$ref": "#/definitions/OnboardingResponse"
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
|
||||
workspaceID, boardID, err := a.app.PrepareOnboardingTour(session.UserID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"workspaceID": workspaceID,
|
||||
"boardID": boardID,
|
||||
}
|
||||
data, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
}
|
||||
|
||||
// Response helpers
|
||||
|
||||
func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) {
|
||||
|
109
server/app/onboarding.go
Normal file
109
server/app/onboarding.go
Normal file
@ -0,0 +1,109 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyPrefix = "focalboard_" // use key prefix to namespace focalboard props
|
||||
KeyOnboardingTourStarted = KeyPrefix + "onboardingTourStarted"
|
||||
KeyOnboardingTourCategory = KeyPrefix + "tourCategory"
|
||||
KeyOnboardingTourStep = KeyPrefix + "onboardingTourStep"
|
||||
|
||||
ValueOnboardingFirstStep = "0"
|
||||
ValueTourCategoryOnboarding = "onboarding"
|
||||
|
||||
WelcomeBoardTitle = "Welcome to Boards!"
|
||||
)
|
||||
|
||||
var (
|
||||
errUnableToFindWelcomeBoard = errors.New("unable to find welcome board in newly created blocks")
|
||||
)
|
||||
|
||||
func (a *App) PrepareOnboardingTour(userID string) (string, string, error) {
|
||||
// create a private workspace for the user
|
||||
workspaceID, err := a.store.CreatePrivateWorkspace(userID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// copy the welcome board into this workspace
|
||||
boardID, err := a.createWelcomeBoard(userID, workspaceID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// set user's tour state to initial state
|
||||
userPropPatch := model.UserPropPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
KeyOnboardingTourStarted: "1",
|
||||
KeyOnboardingTourStep: ValueOnboardingFirstStep,
|
||||
KeyOnboardingTourCategory: ValueTourCategoryOnboarding,
|
||||
},
|
||||
}
|
||||
if err := a.store.PatchUserProps(userID, userPropPatch); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return workspaceID, boardID, nil
|
||||
}
|
||||
|
||||
func (a *App) getOnboardingBoardID() (string, error) {
|
||||
blocks, err := a.store.GetDefaultTemplateBlocks()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var onboardingBoardID string
|
||||
for _, block := range blocks {
|
||||
if block.Type == model.TypeBoard && block.Title == WelcomeBoardTitle {
|
||||
onboardingBoardID = block.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if onboardingBoardID == "" {
|
||||
return "", errUnableToFindWelcomeBoard
|
||||
}
|
||||
|
||||
return onboardingBoardID, nil
|
||||
}
|
||||
|
||||
func (a *App) createWelcomeBoard(userID, workspaceID string) (string, error) {
|
||||
onboardingBoardID, err := a.getOnboardingBoardID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blocks, err := a.GetSubTree(store.Container{WorkspaceID: "0"}, onboardingBoardID, 3)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blocks = model.GenerateBlockIDs(blocks, a.logger)
|
||||
|
||||
// we're copying from a global template, so we need to set the
|
||||
// `isTemplate` flag to false on the board
|
||||
var welcomeBoardID string
|
||||
for i := range blocks {
|
||||
if blocks[i].Type == model.TypeBoard {
|
||||
blocks[i].Fields["isTemplate"] = false
|
||||
|
||||
if blocks[i].Title == WelcomeBoardTitle {
|
||||
welcomeBoardID = blocks[i].ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.StampModificationMetadata(userID, blocks, nil)
|
||||
_, err = a.InsertBlocks(store.Container{WorkspaceID: workspaceID}, blocks, userID, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return welcomeBoardID, nil
|
||||
}
|
214
server/app/onboarding_test.go
Normal file
214
server/app/onboarding_test.go
Normal file
@ -0,0 +1,214 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrepareOnboardingTour(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Welcome to Boards!",
|
||||
Fields: map[string]interface{}{
|
||||
"isTemplate": true,
|
||||
},
|
||||
}
|
||||
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"block_id_1",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
th.Store.EXPECT().CreatePrivateWorkspace("user_id_1").Return("workspace_id_1", nil)
|
||||
|
||||
userPropPatch := model.UserPropPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
KeyOnboardingTourStarted: "1",
|
||||
KeyOnboardingTourStep: ValueOnboardingFirstStep,
|
||||
KeyOnboardingTourCategory: ValueTourCategoryOnboarding,
|
||||
},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().PatchUserProps("user_id_1", userPropPatch).Return(nil)
|
||||
|
||||
workspaceID, boardID, err := th.App.PrepareOnboardingTour("user_id_1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "workspace_id_1", workspaceID)
|
||||
assert.NotEmpty(t, boardID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateWelcomeBoard(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Welcome to Boards!",
|
||||
Fields: map[string]interface{}{
|
||||
"isTemplate": true,
|
||||
},
|
||||
}
|
||||
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"block_id_1",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
|
||||
assert.Nil(t, err)
|
||||
assert.NotEmpty(t, boardID)
|
||||
})
|
||||
|
||||
t.Run("template doesn't contain a board", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeComment,
|
||||
Title: "Welcome to Boards!",
|
||||
}
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"buixxjic3xjfkieees4iafdrznc",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, boardID)
|
||||
})
|
||||
|
||||
t.Run("template doesn't contain the welcome board", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Jean luc Picard",
|
||||
Fields: map[string]interface{}{
|
||||
"isTemplate": true,
|
||||
},
|
||||
}
|
||||
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"buixxjic3xjfkieees4iafdrznc",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, boardID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOnboardingBoardID(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
board := model.Block{
|
||||
ID: "board_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Welcome to Boards!",
|
||||
}
|
||||
|
||||
card := model.Block{
|
||||
ID: "card_id_1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: board.ID,
|
||||
}
|
||||
|
||||
blocks := []model.Block{
|
||||
board,
|
||||
card,
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
onboardingBoardID, err := th.App.getOnboardingBoardID()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "board_id_1", onboardingBoardID)
|
||||
})
|
||||
|
||||
t.Run("no blocks found", func(t *testing.T) {
|
||||
blocks := []model.Block{}
|
||||
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
onboardingBoardID, err := th.App.getOnboardingBoardID()
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, onboardingBoardID)
|
||||
})
|
||||
|
||||
t.Run("onboarding board doesn't exists", func(t *testing.T) {
|
||||
board := model.Block{
|
||||
ID: "board_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Some board title",
|
||||
}
|
||||
|
||||
card := model.Block{
|
||||
ID: "card_id_1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: board.ID,
|
||||
}
|
||||
|
||||
blocks := []model.Block{
|
||||
board,
|
||||
card,
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
onboardingBoardID, err := th.App.getOnboardingBoardID()
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, onboardingBoardID)
|
||||
})
|
||||
}
|
@ -5,3 +5,16 @@ import "github.com/mattermost/focalboard/server/model"
|
||||
func (a *App) GetWorkspaceUsers(workspaceID string) ([]*model.User, error) {
|
||||
return a.store.GetUsersByWorkspace(workspaceID)
|
||||
}
|
||||
|
||||
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) {
|
||||
if err := a.store.PatchUserProps(userID, patch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := a.store.GetUserByID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.Props, nil
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ package model
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/focalboard/server/services/audit"
|
||||
)
|
||||
|
||||
// Block is the basic data unit
|
||||
@ -177,3 +180,19 @@ type QueryBlockHistoryOptions struct {
|
||||
Limit uint64 // if non-zero then limit the number of returned records
|
||||
Descending bool // if true then the records are sorted by insert_at in descending order
|
||||
}
|
||||
|
||||
func StampModificationMetadata(userID string, blocks []Block, auditRec *audit.Record) {
|
||||
if userID == SingleUser {
|
||||
userID = ""
|
||||
}
|
||||
|
||||
now := GetMillis()
|
||||
for i := range blocks {
|
||||
blocks[i].ModifiedBy = userID
|
||||
blocks[i].UpdateAt = now
|
||||
|
||||
if auditRec != nil {
|
||||
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package model
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
@ -252,3 +254,16 @@ func TestGenerateBlockIDs(t *testing.T) {
|
||||
require.Equal(t, blocks[2].ID, block4ContentOrder[1].([]interface{})[1])
|
||||
})
|
||||
}
|
||||
|
||||
func TestStampModificationMetadata(t *testing.T) {
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
block := Block{}
|
||||
blocks := []Block{block}
|
||||
assert.Empty(t, block.ModifiedBy)
|
||||
assert.Empty(t, block.UpdateAt)
|
||||
|
||||
StampModificationMetadata("user_id_1", blocks, nil)
|
||||
assert.Equal(t, "user_id_1", blocks[0].ModifiedBy)
|
||||
assert.NotEmpty(t, blocks[0].UpdateAt)
|
||||
})
|
||||
}
|
||||
|
@ -57,6 +57,16 @@ type User struct {
|
||||
IsBot bool `json:"is_bot"`
|
||||
}
|
||||
|
||||
type UserPropPatch struct {
|
||||
// The user prop updated fields
|
||||
// required: false
|
||||
UpdatedFields map[string]string `json:"updatedFields"`
|
||||
|
||||
// The user prop removed fields
|
||||
// required: false
|
||||
DeletedFields []string `json:"deletedFields"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/notify/notifylogger"
|
||||
"github.com/mattermost/focalboard/server/services/scheduler"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
"github.com/mattermost/focalboard/server/services/telemetry"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
@ -233,13 +232,6 @@ func NewStore(config *config.Configuration, logger *mlog.Logger) (store.Store, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.AuthMode == MattermostAuthMod {
|
||||
layeredStore, err2 := mattermostauthlayer.New(config.DBType, db.(*sqlstore.SQLStore).DBHandle(), db, logger)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
db = layeredStore
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
@ -40,18 +42,20 @@ func (pe NotSupportedError) Error() string {
|
||||
// Store represents the abstraction of the data storage.
|
||||
type MattermostAuthLayer struct {
|
||||
store.Store
|
||||
dbType string
|
||||
mmDB *sql.DB
|
||||
logger *mlog.Logger
|
||||
dbType string
|
||||
mmDB *sql.DB
|
||||
logger *mlog.Logger
|
||||
pluginAPI plugin.API
|
||||
}
|
||||
|
||||
// New creates a new SQL implementation of the store.
|
||||
func New(dbType string, db *sql.DB, store store.Store, logger *mlog.Logger) (*MattermostAuthLayer, error) {
|
||||
func New(dbType string, db *sql.DB, store store.Store, logger *mlog.Logger, pluginAPI plugin.API) (*MattermostAuthLayer, error) {
|
||||
layer := &MattermostAuthLayer{
|
||||
Store: store,
|
||||
dbType: dbType,
|
||||
mmDB: db,
|
||||
logger: logger,
|
||||
Store: store,
|
||||
dbType: dbType,
|
||||
mmDB: db,
|
||||
logger: logger,
|
||||
pluginAPI: pluginAPI,
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
@ -156,6 +160,33 @@ func (s *MattermostAuthLayer) UpdateUserPasswordByID(userID, password string) er
|
||||
return NotSupportedError{"no update allowed from focalboard, update it using mattermost"}
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) PatchUserProps(userID string, patch model.UserPropPatch) error {
|
||||
user, err := s.pluginAPI.GetUser(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to fetch user", mlog.String("userID", userID), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
|
||||
props := user.Props
|
||||
|
||||
for _, key := range patch.DeletedFields {
|
||||
delete(props, key)
|
||||
}
|
||||
|
||||
for key, value := range patch.UpdatedFields {
|
||||
props[key] = value
|
||||
}
|
||||
|
||||
user.Props = props
|
||||
|
||||
if _, err := s.pluginAPI.UpdateUser(user); err != nil {
|
||||
s.logger.Error("failed to update user", mlog.String("userID", userID), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveUserCount returns the number of users with active sessions within N seconds ago.
|
||||
func (s *MattermostAuthLayer) GetActiveUserCount(updatedSecondsAgo int64) (int, error) {
|
||||
query := s.getQueryBuilder().
|
||||
@ -456,3 +487,15 @@ func (s *MattermostAuthLayer) userWorkspacesFromRows(rows *sql.Rows) ([]model.Us
|
||||
|
||||
return userWorkspaces, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) CreatePrivateWorkspace(userID string) (string, error) {
|
||||
// we emulate a private workspace by creating
|
||||
// a DM channel from the user to themselves.
|
||||
channel, err := s.pluginAPI.GetDirectChannel(userID, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("error fetching private workspace", mlog.String("userID", userID), mlog.Err(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return channel.Id, nil
|
||||
}
|
||||
|
@ -50,6 +50,21 @@ func (mr *MockStoreMockRecorder) CleanUpSessions(arg0 interface{}) *gomock.Call
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpSessions", reflect.TypeOf((*MockStore)(nil).CleanUpSessions), arg0)
|
||||
}
|
||||
|
||||
// CreatePrivateWorkspace mocks base method.
|
||||
func (m *MockStore) CreatePrivateWorkspace(arg0 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreatePrivateWorkspace", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreatePrivateWorkspace indicates an expected call of CreatePrivateWorkspace.
|
||||
func (mr *MockStoreMockRecorder) CreatePrivateWorkspace(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePrivateWorkspace", reflect.TypeOf((*MockStore)(nil).CreatePrivateWorkspace), arg0)
|
||||
}
|
||||
|
||||
// CreateSession mocks base method.
|
||||
func (m *MockStore) CreateSession(arg0 *model.Session) error {
|
||||
m.ctrl.T.Helper()
|
||||
@ -760,6 +775,20 @@ func (mr *MockStoreMockRecorder) PatchBlocks(arg0, arg1, arg2 interface{}) *gomo
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBlocks", reflect.TypeOf((*MockStore)(nil).PatchBlocks), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// PatchUserProps mocks base method.
|
||||
func (m *MockStore) PatchUserProps(arg0 string, arg1 model.UserPropPatch) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PatchUserProps", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PatchUserProps indicates an expected call of PatchUserProps.
|
||||
func (mr *MockStoreMockRecorder) PatchUserProps(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchUserProps", reflect.TypeOf((*MockStore)(nil).PatchUserProps), arg0, arg1)
|
||||
}
|
||||
|
||||
// RefreshSession mocks base method.
|
||||
func (m *MockStore) RefreshSession(arg0 *model.Session) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -27,6 +27,11 @@ func (s *SQLStore) CleanUpSessions(expireTime int64) error {
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) CreatePrivateWorkspace(userID string) (string, error) {
|
||||
return s.createPrivateWorkspace(s.db, userID)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) CreateSession(session *model.Session) error {
|
||||
return s.createSession(s.db, session)
|
||||
|
||||
@ -352,6 +357,11 @@ func (s *SQLStore) PatchBlocks(c store.Container, blockPatches *model.BlockPatch
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) PatchUserProps(userID string, patch model.UserPropPatch) error {
|
||||
return s.patchUserProps(s.db, userID, patch)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) RefreshSession(session *model.Session) error {
|
||||
return s.refreshSession(s.db, session)
|
||||
|
||||
|
@ -234,3 +234,24 @@ func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) patchUserProps(db sq.BaseRunner, userID string, patch model.UserPropPatch) error {
|
||||
user, err := s.getUserByID(db, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Props == nil {
|
||||
user.Props = map[string]interface{}{}
|
||||
}
|
||||
|
||||
for _, key := range patch.DeletedFields {
|
||||
delete(user.Props, key)
|
||||
}
|
||||
|
||||
for key, value := range patch.UpdatedFields {
|
||||
user.Props[key] = value
|
||||
}
|
||||
|
||||
return s.updateUser(db, user)
|
||||
}
|
||||
|
@ -154,3 +154,9 @@ func (s *SQLStore) getWorkspaceCount(db sq.BaseRunner) (int64, error) {
|
||||
func (s *SQLStore) getUserWorkspaces(_ sq.BaseRunner, _ string) ([]model.UserWorkspace, error) {
|
||||
return nil, fmt.Errorf("GetUserWorkspaces %w", errUnsupportedOperation)
|
||||
}
|
||||
|
||||
func (s *SQLStore) createPrivateWorkspace(_ sq.BaseRunner, _ string) (string, error) {
|
||||
// for personal server we always have only
|
||||
// a single workspace, with id "0".
|
||||
return "0", nil
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ type Store interface {
|
||||
UpdateUserPassword(username, password string) error
|
||||
UpdateUserPasswordByID(userID, password string) error
|
||||
GetUsersByWorkspace(workspaceID string) ([]*model.User, error)
|
||||
PatchUserProps(userID string, patch model.UserPropPatch) error
|
||||
|
||||
GetActiveUserCount(updatedSecondsAgo int64) (int, error)
|
||||
GetSession(token string, expireTime int64) (*model.Session, error)
|
||||
@ -78,6 +79,7 @@ type Store interface {
|
||||
HasWorkspaceAccess(userID string, workspaceID string) (bool, error)
|
||||
GetWorkspaceCount() (int64, error)
|
||||
GetUserWorkspaces(userID string) ([]model.UserWorkspace, error)
|
||||
CreatePrivateWorkspace(userID string) (string, error)
|
||||
|
||||
CreateSubscription(c Container, sub *model.Subscription) (*model.Subscription, error)
|
||||
DeleteSubscription(c Container, blockID string, subscriberID string) error
|
||||
|
@ -39,6 +39,11 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
|
||||
defer tearDown()
|
||||
testCreateAndGetRegisteredUserCount(t, store)
|
||||
})
|
||||
t.Run("TestPatchUserProps", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testPatchUserProps(t, store)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetWorkspaceUsers(t *testing.T, store store.Store) {
|
||||
@ -164,3 +169,61 @@ func testCreateAndGetRegisteredUserCount(t *testing.T, store store.Store) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, randomN, got)
|
||||
}
|
||||
|
||||
func testPatchUserProps(t *testing.T, store store.Store) {
|
||||
user := &model.User{
|
||||
ID: utils.NewID(utils.IDTypeUser),
|
||||
}
|
||||
err := store.CreateUser(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Only update props
|
||||
patch := model.UserPropPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
"new_key_1": "new_value_1",
|
||||
"new_key_2": "new_value_2",
|
||||
"new_key_3": "new_value_3",
|
||||
},
|
||||
}
|
||||
|
||||
err = store.PatchUserProps(user.ID, patch)
|
||||
require.NoError(t, err)
|
||||
fetchedUser, err := store.GetUserByID(user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fetchedUser.Props["new_key_1"], "new_value_1")
|
||||
require.Equal(t, fetchedUser.Props["new_key_2"], "new_value_2")
|
||||
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3")
|
||||
|
||||
// Delete a prop
|
||||
patch = model.UserPropPatch{
|
||||
DeletedFields: []string{
|
||||
"new_key_1",
|
||||
},
|
||||
}
|
||||
|
||||
err = store.PatchUserProps(user.ID, patch)
|
||||
require.NoError(t, err)
|
||||
fetchedUser, err = store.GetUserByID(user.ID)
|
||||
require.NoError(t, err)
|
||||
_, ok := fetchedUser.Props["new_key_1"]
|
||||
require.False(t, ok)
|
||||
require.Equal(t, fetchedUser.Props["new_key_2"], "new_value_2")
|
||||
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3")
|
||||
|
||||
// update and delete together
|
||||
patch = model.UserPropPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
"new_key_3": "new_value_3_new_again",
|
||||
},
|
||||
DeletedFields: []string{
|
||||
"new_key_2",
|
||||
},
|
||||
}
|
||||
err = store.PatchUserProps(user.ID, patch)
|
||||
require.NoError(t, err)
|
||||
fetchedUser, err = store.GetUserByID(user.ID)
|
||||
require.NoError(t, err)
|
||||
_, ok = fetchedUser.Props["new_key_2"]
|
||||
require.False(t, ok)
|
||||
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3_new_again")
|
||||
}
|
||||
|
@ -109,6 +109,23 @@ definitions:
|
||||
x-go-name: UpdatedFields
|
||||
type: object
|
||||
x-go-package: github.com/mattermost/focalboard/server/model
|
||||
UserPropPatch:
|
||||
description: UserConfigPatch is a patch for user config
|
||||
properties:
|
||||
deletedFields:
|
||||
description: The config fields removed
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-go-name: DeletedFields
|
||||
updatedFields:
|
||||
additionalProperties:
|
||||
type: object
|
||||
description: The updated config
|
||||
type: object
|
||||
x-go-name: UpdatedFields
|
||||
type: object
|
||||
x-go-package: github.com/mattermost/focalboard/server/model
|
||||
BlockPatchBatch:
|
||||
description: BlockPatchBatch is a batch of IDs and patches for modify blocks
|
||||
properties:
|
||||
@ -476,6 +493,15 @@ definitions:
|
||||
- updateAt
|
||||
type: object
|
||||
x-go-package: github.com/mattermost/focalboard/server/model
|
||||
OnboardingResponse:
|
||||
description: OnboardResponse contains basic data required by the client to complete the onboarding
|
||||
properties:
|
||||
workspaceID:
|
||||
description: the workspace to send to user to, to start the onboarding tour
|
||||
type: string
|
||||
boardID:
|
||||
description: the board to send to user to, to start the onboarding tour
|
||||
type: string
|
||||
host: localhost
|
||||
info:
|
||||
contact:
|
||||
|
2
webapp/cypress/global.d.ts
vendored
2
webapp/cypress/global.d.ts
vendored
@ -17,6 +17,8 @@ declare namespace Cypress {
|
||||
apiInitServer: () => Chainable
|
||||
apiDeleteBlock: (id: string) => Chainable
|
||||
apiResetBoards: () => Chainable
|
||||
apiSkipTour: (userID: string) => Chainable
|
||||
|
||||
uiCreateNewBoard: (title?: string) => Chainable
|
||||
uiAddNewGroup: (name?: string) => Chainable
|
||||
uiAddNewCard: (title?: string, columnIndex?: number) => Chainable
|
||||
|
@ -5,6 +5,7 @@ describe('Card badges', () => {
|
||||
beforeEach(() => {
|
||||
cy.apiInitServer()
|
||||
cy.apiResetBoards()
|
||||
cy.apiGetMe().then((userID) => cy.apiSkipTour(userID))
|
||||
localStorage.setItem('welcomePageViewed', 'true')
|
||||
})
|
||||
|
||||
|
@ -5,6 +5,7 @@ describe('Card URL Property', () => {
|
||||
beforeEach(() => {
|
||||
cy.apiInitServer()
|
||||
cy.apiResetBoards()
|
||||
cy.apiGetMe().then((userID) => cy.apiSkipTour(userID))
|
||||
localStorage.setItem('welcomePageViewed', 'true')
|
||||
})
|
||||
|
||||
|
@ -9,6 +9,7 @@ describe('Create and delete board / card', () => {
|
||||
beforeEach(() => {
|
||||
cy.apiInitServer()
|
||||
cy.apiResetBoards()
|
||||
cy.apiGetMe().then((userID) => cy.apiSkipTour(userID))
|
||||
localStorage.setItem('welcomePageViewed', 'true')
|
||||
})
|
||||
|
||||
@ -59,8 +60,8 @@ describe('Create and delete board / card', () => {
|
||||
// Rename board view
|
||||
cy.log('**Rename board view**')
|
||||
const boardViewTitle = `Test board (${timestamp})`
|
||||
cy.get(".ViewHeader>.Editable[title='Board view']").should('exist')
|
||||
cy.get('.ViewHeader>.Editable').
|
||||
cy.get(".ViewHeader>.viewSelector>.Editable[title='Board view']").should('exist')
|
||||
cy.get('.ViewHeader>.viewSelector>.Editable').
|
||||
clear().
|
||||
type(boardViewTitle).
|
||||
type('{esc}')
|
||||
|
@ -5,6 +5,7 @@ describe('Group board by different properties', () => {
|
||||
beforeEach(() => {
|
||||
cy.apiInitServer()
|
||||
cy.apiResetBoards()
|
||||
cy.apiGetMe().then((userID) => cy.apiSkipTour(userID))
|
||||
localStorage.setItem('welcomePageViewed', 'true')
|
||||
})
|
||||
|
||||
|
@ -27,6 +27,8 @@ describe('Login actions', () => {
|
||||
cy.get('#login-username').type(username)
|
||||
cy.get('#login-password').type(password)
|
||||
cy.get('button').contains('Register').click()
|
||||
cy.location('pathname', {timeout: 10000}).should('include', '/welcome')
|
||||
cy.get('a').contains('No thanks').click()
|
||||
workspaceIsAvailable()
|
||||
|
||||
// Can log out user
|
||||
@ -94,6 +96,8 @@ describe('Login actions', () => {
|
||||
cy.get('#login-username').type('new-user')
|
||||
cy.get('#login-password').type('new-password')
|
||||
cy.get('button').contains('Register').click()
|
||||
cy.location('pathname', {timeout: 10000}).should('include', '/welcome')
|
||||
cy.get('a').contains('No thanks').click()
|
||||
workspaceIsAvailable()
|
||||
})
|
||||
})
|
||||
|
@ -5,6 +5,7 @@ describe('Manage groups', () => {
|
||||
beforeEach(() => {
|
||||
cy.apiInitServer()
|
||||
cy.apiResetBoards()
|
||||
cy.apiGetMe().then((userID) => cy.apiSkipTour(userID))
|
||||
localStorage.setItem('welcomePageViewed', 'true')
|
||||
})
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Board} from '../../src/blocks/board'
|
||||
import {UserConfigPatch} from '../../src/user'
|
||||
|
||||
Cypress.Commands.add('apiRegisterUser', (data: Cypress.UserData, token?: string, failOnError?: boolean) => {
|
||||
return cy.request({
|
||||
@ -81,6 +82,21 @@ Cypress.Commands.add('apiResetBoards', () => {
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('apiSkipTour', (userID: string) => {
|
||||
const body: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
focalboard_welcomePageViewed: '1',
|
||||
},
|
||||
}
|
||||
|
||||
return cy.request({
|
||||
method: 'PUT',
|
||||
url: `/api/v1/users/${encodeURIComponent(userID)}/config`,
|
||||
...headers(),
|
||||
body,
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('apiGetMe', () => {
|
||||
return cy.request({
|
||||
method: 'GET',
|
||||
|
@ -246,7 +246,8 @@
|
||||
"ViewTitle.show-description": "show description",
|
||||
"ViewTitle.untitled-board": "Untitled board",
|
||||
"WelcomePage.Description": "Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view",
|
||||
"WelcomePage.Explore.Button": "Explore",
|
||||
"WelcomePage.Explore.Button": "Take a tour",
|
||||
"WelcomePage.NoThanks.Text": "No thanks, I'll figure it out myself",
|
||||
"WelcomePage.Heading": "Welcome To Boards",
|
||||
"Workspace.editing-board-template": "You're editing a board template.",
|
||||
"calendar.month": "Month",
|
||||
@ -260,5 +261,25 @@
|
||||
"login.log-in-title": "Log in",
|
||||
"login.register-button": "or create an account if you don't have one",
|
||||
"register.login-button": "or log in if you already have an account",
|
||||
"OnboardingTour.OpenACard.Title": "Open a card",
|
||||
"OnboardingTour.OpenACard.Body": "Open a card to explore the powerful ways that Boards can help you organize your work.",
|
||||
"OnboardingTour.AddProperties.Title": "Add properties",
|
||||
"OnboardingTour.AddProperties.Body": "Add various properties to cards to make them more powerful!",
|
||||
"OnboardingTour.AddComments.Title": "Add comments",
|
||||
"OnboardingTour.AddComments.Body": "You can comment on issues, and even @mention your fellow Mattermost users to get their attention.",
|
||||
"OnboardingTour.AddDescription.Title": "Add description",
|
||||
"OnboardingTour.AddDescription.Body": "Add a description to your card so your teammates know what the card is about.",
|
||||
"OnboardingTour.AddView.Title": "Add a new view",
|
||||
"OnboardingTour.AddView.Body": "Go here to create a new view to organise your board using different layouts.",
|
||||
"OnboardingTour.CopyLink.Title": "Copy link",
|
||||
"OnboardingTour.CopyLink.Body": "You can share your cards with teammates by copying the link and pasting it in a channel, Direct Message, or Group Message.",
|
||||
"OnboardingTour.ShareBoard.Title": "Share board",
|
||||
"OnboardingTour.ShareBoard.Body": "You can share your board internally, within your team, or publish it publicly for visibility outside of your organization.",
|
||||
"tutorial_tip.finish_tour": "Done",
|
||||
"tutorial_tip.got_it": "Got it",
|
||||
"tutorial_tip.ok": "Next",
|
||||
"generic.previous": "Previous",
|
||||
"tutorial_tip.seen": "Seen this before?",
|
||||
"tutorial_tip.out": "Opt out of these tips",
|
||||
"register.signup-title": "Sign up for your account"
|
||||
}
|
||||
}
|
||||
|
1245
webapp/package-lock.json
generated
1245
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,7 @@
|
||||
"@fullcalendar/react": "^5.10.0",
|
||||
"@mattermost/compass-icons": "^0.1.10",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"color": "^4.0.0",
|
||||
"draft-js": "^0.11.7",
|
||||
"emoji-mart": "^3.0.1",
|
||||
|
@ -11,8 +11,7 @@ import {IntlProvider} from 'react-intl'
|
||||
import {DndProvider} from 'react-dnd'
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend'
|
||||
import {TouchBackend} from 'react-dnd-touch-backend'
|
||||
|
||||
import {createBrowserHistory} from 'history'
|
||||
import {createBrowserHistory, History} from 'history'
|
||||
|
||||
import TelemetryClient from './telemetry/telemetryClient'
|
||||
|
||||
@ -35,57 +34,32 @@ import {setGlobalError, getGlobalError} from './store/globalError'
|
||||
import {useAppSelector, useAppDispatch} from './store/hooks'
|
||||
import {fetchClientConfig} from './store/clientConfig'
|
||||
|
||||
import {IUser} from './user'
|
||||
import {UserSettings} from './userSettings'
|
||||
import {IUser, UserPropPrefix} from './user'
|
||||
import {UserSettingKey, UserSettings} from './userSettings'
|
||||
|
||||
declare let window: IAppWindow
|
||||
|
||||
const UUID_REGEX = new RegExp(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
type Props = {
|
||||
history?: History<unknown>
|
||||
}
|
||||
|
||||
const App = (props: Props): JSX.Element => {
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
|
||||
const globalError = useAppSelector<string>(getGlobalError)
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const browserHistory: ReturnType<typeof createBrowserHistory> = useMemo(() => {
|
||||
const history = createBrowserHistory({basename: Utils.getFrontendBaseURL()})
|
||||
|
||||
if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
|
||||
window.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) {
|
||||
return
|
||||
}
|
||||
|
||||
const pathName = event.data.message?.pathName
|
||||
if (!pathName || !pathName.startsWith(window.frontendBaseURL)) {
|
||||
return
|
||||
}
|
||||
|
||||
Utils.log(`Navigating Boards to ${pathName}`)
|
||||
history.replace(pathName.replace(window.frontendBaseURL, ''))
|
||||
})
|
||||
}
|
||||
return {
|
||||
...history,
|
||||
push: (path: string, state?: unknown) => {
|
||||
if (Utils.isDesktop() && Utils.isFocalboardPlugin()) {
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'browser-history-push',
|
||||
message: {
|
||||
path: `${window.frontendBaseURL}${path}`,
|
||||
},
|
||||
},
|
||||
window.location.origin,
|
||||
)
|
||||
} else {
|
||||
history.push(path, state)
|
||||
}
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
let browserHistory: History<unknown>
|
||||
if (props.history) {
|
||||
browserHistory = props.history
|
||||
} else {
|
||||
browserHistory = useMemo(() => {
|
||||
return createBrowserHistory({basename: Utils.getFrontendBaseURL()})
|
||||
}, [])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchLanguage())
|
||||
@ -128,7 +102,7 @@ const App = (): JSX.Element => {
|
||||
}
|
||||
|
||||
const continueToWelcomeScreen = () => {
|
||||
return Utils.isFocalboardPlugin() && loggedIn === true && !UserSettings.welcomePageViewed
|
||||
return loggedIn === true && !me?.props[UserPropPrefix + UserSettingKey.WelcomePageViewed]
|
||||
}
|
||||
|
||||
return (
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -364,43 +364,51 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ShareBoardButton"
|
||||
class="shareButtonWrapper"
|
||||
>
|
||||
<button
|
||||
title="Share board"
|
||||
type="button"
|
||||
<div
|
||||
class="ShareBoardButton"
|
||||
>
|
||||
<span>
|
||||
<button
|
||||
title="Share board"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-globe CompassIcon"
|
||||
/>
|
||||
Share
|
||||
</span>
|
||||
</button>
|
||||
<span>
|
||||
Share
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ViewHeader"
|
||||
>
|
||||
<input
|
||||
class="Editable undefined"
|
||||
placeholder="Untitled View"
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
class="viewSelector"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<input
|
||||
class="Editable undefined"
|
||||
placeholder="Untitled View"
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
@ -635,13 +643,13 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -678,13 +686,13 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -721,13 +729,13 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -923,30 +931,37 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shareButtonWrapper"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ViewHeader"
|
||||
>
|
||||
<input
|
||||
class="Editable readonly undefined"
|
||||
placeholder="Untitled View"
|
||||
readonly=""
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
class="viewSelector"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<input
|
||||
class="Editable readonly undefined"
|
||||
placeholder="Untitled View"
|
||||
readonly=""
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
@ -1037,7 +1052,7 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="false"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -1067,7 +1082,7 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="false"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -1097,7 +1112,7 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="false"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -1588,43 +1603,51 @@ exports[`src/components/workspace should match snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ShareBoardButton"
|
||||
class="shareButtonWrapper"
|
||||
>
|
||||
<button
|
||||
title="Share board"
|
||||
type="button"
|
||||
<div
|
||||
class="ShareBoardButton"
|
||||
>
|
||||
<span>
|
||||
<button
|
||||
title="Share board"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-globe CompassIcon"
|
||||
/>
|
||||
Share
|
||||
</span>
|
||||
</button>
|
||||
<span>
|
||||
Share
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ViewHeader"
|
||||
>
|
||||
<input
|
||||
class="Editable undefined"
|
||||
placeholder="Untitled View"
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
class="viewSelector"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<input
|
||||
class="Editable undefined"
|
||||
placeholder="Untitled View"
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
@ -1859,13 +1882,13 @@ exports[`src/components/workspace should match snapshot 1`] = `
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -1902,13 +1925,13 @@ exports[`src/components/workspace should match snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -1945,13 +1968,13 @@ exports[`src/components/workspace should match snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -2147,30 +2170,37 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shareButtonWrapper"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ViewHeader"
|
||||
>
|
||||
<input
|
||||
class="Editable readonly undefined"
|
||||
placeholder="Untitled View"
|
||||
readonly=""
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
class="viewSelector"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<input
|
||||
class="Editable readonly undefined"
|
||||
placeholder="Untitled View"
|
||||
readonly=""
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
@ -2261,7 +2291,7 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="false"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -2291,7 +2321,7 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="false"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -2321,7 +2351,7 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="false"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -2361,3 +2391,284 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/workspace show add new view tooltip 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddViewTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="bottom-start"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add a new view
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Go here to create a new view to organise your board using different layouts.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/workspace show copy link tooltip 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box CopyLinkTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="right-start"
|
||||
data-reference-hidden=""
|
||||
data-state="visible"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 250ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="visible"
|
||||
style="transition-duration: 250ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Copy link
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
You can share your cards with teammates by copying the link and pasting it in a channel, Direct Message, or Group Message.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
title="Previous"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-left icon"
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; top: 0px; transform: translate(0px, 3px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/workspace show open card tooltip 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box OpenCardTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="top"
|
||||
data-reference-hidden=""
|
||||
data-state="visible"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 250ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="visible"
|
||||
style="transition-duration: 250ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Open a card
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Open a card to explore the powerful ways that Boards can help you organize your work.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
/>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
@ -49,25 +49,29 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should ma
|
||||
<div
|
||||
class="ViewHeader"
|
||||
>
|
||||
<input
|
||||
class="Editable undefined"
|
||||
placeholder="Untitled View"
|
||||
spellcheck="true"
|
||||
title="View"
|
||||
value="View"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
class="viewSelector"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<input
|
||||
class="Editable undefined"
|
||||
placeholder="Untitled View"
|
||||
spellcheck="true"
|
||||
title="View"
|
||||
value="View"
|
||||
/>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
@ -354,13 +358,13 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should ma
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
|
@ -103,8 +103,21 @@ describe('components/boardTemplateSelector/boardTemplateSelectorPreview', () =>
|
||||
jest.clearAllMocks()
|
||||
const state = {
|
||||
searchText: {value: ''},
|
||||
users: {me: {id: 'user-id'}},
|
||||
cards: {templates: []},
|
||||
users: {
|
||||
me: {
|
||||
id: 'user-id',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
templates: [],
|
||||
cards: {
|
||||
card_id_1: {title: 'Create a new card'},
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
views: {views: []},
|
||||
contents: {contents: []},
|
||||
comments: {comments: []},
|
||||
|
@ -0,0 +1,362 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/cardDetail/CardDetail should show add comments tour tip 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddCommentTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="right-end"
|
||||
data-reference-hidden=""
|
||||
data-state="visible"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 250ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="visible"
|
||||
style="transition-duration: 250ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add comments
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
You can comment on issues, and even @mention your fellow Mattermost users to get their attention.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--tertiary size--small"
|
||||
title="Previous"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-left icon"
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; top: 0px; transform: translate(0px, 3px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/cardDetail/CardDetail should show add description tour tip 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddDescriptionTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="top-start"
|
||||
data-reference-hidden=""
|
||||
data-state="visible"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 250ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="visible"
|
||||
style="transition-duration: 250ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add description
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Add a description to your card so your teammates know what the card is about.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--tertiary size--small"
|
||||
title="Previous"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-left icon"
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Done
|
||||
</span>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/cardDetail/CardDetail should show add properties tour tip 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddPropertiesTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="right-end"
|
||||
data-reference-hidden=""
|
||||
data-state="visible"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 250ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="visible"
|
||||
style="transition-duration: 250ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add properties
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Add various properties to cards to make them more powerful!
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; top: 0px; transform: translate(0px, 3px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
@ -118,10 +118,13 @@
|
||||
&.add-property {
|
||||
margin: 8px 0 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.4);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
&.content-blocks {
|
||||
position: relative;
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.CardDetailContentsMenu {
|
||||
|
@ -8,14 +8,25 @@ import {act, render} from '@testing-library/react'
|
||||
import configureStore from 'redux-mock-store'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import {mockDOM, wrapIntl} from '../../testUtils'
|
||||
import {mockDOM, wrapDNDIntl, wrapIntl} from '../../testUtils'
|
||||
|
||||
import octoClient from '../../octoClient'
|
||||
|
||||
import {createTextBlock} from '../../blocks/textBlock'
|
||||
|
||||
import CardDetail from './cardDetail'
|
||||
|
||||
global.fetch = FetchMock.fn
|
||||
jest.mock('../../octoClient')
|
||||
|
||||
const mockedOctoClient = mocked(octoClient, true)
|
||||
|
||||
beforeEach(() => {
|
||||
FetchMock.fn.mockReset()
|
||||
@ -61,6 +72,18 @@ describe('components/cardDetail/CardDetail', () => {
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
current: board.id,
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
current: card.id,
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
@ -142,4 +165,290 @@ describe('components/cardDetail/CardDetail', () => {
|
||||
const newCommentSection = container!.querySelectorAll('.newcomment')
|
||||
expect(newCommentSection.length).toBe(0)
|
||||
})
|
||||
|
||||
test('should show add properties tour tip', async () => {
|
||||
const mockStore = configureStore([])
|
||||
|
||||
const welcomeBoard = TestBlockFactory.createBoard()
|
||||
welcomeBoard.title = 'Welcome to Boards!'
|
||||
|
||||
const welcomeCard = TestBlockFactory.createCard(welcomeBoard)
|
||||
welcomeCard.title = 'Create a new card'
|
||||
|
||||
const store = mockStore({
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_welcomePageViewed: '1',
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'card',
|
||||
focalboard_onboardingTourStep: '0',
|
||||
},
|
||||
},
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[welcomeBoard.id]: welcomeBoard,
|
||||
},
|
||||
current: welcomeBoard.id,
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[welcomeCard.id]: welcomeCard,
|
||||
},
|
||||
current: welcomeCard.id,
|
||||
},
|
||||
})
|
||||
|
||||
const onboardingBoard = TestBlockFactory.createBoard()
|
||||
onboardingBoard.title = 'Welcome to Boards!'
|
||||
|
||||
const onboardingCard = TestBlockFactory.createCard(board)
|
||||
onboardingCard.title = 'Create a new card'
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<CardDetail
|
||||
board={onboardingBoard}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
cards={[onboardingCard]}
|
||||
card={onboardingCard}
|
||||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={false}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toBeDefined()
|
||||
expect(container).not.toBeNull()
|
||||
|
||||
const tourTip = document.querySelectorAll('.AddPropertiesTourStep')
|
||||
expect(tourTip.length).toBe(2)
|
||||
expect(tourTip[1]).toMatchSnapshot()
|
||||
|
||||
// moving to next step
|
||||
mockedOctoClient.patchUserConfig.mockResolvedValueOnce({})
|
||||
|
||||
const nextBtn = document!.querySelector('.tipNextButton')
|
||||
expect(nextBtn).toBeDefined()
|
||||
expect(nextBtn).not.toBeNull()
|
||||
await act(async () => {
|
||||
userEvent.click(nextBtn!)
|
||||
})
|
||||
expect(mockedOctoClient.patchUserConfig).toBeCalledWith(
|
||||
'user_id_1',
|
||||
{
|
||||
updatedFields: {
|
||||
focalboard_onboardingTourStep: '1',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('should show add comments tour tip', async () => {
|
||||
const mockStore = configureStore([])
|
||||
|
||||
const welcomeBoard = TestBlockFactory.createBoard()
|
||||
welcomeBoard.title = 'Welcome to Boards!'
|
||||
|
||||
const welcomeCard = TestBlockFactory.createCard(welcomeBoard)
|
||||
welcomeCard.title = 'Create a new card'
|
||||
|
||||
const store = mockStore({
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_welcomePageViewed: '1',
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'card',
|
||||
focalboard_onboardingTourStep: '1',
|
||||
},
|
||||
},
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[welcomeBoard.id]: welcomeBoard,
|
||||
},
|
||||
current: welcomeBoard.id,
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[welcomeCard.id]: welcomeCard,
|
||||
},
|
||||
current: welcomeCard.id,
|
||||
},
|
||||
})
|
||||
|
||||
const onboardingBoard = TestBlockFactory.createBoard()
|
||||
onboardingBoard.title = 'Welcome to Boards!'
|
||||
|
||||
const onboardingCard = TestBlockFactory.createCard(board)
|
||||
onboardingCard.title = 'Create a new card'
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<CardDetail
|
||||
board={onboardingBoard}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
cards={[onboardingCard]}
|
||||
card={onboardingCard}
|
||||
comments={[comment1, comment2]}
|
||||
contents={[]}
|
||||
readonly={false}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toBeDefined()
|
||||
expect(container).not.toBeNull()
|
||||
|
||||
const tourTip = document.querySelectorAll('.AddCommentTourStep')
|
||||
expect(tourTip.length).toBe(2)
|
||||
expect(tourTip[1]).toMatchSnapshot()
|
||||
|
||||
// moving to next step
|
||||
mockedOctoClient.patchUserConfig.mockResolvedValueOnce({})
|
||||
|
||||
const nextBtn = document!.querySelector('.tipNextButton')
|
||||
expect(nextBtn).toBeDefined()
|
||||
expect(nextBtn).not.toBeNull()
|
||||
await act(async () => {
|
||||
userEvent.click(nextBtn!)
|
||||
})
|
||||
expect(mockedOctoClient.patchUserConfig).toBeCalledWith(
|
||||
'user_id_1',
|
||||
{
|
||||
updatedFields: {
|
||||
focalboard_onboardingTourStep: '2',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('should show add description tour tip', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const welcomeBoard = TestBlockFactory.createBoard()
|
||||
welcomeBoard.title = 'Welcome to Boards!'
|
||||
|
||||
const welcomeCard = TestBlockFactory.createCard(welcomeBoard)
|
||||
welcomeCard.title = 'Create a new card'
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_welcomePageViewed: '1',
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'card',
|
||||
focalboard_onboardingTourStep: '2',
|
||||
},
|
||||
},
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[welcomeBoard.id]: welcomeBoard,
|
||||
},
|
||||
current: welcomeBoard.id,
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[welcomeCard.id]: welcomeCard,
|
||||
},
|
||||
current: welcomeCard.id,
|
||||
},
|
||||
}
|
||||
const store = mockStore(state)
|
||||
|
||||
const onboardingBoard = TestBlockFactory.createBoard()
|
||||
onboardingBoard.title = 'Welcome to Boards!'
|
||||
|
||||
const onboardingCard = TestBlockFactory.createCard(board)
|
||||
onboardingCard.title = 'Create a new card'
|
||||
|
||||
const text = createTextBlock()
|
||||
text.title = 'description'
|
||||
text.parentId = onboardingCard.id
|
||||
onboardingCard.fields.contentOrder = [text.id]
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapDNDIntl(
|
||||
<CardDetail
|
||||
board={onboardingBoard}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
cards={[onboardingCard]}
|
||||
card={onboardingCard}
|
||||
comments={[comment1, comment2]}
|
||||
contents={[text]}
|
||||
readonly={false}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toBeDefined()
|
||||
expect(container).not.toBeNull()
|
||||
|
||||
const tourTip = document.querySelectorAll('.AddDescriptionTourStep')
|
||||
expect(tourTip.length).toBe(2)
|
||||
expect(tourTip[1]).toMatchSnapshot()
|
||||
|
||||
// moving to next step
|
||||
mockedOctoClient.patchUserConfig.mockResolvedValueOnce({})
|
||||
|
||||
const nextBtn = document!.querySelector('.tipNextButton')
|
||||
expect(nextBtn).toBeDefined()
|
||||
expect(nextBtn).not.toBeNull()
|
||||
await act(async () => {
|
||||
userEvent.click(nextBtn!)
|
||||
})
|
||||
expect(mockedOctoClient.patchUserConfig).toBeCalledWith(
|
||||
'user_id_1',
|
||||
{
|
||||
updatedFields: {
|
||||
focalboard_onboardingTourStep: '999',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -18,6 +18,9 @@ import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../teleme
|
||||
|
||||
import BlockIconSelector from '../blockIconSelector'
|
||||
|
||||
import {useAppDispatch} from '../../store/hooks'
|
||||
import {setCurrent as setCurrentCard} from '../../store/cards'
|
||||
|
||||
import CommentsList from './commentsList'
|
||||
import {CardDetailProvider} from './cardDetailContext'
|
||||
import CardDetailContents from './cardDetailContents'
|
||||
@ -27,6 +30,9 @@ import useImagePaste from './imagePaste'
|
||||
|
||||
import './cardDetail.scss'
|
||||
|
||||
export const OnboardingBoardTitle = 'Welcome to Boards!'
|
||||
export const OnboardingCardTitle = 'Create a new card'
|
||||
|
||||
type Props = {
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
@ -79,6 +85,11 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
mutator.changeIcon(card.id, card.fields.icon, newIcon)
|
||||
}, [card.id, card.fields.icon])
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
useEffect(() => {
|
||||
dispatch(setCurrentCard(card.id))
|
||||
}, [card.id])
|
||||
|
||||
if (!card) {
|
||||
return null
|
||||
}
|
||||
|
@ -60,6 +60,18 @@ describe('components/cardDetail/cardDetailContents', () => {
|
||||
5: {username: 'g'},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
current: board.id,
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
current: card.id,
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
const wrap = (child: ReactNode): ReactElement => (
|
||||
|
@ -12,6 +12,8 @@ import {useSortableWithGrip} from '../../hooks/sortable'
|
||||
import ContentBlock from '../contentBlock'
|
||||
import {MarkdownEditor} from '../markdownEditor'
|
||||
|
||||
import AddDescriptionTourStep from '../onboardingTour/addDescription/add_description'
|
||||
|
||||
import {dragAndDropRearrange} from './cardDetailContentsUtility'
|
||||
|
||||
export type Position = 'left' | 'right' | 'above' | 'below' | 'aboveRow' | 'belowRow'
|
||||
@ -155,15 +157,17 @@ const CardDetailContents = (props: Props) => {
|
||||
<div className='octo-content'>
|
||||
{contents.map((block, x) =>
|
||||
(
|
||||
<ContentBlockWithDragAndDrop
|
||||
key={x}
|
||||
block={block}
|
||||
x={x}
|
||||
card={card}
|
||||
contents={contents}
|
||||
intl={intl}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
<React.Fragment key={x}>
|
||||
<ContentBlockWithDragAndDrop
|
||||
block={block}
|
||||
x={x}
|
||||
card={card}
|
||||
contents={contents}
|
||||
intl={intl}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
{x === 0 && <AddDescriptionTourStep/>}
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
@ -8,6 +8,9 @@ import {mocked} from 'ts-jest/utils'
|
||||
import '@testing-library/jest-dom'
|
||||
import {createIntl} from 'react-intl'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {PropertyType} from '../../blocks/board'
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
@ -67,19 +70,53 @@ describe('components/cardDetail/CardDetailProperties', () => {
|
||||
|
||||
const cards = [card]
|
||||
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'card',
|
||||
focalboard_onboardingTourStep: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
current: board.id,
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
current: card.id,
|
||||
},
|
||||
}
|
||||
|
||||
const mockStore = configureStore([])
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
function renderComponent() {
|
||||
const component = wrapIntl((
|
||||
<CardDetailProperties
|
||||
board={board!}
|
||||
card={card}
|
||||
cards={[card]}
|
||||
contents={[]}
|
||||
comments={[]}
|
||||
activeView={view}
|
||||
views={views}
|
||||
readonly={false}
|
||||
/>
|
||||
))
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CardDetailProperties
|
||||
board={board!}
|
||||
card={card}
|
||||
cards={[card]}
|
||||
contents={[]}
|
||||
comments={[]}
|
||||
activeView={view}
|
||||
views={views}
|
||||
readonly={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
return render(component)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmation
|
||||
import {sendFlashMessage} from '../flashMessages'
|
||||
import Menu from '../../widgets/menu'
|
||||
import {IDType, Utils} from '../../utils'
|
||||
import AddPropertiesTourStep from '../onboardingTour/addProperties/add_properties'
|
||||
|
||||
type Props = {
|
||||
board: Board
|
||||
@ -197,6 +198,8 @@ const CardDetailProperties = (props: Props) => {
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
<AddPropertiesTourStep/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@
|
||||
}
|
||||
|
||||
.CommentsList__new {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -52,6 +52,18 @@ describe('components/cardDetail/CommentsList', () => {
|
||||
{username: 'username_1'},
|
||||
],
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Board'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
card_id_1: {title: 'Card'},
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
|
@ -14,7 +14,10 @@ import {MarkdownEditor} from '../markdownEditor'
|
||||
import {IUser} from '../../user'
|
||||
import {getMe} from '../../store/users'
|
||||
|
||||
import AddCommentTourStep from '../onboardingTour/addComments/addComments'
|
||||
|
||||
import Comment from './comment'
|
||||
|
||||
import './commentsList.scss'
|
||||
|
||||
type Props = {
|
||||
@ -75,6 +78,8 @@ const CommentsList = (props: Props) => {
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AddCommentTourStep/>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
@ -56,6 +56,12 @@ describe('components/cardDialog', () => {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
current: board.id,
|
||||
},
|
||||
users: {
|
||||
workspaceUsers: {
|
||||
1: {username: 'abc'},
|
||||
|
@ -52,7 +52,7 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
> div:nth-child(2) {
|
||||
padding: 0 0 0 1px;
|
||||
@ -64,4 +64,9 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
.shareButtonWrapper {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,12 @@ describe('components/centerPanel', () => {
|
||||
},
|
||||
searchText: '',
|
||||
users: {
|
||||
me: {},
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: false,
|
||||
},
|
||||
},
|
||||
workspaceUsers: [
|
||||
{username: 'username_1'},
|
||||
],
|
||||
@ -85,6 +90,9 @@ describe('components/centerPanel', () => {
|
||||
},
|
||||
boards: {
|
||||
current: board.id,
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
templates: [card1, card2],
|
||||
|
@ -22,10 +22,24 @@ import {updateView} from '../store/views'
|
||||
import {getVisibleAndHiddenGroups} from '../boardUtils'
|
||||
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
|
||||
|
||||
import ShareBoardButton from './shareBoard/shareBoardButton'
|
||||
|
||||
import './centerPanel.scss'
|
||||
|
||||
import {RootState} from '../store'
|
||||
|
||||
import {
|
||||
getMe,
|
||||
getOnboardingTourCategory,
|
||||
getOnboardingTourStarted,
|
||||
getOnboardingTourStep,
|
||||
patchProps,
|
||||
} from '../store/users'
|
||||
|
||||
import {IUser, UserConfigPatch} from '../user'
|
||||
|
||||
import octoClient from '../octoClient'
|
||||
|
||||
import ShareBoardButton from './shareBoard/shareBoardButton'
|
||||
|
||||
import CardDialog from './cardDialog'
|
||||
import RootPortal from './rootPortal'
|
||||
import TopBar from './topBar'
|
||||
@ -38,6 +52,8 @@ import Table from './table/table'
|
||||
import CalendarFullView from './calendar/fullCalendar'
|
||||
|
||||
import Gallery from './gallery/gallery'
|
||||
import {BoardTourSteps, FINISHED, TOUR_BOARD, TOUR_CARD} from './onboardingTour'
|
||||
import ShareBoardTourStep from './onboardingTour/shareBoard/shareBoard'
|
||||
|
||||
type Props = {
|
||||
clientConfig?: ClientConfig
|
||||
@ -55,6 +71,12 @@ type Props = {
|
||||
shownCardId?: string
|
||||
showCard: (cardId?: string) => void
|
||||
showShared: boolean
|
||||
onboardingTourStarted: boolean
|
||||
onboardingTourCategory: string
|
||||
onboardingTourStep: string
|
||||
me: IUser|null
|
||||
patchProps: (props: Record<string, string>) => void
|
||||
currentCard?: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -112,8 +134,45 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
return true
|
||||
}
|
||||
|
||||
shouldStartBoardsTour(): boolean {
|
||||
const isOnboardingBoard = this.props.board.title === 'Welcome to Boards!'
|
||||
const isTourStarted = this.props.onboardingTourStarted
|
||||
const completedCardsTour = this.props.onboardingTourCategory === TOUR_CARD && this.props.onboardingTourStep === FINISHED.toString()
|
||||
const noCardOpen = !this.props.currentCard
|
||||
|
||||
return isOnboardingBoard && isTourStarted && completedCardsTour && noCardOpen
|
||||
}
|
||||
|
||||
async prepareBoardsTour(): Promise<void> {
|
||||
if (!this.props.me) {
|
||||
return
|
||||
}
|
||||
|
||||
const patch: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
focalboard_tourCategory: TOUR_BOARD,
|
||||
focalboard_onboardingTourStep: BoardTourSteps.ADD_VIEW.toString(),
|
||||
},
|
||||
}
|
||||
|
||||
const patchedProps = await octoClient.patchUserConfig(this.props.me.id, patch)
|
||||
if (patchedProps) {
|
||||
await this.props.patchProps(patchedProps)
|
||||
}
|
||||
}
|
||||
|
||||
async startBoardsTour(): Promise<void> {
|
||||
if (!this.shouldStartBoardsTour()) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.prepareBoardsTour()
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ViewBoard, {board: this.props.board.id, view: this.props.activeView.id, viewType: this.props.activeView.fields.viewType})
|
||||
|
||||
this.startBoardsTour()
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
@ -155,11 +214,16 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
board={board}
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
{!this.props.readonly && this.props.showShared &&
|
||||
<ShareBoardButton
|
||||
boardId={this.props.board.id}
|
||||
/>
|
||||
}
|
||||
<div className='shareButtonWrapper'>
|
||||
{!this.props.readonly && this.props.showShared &&
|
||||
(
|
||||
<ShareBoardButton
|
||||
boardId={this.props.board.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<ShareBoardTourStep/>
|
||||
</div>
|
||||
</div>
|
||||
<ViewHeader
|
||||
board={this.props.board}
|
||||
@ -173,6 +237,7 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
addCardTemplate={this.addCardTemplate}
|
||||
editCardTemplate={this.editCardTemplate}
|
||||
readonly={this.props.readonly}
|
||||
showShared={this.props.showShared}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -412,4 +477,25 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(undefined, {addCard, addTemplate, updateView})(injectIntl(CenterPanel))
|
||||
function mapStateToProps(state: RootState) {
|
||||
const onboardingTourStarted = getOnboardingTourStarted(state)
|
||||
const onboardingTourCategory = getOnboardingTourCategory(state)
|
||||
const onboardingTourStep = getOnboardingTourStep(state)
|
||||
const me = getMe(state)
|
||||
const currentCard = state.cards.current
|
||||
|
||||
return {
|
||||
onboardingTourStarted,
|
||||
onboardingTourCategory,
|
||||
onboardingTourStep,
|
||||
me,
|
||||
currentCard,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
addCard,
|
||||
addTemplate,
|
||||
updateView,
|
||||
patchProps,
|
||||
})(injectIntl(CenterPanel))
|
||||
|
@ -246,6 +246,23 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu imports menu open should ma
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Product tour"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Product tour
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -717,6 +734,23 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu languages menu open should
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Product tour"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Product tour
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -879,6 +913,23 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu settings menu open should m
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Product tour"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Product tour
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {createMemoryHistory} from 'history'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
@ -14,6 +15,8 @@ import GlobalHeader from './globalHeader'
|
||||
|
||||
describe('components/sidebar/GlobalHeader', () => {
|
||||
const mockStore = configureStore([])
|
||||
const history = createMemoryHistory()
|
||||
|
||||
let store = mockStore({})
|
||||
beforeEach(() => {
|
||||
store = mockStore({})
|
||||
@ -21,7 +24,7 @@ describe('components/sidebar/GlobalHeader', () => {
|
||||
test('header menu should match snapshot', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GlobalHeader/>
|
||||
<GlobalHeader history={history}/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {IntlProvider} from 'react-intl'
|
||||
import {History} from 'history'
|
||||
|
||||
import HelpIcon from '../../widgets/icons/help'
|
||||
import store from '../../store'
|
||||
@ -17,7 +18,11 @@ import GlobalHeaderSettingsMenu from './globalHeaderSettingsMenu'
|
||||
|
||||
import './globalHeader.scss'
|
||||
|
||||
const HeaderItems = () => {
|
||||
type HeaderItemProps = {
|
||||
history: History<unknown>
|
||||
}
|
||||
|
||||
const HeaderItems = (props: HeaderItemProps) => {
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
const helpUrl = 'https://www.focalboard.com/fwlink/doc-boards.html?v=' + Constants.versionString
|
||||
|
||||
@ -36,16 +41,20 @@ const HeaderItems = () => {
|
||||
>
|
||||
<HelpIcon/>
|
||||
</a>
|
||||
<GlobalHeaderSettingsMenu/>
|
||||
<GlobalHeaderSettingsMenu history={props.history}/>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const GlobalHeader = (): JSX.Element => {
|
||||
type Props = {
|
||||
history: History<unknown>
|
||||
}
|
||||
|
||||
const GlobalHeader = (props: Props): JSX.Element => {
|
||||
return (
|
||||
<ReduxProvider store={store}>
|
||||
<HeaderItems/>
|
||||
<HeaderItems history={props.history}/>
|
||||
</ReduxProvider>
|
||||
)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {createMemoryHistory} from 'history'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
@ -22,14 +23,21 @@ const mockedTelemetry = mocked(TelemetryClient, true)
|
||||
|
||||
describe('components/sidebar/GlobalHeaderSettingsMenu', () => {
|
||||
const mockStore = configureStore([])
|
||||
const history = createMemoryHistory()
|
||||
let store = mockStore({})
|
||||
beforeEach(() => {
|
||||
store = mockStore({})
|
||||
store = mockStore({
|
||||
users: {
|
||||
me: {
|
||||
id: 'user-id',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
test('settings menu closed should match snapshot', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GlobalHeaderSettingsMenu/>
|
||||
<GlobalHeaderSettingsMenu history={history}/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
@ -40,7 +48,7 @@ describe('components/sidebar/GlobalHeaderSettingsMenu', () => {
|
||||
test('settings menu open should match snapshot', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GlobalHeaderSettingsMenu/>
|
||||
<GlobalHeaderSettingsMenu history={history}/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
@ -52,7 +60,7 @@ describe('components/sidebar/GlobalHeaderSettingsMenu', () => {
|
||||
test('languages menu open should match snapshot', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GlobalHeaderSettingsMenu/>
|
||||
<GlobalHeaderSettingsMenu history={history}/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
@ -66,7 +74,7 @@ describe('components/sidebar/GlobalHeaderSettingsMenu', () => {
|
||||
window.open = jest.fn()
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<GlobalHeaderSettingsMenu/>
|
||||
<GlobalHeaderSettingsMenu history={history}/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
|
@ -2,12 +2,16 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
import {History} from 'history'
|
||||
|
||||
import {Archiver} from '../../archiver'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {useAppDispatch} from '../../store/hooks'
|
||||
import {useAppDispatch, useAppSelector} from '../../store/hooks'
|
||||
import {storeLanguage} from '../../store/language'
|
||||
import {patchProps, getMe} from '../../store/users'
|
||||
import {IUser, UserConfigPatch, UserPropPrefix} from '../../user'
|
||||
import octoClient from '../../octoClient'
|
||||
import {UserSettings} from '../../userSettings'
|
||||
import CheckIcon from '../../widgets/icons/check'
|
||||
import SettingsIcon from '../../widgets/icons/settings'
|
||||
@ -17,8 +21,13 @@ import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../teleme
|
||||
|
||||
import './globalHeaderSettingsMenu.scss'
|
||||
|
||||
const GlobalHeaderSettingsMenu = () => {
|
||||
type Props = {
|
||||
history: History<unknown>
|
||||
}
|
||||
|
||||
const GlobalHeaderSettingsMenu = (props: Props) => {
|
||||
const intl = useIntl()
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [randomIcons, setRandomIcons] = useState(UserSettings.prefillRandomIcons)
|
||||
@ -92,6 +101,35 @@ const GlobalHeaderSettingsMenu = () => {
|
||||
isOn={randomIcons}
|
||||
onClick={async () => toggleRandomIcons()}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='product-tour'
|
||||
name={intl.formatMessage({id: 'Sidebar.product-tour', defaultMessage: 'Product tour'})}
|
||||
onClick={async () => {
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.StartTour)
|
||||
|
||||
if (!me) {
|
||||
return
|
||||
}
|
||||
|
||||
const patch: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
[UserPropPrefix + 'onboardingTourStep']: '0',
|
||||
[UserPropPrefix + 'tourCategory']: 'onboarding',
|
||||
},
|
||||
}
|
||||
|
||||
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
|
||||
if (patchedProps) {
|
||||
await dispatch(patchProps(patchedProps))
|
||||
}
|
||||
|
||||
const onboardingData = await octoClient.prepareOnboarding()
|
||||
|
||||
const newPath = `/workspace/${onboardingData?.workspaceID}/${onboardingData?.boardID}`
|
||||
|
||||
props.history.push(newPath)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
|
@ -144,13 +144,13 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -177,13 +177,13 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -221,13 +221,13 @@ exports[`src/component/kanban/kanban return kanban and change title on KanbanCol
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -589,13 +589,13 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -622,13 +622,13 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -666,13 +666,13 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -875,13 +875,13 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -908,13 +908,13 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -952,13 +952,13 @@ exports[`src/component/kanban/kanban should match snapshot 1`] = `
|
||||
class="octo-board-column"
|
||||
>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
|
@ -3,13 +3,13 @@
|
||||
exports[`src/components/kanban/kanbanCard return kanbanCard and click on copy link menu 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -136,13 +136,13 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on copy li
|
||||
exports[`src/components/kanban/kanbanCard return kanbanCard and click on delete menu 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -269,13 +269,13 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on delete
|
||||
exports[`src/components/kanban/kanbanCard return kanbanCard and click on duplicate menu 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -402,13 +402,13 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on duplica
|
||||
exports[`src/components/kanban/kanbanCard should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
class="MenuWrapper optionsMenu "
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
@ -448,7 +448,7 @@ exports[`src/components/kanban/kanbanCard should match snapshot 1`] = `
|
||||
exports[`src/components/kanban/kanbanCard should match snapshot with readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="KanbanCard"
|
||||
class="KanbanCard false"
|
||||
draggable="false"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
|
@ -5,6 +5,7 @@ import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {MemoryRouter} from 'react-router-dom'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
@ -66,6 +67,12 @@ describe('src/component/kanban/kanban', () => {
|
||||
}
|
||||
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {},
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: [card1, card2, card3],
|
||||
},
|
||||
@ -110,7 +117,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('do not return a kanban with groupByProperty undefined', () => {
|
||||
@ -143,7 +150,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
|
||||
expect(mockedUtils.assertFailure).toBeCalled()
|
||||
expect(container).toMatchSnapshot()
|
||||
@ -178,7 +185,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
|
||||
const cardsElement = container.querySelectorAll('.KanbanCard')
|
||||
expect(cardsElement).not.toBeNull()
|
||||
@ -223,7 +230,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
|
||||
const cardsElement = container.querySelectorAll('.KanbanCard')
|
||||
expect(cardsElement).not.toBeNull()
|
||||
@ -268,7 +275,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
|
||||
const cardsElement = container.querySelectorAll('.KanbanCard')
|
||||
expect(cardsElement).not.toBeNull()
|
||||
@ -314,7 +321,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
const allButtonsNew = screen.getAllByRole('button', {name: '+ New'})
|
||||
expect(allButtonsNew).not.toBeNull()
|
||||
userEvent.click(allButtonsNew[0])
|
||||
@ -351,7 +358,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
const buttonKanbanCalculation = screen.getByRole('button', {name: '2'})
|
||||
expect(buttonKanbanCalculation).toBeDefined()
|
||||
userEvent.click(buttonKanbanCalculation!)
|
||||
@ -388,7 +395,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
|
||||
const inputTitle = screen.getByRole('textbox', {name: optionQ1.value})
|
||||
expect(inputTitle).toBeDefined()
|
||||
@ -432,7 +439,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
showCard={jest.fn()}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
const buttonAddGroup = screen.getByRole('button', {name: '+ Add a group'})
|
||||
expect(buttonAddGroup).toBeDefined()
|
||||
userEvent.click(buttonAddGroup)
|
||||
|
@ -33,6 +33,10 @@
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
&.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.octo-tooltip {
|
||||
|
@ -3,6 +3,7 @@
|
||||
import React from 'react'
|
||||
import {render, screen, within} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import {MemoryRouter} from 'react-router-dom'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
@ -53,6 +54,12 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
beforeEach(jest.clearAllMocks)
|
||||
@ -71,7 +78,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
isManualSort={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('should match snapshot with readonly', () => {
|
||||
@ -89,7 +96,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
isManualSort={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('return kanbanCard and click on delete menu ', () => {
|
||||
@ -107,7 +114,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
isManualSort={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
|
||||
const {container} = result
|
||||
|
||||
@ -143,7 +150,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
isManualSort={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
const elementMenuWrapper = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
expect(elementMenuWrapper).not.toBeNull()
|
||||
userEvent.click(elementMenuWrapper)
|
||||
@ -169,7 +176,7 @@ describe('src/components/kanban/kanbanCard', () => {
|
||||
isManualSort={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
), {wrapper: MemoryRouter})
|
||||
const elementMenuWrapper = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
expect(elementMenuWrapper).not.toBeNull()
|
||||
userEvent.click(elementMenuWrapper)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState} from 'react'
|
||||
import {useRouteMatch} from 'react-router-dom'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
@ -26,6 +27,10 @@ import PropertyValueElement from '../propertyValueElement'
|
||||
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
|
||||
import './kanbanCard.scss'
|
||||
import CardBadges from '../cardBadges'
|
||||
import OpenCardTourStep from '../onboardingTour/openCard/open_card'
|
||||
import CopyLinkTourStep from '../onboardingTour/copyLink/copy_link'
|
||||
|
||||
export const OnboardingCardClassName = 'onboardingCard'
|
||||
|
||||
type Props = {
|
||||
card: Card
|
||||
@ -33,7 +38,7 @@ type Props = {
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
isSelected: boolean
|
||||
visibleBadges: boolean
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onClick?: (e: React.MouseEvent) => void
|
||||
readonly: boolean
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
showCard: (cardId?: string) => void
|
||||
@ -45,6 +50,7 @@ const KanbanCard = (props: Props) => {
|
||||
const intl = useIntl()
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly, props.onDrop)
|
||||
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
|
||||
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string}>()
|
||||
let className = props.isSelected ? 'KanbanCard selected' : 'KanbanCard'
|
||||
if (props.isManualSort && isOver) {
|
||||
className += ' dragover'
|
||||
@ -81,18 +87,27 @@ const KanbanCard = (props: Props) => {
|
||||
setShowConfirmationDialogBox(true)
|
||||
}
|
||||
|
||||
const isOnboardingCard = card.title === 'Create a new card'
|
||||
const showOnboarding = isOnboardingCard && !match.params.cardId && !board.fields.isTemplate && Utils.isFocalboardPlugin()
|
||||
|
||||
const handleOnClick = async (e: React.MouseEvent) => {
|
||||
if (props.onClick) {
|
||||
props.onClick(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={props.readonly ? () => null : cardRef}
|
||||
className={className}
|
||||
className={`${className} ${showOnboarding && OnboardingCardClassName}`}
|
||||
draggable={!props.readonly}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
onClick={props.onClick}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{!props.readonly &&
|
||||
<MenuWrapper
|
||||
className='optionsMenu'
|
||||
className={`optionsMenu ${showOnboarding ? 'show' : ''}`}
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
@ -168,6 +183,8 @@ const KanbanCard = (props: Props) => {
|
||||
</Tooltip>
|
||||
))}
|
||||
{props.visibleBadges && <CardBadges card={card}/>}
|
||||
{showOnboarding && !match.params.cardId && <OpenCardTourStep/>}
|
||||
{showOnboarding && !match.params.cardId && <CopyLinkTourStep/>}
|
||||
</div>
|
||||
|
||||
{showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>}
|
||||
|
@ -0,0 +1,139 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddCommentTourStep after hover 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddCommentTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="right-end"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add comments
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
You can comment on issues, and even @mention your fellow Mattermost users to get their attention.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--tertiary size--small"
|
||||
title="Previous"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-left icon"
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; top: 0px; transform: translate(0px, 3px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddCommentTourStep before hover 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="tutorial-tour-tip__pulsating-dot-ctr AddCommentTourStep"
|
||||
>
|
||||
<span
|
||||
class="pulsating_dot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {wrapIntl} from '../../../testUtils'
|
||||
|
||||
import AddCommentTourStep from './addComments'
|
||||
|
||||
describe('components/onboardingTour/addComments/AddCommentTourStep', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'card',
|
||||
focalboard_onboardingTourStep: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Welcome to Boards!'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
card_id_1: {title: 'Create a new card'},
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
test('before hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddCommentTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('after hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddCommentTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
render(component)
|
||||
const elements = document.querySelectorAll('.AddCommentTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {useMeasurePunchouts} from '../../tutorial_tour_tip/hooks'
|
||||
|
||||
import './add_comments.scss'
|
||||
import {Utils} from '../../../utils'
|
||||
import addComment from '../../../../static/comment.gif'
|
||||
|
||||
import {CardTourSteps, TOUR_CARD} from '../index'
|
||||
import TourTipRenderer from '../tourTipRenderer/tourTipRenderer'
|
||||
|
||||
const AddCommentTourStep = (): JSX.Element | null => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddComments.Title'
|
||||
defaultMessage='Add comments'
|
||||
/>
|
||||
)
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddComments.Body'
|
||||
defaultMessage='You can comment on issues, and even @mention your fellow Mattermost users to get their attention.'
|
||||
/>
|
||||
)
|
||||
|
||||
const punchout = useMeasurePunchouts(['.CommentsList__new'], [])
|
||||
|
||||
return (
|
||||
<TourTipRenderer
|
||||
key='AddCommentTourStep'
|
||||
requireCard={true}
|
||||
category={TOUR_CARD}
|
||||
step={CardTourSteps.ADD_COMMENTS}
|
||||
screen={screen}
|
||||
title={title}
|
||||
punchout={punchout}
|
||||
classname='AddCommentTourStep'
|
||||
telemetryTag='tourPoint2b'
|
||||
placement={'right-end'}
|
||||
imageURL={Utils.buildURL(addComment, true)}
|
||||
hideBackdrop={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddCommentTourStep
|
@ -0,0 +1,14 @@
|
||||
.AddCommentTourStep {
|
||||
&.tutorial-tour-tip__pulsating-dot-ctr {
|
||||
left: 170px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
.tippy-arrow {
|
||||
top: -21px !important;
|
||||
}
|
||||
|
||||
&.tippy-box.tutorial-tour-tip__box {
|
||||
top: 24px;
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddDescriptionTourStep after hover 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddDescriptionTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="top-start"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add description
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Add a description to your card so your teammates know what the card is about.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--tertiary size--small"
|
||||
title="Previous"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-left icon"
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Done
|
||||
</span>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddDescriptionTourStep before hover 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="tutorial-tour-tip__pulsating-dot-ctr AddDescriptionTourStep"
|
||||
>
|
||||
<span
|
||||
class="pulsating_dot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {wrapIntl} from '../../../testUtils'
|
||||
|
||||
import AddDescriptionTourStep from './add_description'
|
||||
|
||||
describe('components/onboardingTour/addComments/AddDescriptionTourStep', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'card',
|
||||
focalboard_onboardingTourStep: '2',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Welcome to Boards!'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
card_id_1: {title: 'Create a new card'},
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
test('before hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddDescriptionTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('after hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddDescriptionTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
render(component)
|
||||
const elements = document.querySelectorAll('.AddDescriptionTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -0,0 +1,14 @@
|
||||
.AddDescriptionTourStep {
|
||||
&.tutorial-tour-tip__pulsating-dot-ctr {
|
||||
top: -10px;
|
||||
left: calc(120px + 20%);
|
||||
}
|
||||
|
||||
.tippy-arrow {
|
||||
left: 21px !important;
|
||||
}
|
||||
|
||||
&.tippy-box.tutorial-tour-tip__box {
|
||||
left: -21px;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {useMeasurePunchouts} from '../../tutorial_tour_tip/hooks'
|
||||
|
||||
import './add_description.scss'
|
||||
import {Utils} from '../../../utils'
|
||||
import addDescription from '../../../../static/addDescription.png'
|
||||
|
||||
import {CardTourSteps, TOUR_CARD} from '../index'
|
||||
import TourTipRenderer from '../tourTipRenderer/tourTipRenderer'
|
||||
|
||||
const AddDescriptionTourStep = (): JSX.Element | null => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddDescription.Title'
|
||||
defaultMessage='Add description'
|
||||
/>
|
||||
)
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddDescription.Body'
|
||||
defaultMessage='Add a description to your card so your teammates know what the card is about.'
|
||||
/>
|
||||
)
|
||||
|
||||
const punchout = useMeasurePunchouts(['.octo-content div:nth-child(1)'], [])
|
||||
|
||||
return (
|
||||
<TourTipRenderer
|
||||
key='AddDescriptionTourStep'
|
||||
requireCard={true}
|
||||
category={TOUR_CARD}
|
||||
step={CardTourSteps.ADD_DESCRIPTION}
|
||||
screen={screen}
|
||||
title={title}
|
||||
punchout={punchout}
|
||||
classname='AddDescriptionTourStep'
|
||||
telemetryTag='tourPoint2c'
|
||||
placement={'top-start'}
|
||||
imageURL={Utils.buildURL(addDescription, true)}
|
||||
hideBackdrop={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddDescriptionTourStep
|
@ -0,0 +1,127 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddPropertiesTourStep after hover 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddPropertiesTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="right-end"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add properties
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Add various properties to cards to make them more powerful!
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; top: 0px; transform: translate(0px, 3px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddPropertiesTourStep before hover 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="tutorial-tour-tip__pulsating-dot-ctr AddPropertiesTourStep"
|
||||
>
|
||||
<span
|
||||
class="pulsating_dot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {wrapIntl} from '../../../testUtils'
|
||||
|
||||
import AddPropertiesTourStep from './add_properties'
|
||||
|
||||
describe('components/onboardingTour/addComments/AddPropertiesTourStep', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'card',
|
||||
focalboard_onboardingTourStep: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Welcome to Boards!'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
card_id_1: {title: 'Create a new card'},
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
test('before hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddPropertiesTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('after hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddPropertiesTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
render(component)
|
||||
const elements = document.querySelectorAll('.AddPropertiesTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -0,0 +1,14 @@
|
||||
.AddPropertiesTourStep {
|
||||
&.tutorial-tour-tip__pulsating-dot-ctr {
|
||||
left: 100%;
|
||||
top: calc(50% - 6px);
|
||||
}
|
||||
|
||||
.tippy-arrow {
|
||||
top: -21px !important;
|
||||
}
|
||||
|
||||
&.tippy-box.tutorial-tour-tip__box {
|
||||
top: 24px;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {useMeasurePunchouts} from '../../tutorial_tour_tip/hooks'
|
||||
|
||||
import './add_properties.scss'
|
||||
import {Utils} from '../../../utils'
|
||||
import addProperty from '../../../../static/addProperty.gif'
|
||||
|
||||
import {CardTourSteps, TOUR_CARD} from '../index'
|
||||
import TourTipRenderer from '../tourTipRenderer/tourTipRenderer'
|
||||
|
||||
const AddPropertiesTourStep = (): JSX.Element | null => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddProperties.Title'
|
||||
defaultMessage='Add properties'
|
||||
/>
|
||||
)
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddProperties.Body'
|
||||
defaultMessage='Add various properties to cards to make them more powerful!'
|
||||
/>
|
||||
)
|
||||
|
||||
const punchout = useMeasurePunchouts(['.octo-propertyname.add-property'], [])
|
||||
|
||||
return (
|
||||
<TourTipRenderer
|
||||
key='AddPropertiesTourStep'
|
||||
requireCard={true}
|
||||
category={TOUR_CARD}
|
||||
step={CardTourSteps.ADD_PROPERTIES}
|
||||
screen={screen}
|
||||
title={title}
|
||||
punchout={punchout}
|
||||
classname='AddPropertiesTourStep'
|
||||
telemetryTag='tourPoint2a'
|
||||
placement={'right-end'}
|
||||
imageURL={Utils.buildURL(addProperty, true)}
|
||||
hideBackdrop={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPropertiesTourStep
|
@ -0,0 +1,127 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddViewTourStep after hover 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box AddViewTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="bottom-start"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Add a new view
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Go here to create a new view to organise your board using different layouts.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/onboardingTour/addComments/AddViewTourStep before hover 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="tutorial-tour-tip__pulsating-dot-ctr AddViewTourStep"
|
||||
>
|
||||
<span
|
||||
class="pulsating_dot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {wrapIntl} from '../../../testUtils'
|
||||
|
||||
import AddViewTourStep from './add_view'
|
||||
|
||||
describe('components/onboardingTour/addComments/AddViewTourStep', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'board',
|
||||
focalboard_onboardingTourStep: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Welcome to Boards!'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
test('before hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddViewTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('after hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<AddViewTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
render(component)
|
||||
const elements = document.querySelectorAll('.AddViewTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
})
|
14
webapp/src/components/onboardingTour/addView/add_view.scss
Normal file
14
webapp/src/components/onboardingTour/addView/add_view.scss
Normal file
@ -0,0 +1,14 @@
|
||||
.AddViewTourStep {
|
||||
&.tutorial-tour-tip__pulsating-dot-ctr {
|
||||
left: 91%;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.tippy-arrow {
|
||||
left: 21px !important;
|
||||
}
|
||||
|
||||
&.tippy-box.tutorial-tour-tip__box {
|
||||
right: 22px;
|
||||
}
|
||||
}
|
50
webapp/src/components/onboardingTour/addView/add_view.tsx
Normal file
50
webapp/src/components/onboardingTour/addView/add_view.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {useMeasurePunchouts} from '../../tutorial_tour_tip/hooks'
|
||||
|
||||
import './add_view.scss'
|
||||
import {Utils} from '../../../utils'
|
||||
import changeViews from '../../../../static/changeViews.gif'
|
||||
|
||||
import {BoardTourSteps, TOUR_BOARD} from '../index'
|
||||
import TourTipRenderer from '../tourTipRenderer/tourTipRenderer'
|
||||
|
||||
const AddViewTourStep = (): JSX.Element => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddView.Title'
|
||||
defaultMessage='Add a new view'
|
||||
/>
|
||||
)
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.AddView.Body'
|
||||
defaultMessage='Go here to create a new view to organise your board using different layouts.'
|
||||
/>
|
||||
)
|
||||
|
||||
const punchout = useMeasurePunchouts(['.viewSelector'], [])
|
||||
|
||||
return (
|
||||
<TourTipRenderer
|
||||
key='AddViewTourStep'
|
||||
requireCard={false}
|
||||
category={TOUR_BOARD}
|
||||
step={BoardTourSteps.ADD_VIEW}
|
||||
screen={screen}
|
||||
title={title}
|
||||
punchout={punchout}
|
||||
classname='AddViewTourStep'
|
||||
telemetryTag='tourPoint3a'
|
||||
placement={'bottom-start'}
|
||||
imageURL={Utils.buildURL(changeViews, true)}
|
||||
hideBackdrop={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddViewTourStep
|
@ -0,0 +1,139 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/onboardingTour/addComments/CopyLinkTourStep after hover 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box CopyLinkTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="right-start"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Copy link
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
You can share your cards with teammates by copying the link and pasting it in a channel, Direct Message, or Group Message.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--tertiary size--small"
|
||||
title="Previous"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-left icon"
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-right icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; top: 0px; transform: translate(0px, 3px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/onboardingTour/addComments/CopyLinkTourStep before hover 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="tutorial-tour-tip__pulsating-dot-ctr CopyLinkTourStep"
|
||||
>
|
||||
<span
|
||||
class="pulsating_dot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {wrapIntl} from '../../../testUtils'
|
||||
|
||||
import CopyLinkTourStep from './copy_link'
|
||||
|
||||
describe('components/onboardingTour/addComments/CopyLinkTourStep', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'board',
|
||||
focalboard_onboardingTourStep: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Welcome to Boards!'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
test('before hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CopyLinkTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('after hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CopyLinkTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
render(component)
|
||||
const elements = document.querySelectorAll('.CopyLinkTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
})
|
14
webapp/src/components/onboardingTour/copyLink/copy_link.scss
Normal file
14
webapp/src/components/onboardingTour/copyLink/copy_link.scss
Normal file
@ -0,0 +1,14 @@
|
||||
.CopyLinkTourStep {
|
||||
&.tutorial-tour-tip__pulsating-dot-ctr {
|
||||
left: calc(100% - 6px);
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
.tippy-arrow {
|
||||
top: 21px !important;
|
||||
}
|
||||
|
||||
&.tippy-box.tutorial-tour-tip__box {
|
||||
top: -24px;
|
||||
}
|
||||
}
|
51
webapp/src/components/onboardingTour/copyLink/copy_link.tsx
Normal file
51
webapp/src/components/onboardingTour/copyLink/copy_link.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {useMeasurePunchouts} from '../../tutorial_tour_tip/hooks'
|
||||
|
||||
import './copy_link.scss'
|
||||
import {Utils} from '../../../utils'
|
||||
import copyLink from '../../../../static/copyLink.gif'
|
||||
|
||||
import {BoardTourSteps, TOUR_BOARD} from '../index'
|
||||
import {OnboardingCardClassName} from '../../kanban/kanbanCard'
|
||||
import TourTipRenderer from '../tourTipRenderer/tourTipRenderer'
|
||||
|
||||
const CopyLinkTourStep = (): JSX.Element | null => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.CopyLink.Title'
|
||||
defaultMessage='Copy link'
|
||||
/>
|
||||
)
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.CopyLink.Body'
|
||||
defaultMessage='You can share your cards with teammates by copying the link and pasting it in a channel, Direct Message, or Group Message.'
|
||||
/>
|
||||
)
|
||||
|
||||
const punchout = useMeasurePunchouts([`.${OnboardingCardClassName} .optionsMenu`], [])
|
||||
|
||||
return (
|
||||
<TourTipRenderer
|
||||
key='CopyLinkTourStep'
|
||||
requireCard={false}
|
||||
category={TOUR_BOARD}
|
||||
step={BoardTourSteps.COPY_LINK}
|
||||
screen={screen}
|
||||
title={title}
|
||||
punchout={punchout}
|
||||
classname='CopyLinkTourStep'
|
||||
telemetryTag='tourPoint3b'
|
||||
placement={'right-start'}
|
||||
imageURL={Utils.buildURL(copyLink, true)}
|
||||
hideBackdrop={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyLinkTourStep
|
35
webapp/src/components/onboardingTour/index.ts
Normal file
35
webapp/src/components/onboardingTour/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
export const BaseTourSteps = {
|
||||
OPEN_A_CARD: 0,
|
||||
}
|
||||
|
||||
export const CardTourSteps = {
|
||||
ADD_PROPERTIES: 0,
|
||||
ADD_COMMENTS: 1,
|
||||
ADD_DESCRIPTION: 2,
|
||||
}
|
||||
|
||||
export const BoardTourSteps: {[key: string]: number} = {
|
||||
ADD_VIEW: 0,
|
||||
COPY_LINK: 1,
|
||||
SHARE_BOARD: 2,
|
||||
}
|
||||
|
||||
export const FINISHED = 999
|
||||
|
||||
export const TOUR_BASE = 'onboarding'
|
||||
export const TOUR_CARD = 'card'
|
||||
export const TOUR_BOARD = 'board'
|
||||
|
||||
export const TOUR_ORDER = [
|
||||
TOUR_BASE,
|
||||
TOUR_CARD,
|
||||
TOUR_BOARD,
|
||||
]
|
||||
|
||||
export const TourCategoriesMapToSteps: Record<string, Record<string, number>> = {
|
||||
[TOUR_BASE]: BaseTourSteps,
|
||||
[TOUR_CARD]: CardTourSteps,
|
||||
[TOUR_BOARD]: BoardTourSteps,
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/onboardingTour/addComments/OpenCardTourStep after hover 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box OpenCardTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="top"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Open a card
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
Open a card to explore the powerful ways that Boards can help you organize your work.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
/>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/onboardingTour/addComments/OpenCardTourStep before hover 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="tutorial-tour-tip__pulsating-dot-ctr OpenCardTourStep"
|
||||
>
|
||||
<span
|
||||
class="pulsating_dot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {wrapIntl} from '../../../testUtils'
|
||||
|
||||
import OpenCardTourStep from './open_card'
|
||||
|
||||
describe('components/onboardingTour/addComments/OpenCardTourStep', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'onboarding',
|
||||
focalboard_onboardingTourStep: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Welcome to Boards!'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
card_id_1: {title: 'Create a new card'},
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
test('before hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<OpenCardTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('after hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<OpenCardTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
render(component)
|
||||
const elements = document.querySelectorAll('.OpenCardTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
})
|
10
webapp/src/components/onboardingTour/openCard/open_card.scss
Normal file
10
webapp/src/components/onboardingTour/openCard/open_card.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.OpenCardTourStep {
|
||||
&.tutorial-tour-tip__pulsating-dot-ctr {
|
||||
left: 80%;
|
||||
top: calc(100% - 6px);
|
||||
}
|
||||
|
||||
.tutorial-tour-tip__overlay {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
52
webapp/src/components/onboardingTour/openCard/open_card.tsx
Normal file
52
webapp/src/components/onboardingTour/openCard/open_card.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {bottom} from '@popperjs/core'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {useMeasurePunchouts} from '../../tutorial_tour_tip/hooks'
|
||||
|
||||
import {BaseTourSteps, TOUR_BASE} from '../index'
|
||||
|
||||
import './open_card.scss'
|
||||
import {OnboardingCardClassName} from '../../kanban/kanbanCard'
|
||||
import TourTipRenderer from '../tourTipRenderer/tourTipRenderer'
|
||||
|
||||
const OpenCardTourStep = (): JSX.Element | null => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.OpenACard.Title'
|
||||
defaultMessage='Open a card'
|
||||
/>
|
||||
)
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.OpenACard.Body'
|
||||
defaultMessage='Open a card to explore the powerful ways that Boards can help you organize your work.'
|
||||
/>
|
||||
)
|
||||
|
||||
const punchout = useMeasurePunchouts([`.${OnboardingCardClassName}`], [])
|
||||
|
||||
return (
|
||||
<TourTipRenderer
|
||||
key='OpenCardTourStep'
|
||||
requireCard={false}
|
||||
category={TOUR_BASE}
|
||||
step={BaseTourSteps.OPEN_A_CARD}
|
||||
screen={screen}
|
||||
title={title}
|
||||
punchout={punchout}
|
||||
classname='OpenCardTourStep'
|
||||
telemetryTag='tourPoint1'
|
||||
placement={bottom}
|
||||
singleTip={true}
|
||||
hideNavButtons={true}
|
||||
hideBackdrop={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpenCardTourStep
|
@ -0,0 +1,137 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/onboardingTour/addComments/ShareBoardTourStep after hover 1`] = `
|
||||
<div
|
||||
class="tippy-box tutorial-tour-tip__box ShareBoardTourStep"
|
||||
data-animation="scale-subtle"
|
||||
data-escaped=""
|
||||
data-placement="bottom-end"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="tutorial-tour-tip__header"
|
||||
>
|
||||
<h4
|
||||
class="tutorial-tour-tip__header__title"
|
||||
>
|
||||
Share board
|
||||
</h4>
|
||||
<button
|
||||
class="IconButton tutorial-tour-tip__header__close size--small"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__body"
|
||||
>
|
||||
You can share your board internally, within your team, or publish it publicly for visibility outside of your organization.
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__image"
|
||||
>
|
||||
<img
|
||||
alt="tutorial tour tip product image"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__footer-buttons"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circles-ctr"
|
||||
>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="0"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle"
|
||||
data-screen="1"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__circular-ring tutorial-tour-tip__circular-ring-active"
|
||||
>
|
||||
<a
|
||||
class="tutorial-tour-tip__circle active"
|
||||
data-screen="2"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tutorial-tour-tip__btn-ctr"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--tertiary size--small"
|
||||
title="Previous"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-left icon"
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="Button filled size--small tipNextButton"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Done
|
||||
</span>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tippy-arrow"
|
||||
style="position: absolute; left: 0px; transform: translate(3px, 0px);"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/onboardingTour/addComments/ShareBoardTourStep before hover 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="tutorial-tour-tip__pulsating-dot-ctr ShareBoardTourStep"
|
||||
>
|
||||
<span
|
||||
class="pulsating_dot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,14 @@
|
||||
.ShareBoardTourStep {
|
||||
&.tutorial-tour-tip__pulsating-dot-ctr {
|
||||
left: calc(80% - 16px);
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.tippy-arrow {
|
||||
left: -20px !important;
|
||||
}
|
||||
|
||||
&.tippy-box.tutorial-tour-tip__box {
|
||||
right: -22px;
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {wrapIntl} from '../../../testUtils'
|
||||
|
||||
import ShareBoardTourStep from './shareBoard'
|
||||
|
||||
describe('components/onboardingTour/addComments/ShareBoardTourStep', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {
|
||||
focalboard_onboardingTourStarted: true,
|
||||
focalboard_tourCategory: 'board',
|
||||
focalboard_onboardingTourStep: '2',
|
||||
},
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
board_id_1: {title: 'Welcome to Boards!'},
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
card_id_1: {title: 'Create a new card'},
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(state)
|
||||
})
|
||||
|
||||
test('before hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<ShareBoardTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('after hover', () => {
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<ShareBoardTourStep/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
render(component)
|
||||
const elements = document.querySelectorAll('.ShareBoardTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {useMeasurePunchouts} from '../../tutorial_tour_tip/hooks'
|
||||
|
||||
import './shareBoard.scss'
|
||||
import {Utils} from '../../../utils'
|
||||
import shareBoard from '../../../../static/share.gif'
|
||||
|
||||
import {BoardTourSteps, TOUR_BOARD} from '../index'
|
||||
import TourTipRenderer from '../tourTipRenderer/tourTipRenderer'
|
||||
|
||||
const ShareBoardTourStep = (): JSX.Element | null => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.ShareBoard.Title'
|
||||
defaultMessage='Share board'
|
||||
/>
|
||||
)
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='OnboardingTour.ShareBoard.Body'
|
||||
defaultMessage='You can share your board internally, within your team, or publish it publicly for visibility outside of your organization.'
|
||||
/>
|
||||
)
|
||||
|
||||
const punchout = useMeasurePunchouts(['.ShareBoardButton > button'], [])
|
||||
|
||||
if (!BoardTourSteps.SHARE_BOARD) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<TourTipRenderer
|
||||
key='ShareBoardTourStep'
|
||||
requireCard={false}
|
||||
category={TOUR_BOARD}
|
||||
step={BoardTourSteps.SHARE_BOARD}
|
||||
screen={screen}
|
||||
title={title}
|
||||
punchout={punchout}
|
||||
classname='ShareBoardTourStep'
|
||||
telemetryTag='tourPoint2b'
|
||||
placement={'bottom-end'}
|
||||
imageURL={Utils.buildURL(shareBoard, true)}
|
||||
hideBackdrop={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShareBoardTourStep
|
@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {Placement} from 'tippy.js'
|
||||
|
||||
import {useAppSelector} from '../../../store/hooks'
|
||||
import {getCurrentBoard} from '../../../store/boards'
|
||||
import {getCurrentCard} from '../../../store/cards'
|
||||
import {OnboardingBoardTitle, OnboardingCardTitle} from '../../cardDetail/cardDetail'
|
||||
import {getOnboardingTourCategory, getOnboardingTourStarted, getOnboardingTourStep} from '../../../store/users'
|
||||
import TourTip from '../../tutorial_tour_tip/tutorial_tour_tip'
|
||||
import {TutorialTourTipPunchout} from '../../tutorial_tour_tip/tutorial_tour_tip_backdrop'
|
||||
|
||||
type Props = {
|
||||
requireCard: boolean
|
||||
category: string
|
||||
step: number
|
||||
screen: JSX.Element
|
||||
title: JSX.Element
|
||||
punchout: TutorialTourTipPunchout | null | undefined
|
||||
classname: string
|
||||
telemetryTag: string
|
||||
placement: Placement | undefined
|
||||
hideBackdrop: boolean
|
||||
imageURL?: string
|
||||
singleTip?: boolean
|
||||
hideNavButtons?: boolean
|
||||
}
|
||||
|
||||
const TourTipRenderer = (props: Props): JSX.Element | null => {
|
||||
const board = useAppSelector(getCurrentBoard)
|
||||
|
||||
const isOnboardingBoard = board ? board.title === OnboardingBoardTitle : false
|
||||
const onboardingTourStarted = useAppSelector(getOnboardingTourStarted)
|
||||
const onboardingTourCategory = useAppSelector(getOnboardingTourCategory)
|
||||
const onboardingTourStep = useAppSelector(getOnboardingTourStep)
|
||||
const showTour = isOnboardingBoard && onboardingTourStarted && onboardingTourCategory === props.category
|
||||
let showTourTip = showTour && onboardingTourStep === props.step.toString()
|
||||
|
||||
if (props.requireCard) {
|
||||
const card = useAppSelector(getCurrentCard)
|
||||
const isOnboardingCard = card ? card.title === OnboardingCardTitle : false
|
||||
|
||||
showTourTip = showTourTip && isOnboardingCard
|
||||
}
|
||||
|
||||
const currentStep = parseInt(useAppSelector(getOnboardingTourStep), 10)
|
||||
|
||||
if (!showTourTip) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<TourTip
|
||||
screen={props.screen}
|
||||
title={props.title}
|
||||
punchOut={props.punchout}
|
||||
step={currentStep}
|
||||
tutorialCategory={props.category}
|
||||
placement={props.placement}
|
||||
className={props.classname}
|
||||
imageURL={props.imageURL}
|
||||
telemetryTag={props.telemetryTag}
|
||||
skipCategoryFromBackdrop={true}
|
||||
autoTour={true}
|
||||
hideBackdrop={props.hideBackdrop}
|
||||
singleTip={props.singleTip}
|
||||
hideNavButtons={props.hideNavButtons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TourTipRenderer
|
40
webapp/src/components/pulsating_dot/index.tsx
Normal file
40
webapp/src/components/pulsating_dot/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import './pulsating_dot.scss'
|
||||
import {Coords} from '../tutorial_tour_tip/tutorial_tour_tip_backdrop'
|
||||
|
||||
type Props = {
|
||||
targetRef?: React.RefObject<HTMLImageElement>;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
coords?: Coords;
|
||||
}
|
||||
|
||||
const PulsatingDot = (props: Props):JSX.Element => {
|
||||
let customStyles = {}
|
||||
if (props?.coords) {
|
||||
customStyles = {
|
||||
transform: `translate(${props.coords?.x}px, ${props.coords?.y}px)`,
|
||||
}
|
||||
}
|
||||
let effectiveClassName = 'pulsating_dot'
|
||||
if (props.onClick) {
|
||||
effectiveClassName += ' pulsating_dot-clickable'
|
||||
}
|
||||
if (props.className) {
|
||||
effectiveClassName = effectiveClassName + ' ' + props.className
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={effectiveClassName}
|
||||
onClick={props.onClick}
|
||||
ref={props.targetRef}
|
||||
style={{...customStyles}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PulsatingDot
|
71
webapp/src/components/pulsating_dot/pulsating_dot.scss
Normal file
71
webapp/src/components/pulsating_dot/pulsating_dot.scss
Normal file
@ -0,0 +1,71 @@
|
||||
.pulsating_dot {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
|
||||
&-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&,
|
||||
&::before,
|
||||
&::after {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #3db887;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
animation: pulse1 2s ease 0s infinite;
|
||||
}
|
||||
|
||||
&::before {
|
||||
animation: pulse2 2s ease 0s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse1 {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse2 {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
}
|
@ -86,7 +86,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -95,10 +95,10 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copy link
|
||||
</span>
|
||||
</button>
|
||||
@ -196,7 +196,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -205,10 +205,10 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copied!
|
||||
</span>
|
||||
</button>
|
||||
@ -306,7 +306,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -315,10 +315,10 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copy link
|
||||
</span>
|
||||
</button>
|
||||
@ -416,7 +416,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -425,10 +425,10 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copy link
|
||||
</span>
|
||||
</button>
|
||||
@ -526,7 +526,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -535,10 +535,10 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copy link
|
||||
</span>
|
||||
</button>
|
||||
@ -705,7 +705,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -714,10 +714,10 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copy link
|
||||
</span>
|
||||
</button>
|
||||
@ -815,7 +815,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -824,10 +824,10 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copy link
|
||||
</span>
|
||||
</button>
|
||||
@ -925,7 +925,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-refresh Icon Icon--right"
|
||||
class="CompassIcon icon-refresh undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -934,10 +934,10 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
title="Copy link"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-content-copy CompassIcon"
|
||||
/>
|
||||
Copy link
|
||||
</span>
|
||||
</button>
|
||||
|
@ -6,14 +6,14 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
|
||||
class="ShareBoardButton"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--secondary size--medium"
|
||||
class="Button emphasis--primary size--medium"
|
||||
title="Share board"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-globe CompassIcon"
|
||||
/>
|
||||
<span>
|
||||
<i
|
||||
class="CompassIcon icon-globe CompassIcon"
|
||||
/>
|
||||
Share
|
||||
</span>
|
||||
</button>
|
||||
|
@ -30,23 +30,12 @@
|
||||
.input-container {
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
width: 400px;
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.icon-refresh {
|
||||
margin: 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.icon-content-copy {
|
||||
margin-right: 4px;
|
||||
font-size: 18px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 0 4px 0 16px;
|
||||
}
|
||||
|
||||
.shareUrl {
|
||||
@ -55,7 +44,8 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: auto 4px;
|
||||
padding: 0 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
:hover {
|
||||
background-color: rgb(var(--center-channel-color-rgb), 0.1);
|
||||
|
@ -134,14 +134,13 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
||||
title={intl.formatMessage({id: 'ShareBoard.regenerate', defaultMessage: 'Regenerate token'})}
|
||||
>
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={onRegenerateToken}
|
||||
icon={
|
||||
<CompassIcon
|
||||
icon='refresh'
|
||||
className='Icon Icon--right'
|
||||
/>}
|
||||
title={intl.formatMessage({id: 'ShareBoard.regenerate', defaultMessage: 'Regenerate token'})}
|
||||
className='IconButton--large'
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -149,16 +148,18 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
||||
emphasis='secondary'
|
||||
size='medium'
|
||||
title='Copy link'
|
||||
icon={
|
||||
<CompassIcon
|
||||
icon='content-copy'
|
||||
className='CompassIcon'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ShareLinkPublicCopy, {board: props.boardId})
|
||||
Utils.copyTextToClipboard(shareUrl.toString())
|
||||
setWasCopied(true)
|
||||
}}
|
||||
>
|
||||
<CompassIcon
|
||||
icon='content-copy'
|
||||
className='CompassIcon'
|
||||
/>
|
||||
{wasCopied &&
|
||||
<FormattedMessage
|
||||
id='ShareBoard.copiedLink'
|
||||
|
@ -1,20 +1,4 @@
|
||||
.ShareBoardButton {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
|
||||
> .Button {
|
||||
margin-top: 36px;
|
||||
padding: 3px 10px;
|
||||
background-color: rgb(var(--button-bg-rgb));
|
||||
color: rgb(var(--button-color-rgb));
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.8);
|
||||
}
|
||||
}
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
|
@ -24,16 +24,18 @@ const ShareBoardButton = (props: Props) => {
|
||||
<Button
|
||||
title='Share board'
|
||||
size='medium'
|
||||
emphasis='secondary'
|
||||
emphasis='primary'
|
||||
icon={
|
||||
<CompassIcon
|
||||
icon='globe'
|
||||
className='CompassIcon'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ShareBoardOpenModal, {board: props.boardId})
|
||||
setShowShareDialog(!showShareDialog)
|
||||
}}
|
||||
>
|
||||
<CompassIcon
|
||||
icon='globe'
|
||||
className='CompassIcon'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='CenterPanel.Share'
|
||||
defaultMessage='Share'
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user