You've already forked focalboard
mirror of
https://github.com/mattermost/focalboard.git
synced 2025-07-06 23:36:34 +02:00
Merge branch 'main' into gh-2712-fix-templates
This commit is contained in:
@ -87,14 +87,9 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/subtree", a.attachSession(a.handleGetSubTree, false)).Methods("GET")
|
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.attachSession(a.handleDuplicateBlock, false)).Methods("POST")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.attachSession(a.handleDuplicateBlock, false)).Methods("POST")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
|
apiv1.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
|
||||||
|
|
||||||
// Import&Export APIs
|
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/export", a.sessionRequired(a.handleExport)).Methods("GET")
|
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/import", a.sessionRequired(a.handleImport)).Methods("POST")
|
|
||||||
|
|
||||||
// Member APIs
|
// Member APIs
|
||||||
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET")
|
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST")
|
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST")
|
||||||
@ -363,23 +358,6 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stampModificationMetadata(r *http.Request, blocks []model.Block, auditRec *audit.Record) {
|
|
||||||
userID := getUserID(r)
|
|
||||||
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) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||||
requestBody, err := ioutil.ReadAll(r.Body)
|
requestBody, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1219,287 +1197,6 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:operation GET /api/v1/boards/{boardID}/blocks/{blockID}/subtree getSubTree
|
|
||||||
//
|
|
||||||
// Returns the blocks of a subtree
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: boardID
|
|
||||||
// in: path
|
|
||||||
// description: Board ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// - name: blockID
|
|
||||||
// in: path
|
|
||||||
// description: The ID of the root block of the subtree
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// - name: l
|
|
||||||
// in: query
|
|
||||||
// description: The number of levels to return. 2 or 3. Defaults to 2.
|
|
||||||
// required: false
|
|
||||||
// type: integer
|
|
||||||
// minimum: 2
|
|
||||||
// maximum: 3
|
|
||||||
// security:
|
|
||||||
// - BearerAuth: []
|
|
||||||
// responses:
|
|
||||||
// '200':
|
|
||||||
// description: success
|
|
||||||
// schema:
|
|
||||||
// type: array
|
|
||||||
// items:
|
|
||||||
// "$ref": "#/definitions/Block"
|
|
||||||
// default:
|
|
||||||
// description: internal error
|
|
||||||
// schema:
|
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
|
||||||
|
|
||||||
userID := getUserID(r)
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
boardID := vars["boardID"]
|
|
||||||
blockID := vars["blockID"]
|
|
||||||
|
|
||||||
if !a.hasValidReadTokenForBoard(r, boardID) && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.URL.Query()
|
|
||||||
levels, err := strconv.ParseInt(query.Get("l"), 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
levels = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if levels != 2 && levels != 3 {
|
|
||||||
a.logger.Error("Invalid levels", mlog.Int64("levels", levels))
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "invalid levels", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "getSubTree", audit.Fail)
|
|
||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
|
||||||
auditRec.AddMeta("boardID", boardID)
|
|
||||||
auditRec.AddMeta("blockID", blockID)
|
|
||||||
|
|
||||||
blocks, err := a.app.GetSubTree(boardID, blockID, int(levels), model.QuerySubtreeOptions{})
|
|
||||||
if err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
a.logger.Debug("GetSubTree",
|
|
||||||
mlog.Int64("levels", levels),
|
|
||||||
mlog.String("boardID", boardID),
|
|
||||||
mlog.String("blockID", blockID),
|
|
||||||
mlog.Int("block_count", len(blocks)),
|
|
||||||
)
|
|
||||||
json, err := json.Marshal(blocks)
|
|
||||||
if err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBytesResponse(w, http.StatusOK, json)
|
|
||||||
|
|
||||||
auditRec.AddMeta("blockCount", len(blocks))
|
|
||||||
auditRec.Success()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *API) handleExport(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:operation GET /api/v1/boards/{boardID}/blocks/export exportBlocks
|
|
||||||
//
|
|
||||||
// Returns all blocks of a board
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: boardID
|
|
||||||
// in: path
|
|
||||||
// description: Board ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// security:
|
|
||||||
// - BearerAuth: []
|
|
||||||
// responses:
|
|
||||||
// '200':
|
|
||||||
// description: success
|
|
||||||
// schema:
|
|
||||||
// type: array
|
|
||||||
// items:
|
|
||||||
// "$ref": "#/definitions/Block"
|
|
||||||
// default:
|
|
||||||
// description: internal error
|
|
||||||
// schema:
|
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
|
||||||
|
|
||||||
userID := getUserID(r)
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
boardID := vars["boardID"]
|
|
||||||
|
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.URL.Query()
|
|
||||||
rootID := query.Get("root_id")
|
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "export", audit.Fail)
|
|
||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
|
||||||
auditRec.AddMeta("boardID", boardID)
|
|
||||||
auditRec.AddMeta("rootID", rootID)
|
|
||||||
|
|
||||||
var blocks []model.Block
|
|
||||||
var err error
|
|
||||||
if rootID == "" {
|
|
||||||
blocks, err = a.app.GetBlocksForBoard(boardID)
|
|
||||||
} else {
|
|
||||||
blocks, err = a.app.GetBlocksWithBoardID(boardID)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
a.logger.Debug("raw blocks", mlog.Int("block_count", len(blocks)))
|
|
||||||
auditRec.AddMeta("rawCount", len(blocks))
|
|
||||||
|
|
||||||
blocks = filterOrphanBlocks(blocks)
|
|
||||||
|
|
||||||
a.logger.Debug("EXPORT filtered blocks", mlog.Int("block_count", len(blocks)))
|
|
||||||
auditRec.AddMeta("filteredCount", len(blocks))
|
|
||||||
|
|
||||||
json, err := json.Marshal(blocks)
|
|
||||||
if err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBytesResponse(w, http.StatusOK, json)
|
|
||||||
|
|
||||||
auditRec.Success()
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterOrphanBlocks(blocks []model.Block) (ret []model.Block) {
|
|
||||||
queue := make([]model.Block, 0)
|
|
||||||
childrenOfBlockWithID := make(map[string]*[]model.Block)
|
|
||||||
|
|
||||||
// Build the trees from nodes
|
|
||||||
for _, block := range blocks {
|
|
||||||
if len(block.ParentID) == 0 {
|
|
||||||
// Queue root blocks to process first
|
|
||||||
queue = append(queue, block)
|
|
||||||
} else {
|
|
||||||
siblings := childrenOfBlockWithID[block.ParentID]
|
|
||||||
if siblings != nil {
|
|
||||||
*siblings = append(*siblings, block)
|
|
||||||
} else {
|
|
||||||
siblings := []model.Block{block}
|
|
||||||
childrenOfBlockWithID[block.ParentID] = &siblings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map the trees to an array, which skips orphaned nodes
|
|
||||||
blocks = make([]model.Block, 0)
|
|
||||||
for len(queue) > 0 {
|
|
||||||
block := queue[0]
|
|
||||||
queue = queue[1:] // dequeue
|
|
||||||
blocks = append(blocks, block)
|
|
||||||
children := childrenOfBlockWithID[block.ID]
|
|
||||||
if children != nil {
|
|
||||||
queue = append(queue, *children...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *API) handleImport(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:operation POST /api/v1/boards/{boardID}/blocks/import importBlocks
|
|
||||||
//
|
|
||||||
// Import blocks on a given board
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: boardID
|
|
||||||
// in: path
|
|
||||||
// description: Board ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// - name: Body
|
|
||||||
// in: body
|
|
||||||
// description: array of blocks to import
|
|
||||||
// required: true
|
|
||||||
// schema:
|
|
||||||
// type: array
|
|
||||||
// items:
|
|
||||||
// "$ref": "#/definitions/Block"
|
|
||||||
// security:
|
|
||||||
// - BearerAuth: []
|
|
||||||
// responses:
|
|
||||||
// '200':
|
|
||||||
// description: success
|
|
||||||
// default:
|
|
||||||
// description: internal error
|
|
||||||
// schema:
|
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
|
||||||
|
|
||||||
userID := getUserID(r)
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
boardID := vars["boardID"]
|
|
||||||
|
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestBody, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var blocks []model.Block
|
|
||||||
|
|
||||||
err = json.Unmarshal(requestBody, &blocks)
|
|
||||||
if err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "import", audit.Fail)
|
|
||||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
|
||||||
auditRec.AddMeta("boardID", boardID)
|
|
||||||
|
|
||||||
// all blocks should now be part of the board that they're being
|
|
||||||
// imported onto
|
|
||||||
for i := range blocks {
|
|
||||||
blocks[i].BoardID = boardID
|
|
||||||
}
|
|
||||||
|
|
||||||
stampModificationMetadata(r, blocks, auditRec)
|
|
||||||
|
|
||||||
if _, err = a.app.InsertBlocks(model.GenerateBlockIDs(blocks, a.logger), userID, false); err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonStringResponse(w, http.StatusOK, "{}")
|
|
||||||
|
|
||||||
a.logger.Debug("IMPORT BlockIDs", mlog.Int("block_count", len(blocks)))
|
|
||||||
auditRec.AddMeta("blockCount", len(blocks))
|
|
||||||
auditRec.Success()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sharing
|
// Sharing
|
||||||
|
|
||||||
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -1799,6 +1496,9 @@ func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http
|
|||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
|
}
|
||||||
|
|
||||||
team, err := a.app.GetRootTeam()
|
team, err := a.app.GetRootTeam()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2832,8 +2532,7 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
|
if userID == "" {
|
||||||
if userID == "" && !hasValidReadToken {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2848,17 +2547,16 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasValidReadToken {
|
|
||||||
if board.Type == model.BoardTypePrivate {
|
if board.Type == model.BoardTypePrivate {
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||||
return
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2927,8 +2625,7 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
asTemplate := query.Get("asTemplate")
|
asTemplate := query.Get("asTemplate")
|
||||||
|
|
||||||
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
|
if userID == "" {
|
||||||
if userID == "" && !hasValidReadToken {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,9 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
teamID := vars["teamID"]
|
teamID := vars["teamID"]
|
||||||
|
@ -166,6 +166,9 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
@ -228,6 +231,9 @@ func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
@ -278,6 +284,9 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
@ -377,6 +386,9 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
|
@ -234,15 +234,6 @@ func (a *App) CopyCardFiles(sourceBoardID string, blocks []model.Block) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetSubTree(boardID, blockID string, levels int, opts model.QuerySubtreeOptions) ([]model.Block, error) {
|
|
||||||
// Only 2 or 3 levels are supported for now
|
|
||||||
if levels >= 3 {
|
|
||||||
return a.store.GetSubTree3(boardID, blockID, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.store.GetSubTree2(boardID, blockID, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
|
func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
|
||||||
return a.store.GetBlock(blockID)
|
return a.store.GetBlock(blockID)
|
||||||
}
|
}
|
||||||
|
@ -164,10 +164,6 @@ func (c *Client) GetBlockRoute(boardID, blockID string) string {
|
|||||||
return fmt.Sprintf("%s/%s", c.GetBlocksRoute(boardID), blockID)
|
return fmt.Sprintf("%s/%s", c.GetBlocksRoute(boardID), blockID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetSubtreeRoute(boardID, blockID string) string {
|
|
||||||
return fmt.Sprintf("%s/subtree", c.GetBlockRoute(boardID, blockID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetBoardsRoute() string {
|
func (c *Client) GetBoardsRoute() string {
|
||||||
return "/boards"
|
return "/boards"
|
||||||
}
|
}
|
||||||
@ -297,16 +293,6 @@ func (c *Client) DeleteBlock(boardID, blockID string) (bool, *Response) {
|
|||||||
return true, BuildResponse(r)
|
return true, BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetSubtree(boardID, blockID string) ([]model.Block, *Response) {
|
|
||||||
r, err := c.DoAPIGet(c.GetSubtreeRoute(boardID, blockID), "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, BuildErrorResponse(r, err)
|
|
||||||
}
|
|
||||||
defer closeBody(r)
|
|
||||||
|
|
||||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boards and blocks.
|
// Boards and blocks.
|
||||||
func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
|
func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
|
||||||
r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab))
|
r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab))
|
||||||
|
@ -429,18 +429,4 @@ func TestGetSubtree(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.Contains(t, blockIDs, parentBlockID)
|
require.Contains(t, blockIDs, parentBlockID)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Get subtree for parent ID", func(t *testing.T) {
|
|
||||||
blocks, resp := th.Client.GetSubtree(board.ID, parentBlockID)
|
|
||||||
require.NoError(t, resp.Error)
|
|
||||||
require.Len(t, blocks, 3)
|
|
||||||
|
|
||||||
blockIDs := make([]string, len(blocks))
|
|
||||||
for i, b := range blocks {
|
|
||||||
blockIDs[i] = b.ID
|
|
||||||
}
|
|
||||||
require.Contains(t, blockIDs, parentBlockID)
|
|
||||||
require.Contains(t, blockIDs, childBlockID1)
|
|
||||||
require.Contains(t, blockIDs, childBlockID2)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,8 @@ describe('Login actions', () => {
|
|||||||
|
|
||||||
it('Can perform login/register actions', () => {
|
it('Can perform login/register actions', () => {
|
||||||
// Redirects to login page
|
// Redirects to login page
|
||||||
cy.log('**Redirects to error then login page**')
|
cy.log('**Redirects to login page (except plugin mode) **')
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.location('pathname').should('eq', '/error')
|
|
||||||
cy.get('button').contains('Log in').click()
|
|
||||||
cy.location('pathname').should('eq', '/login')
|
cy.location('pathname').should('eq', '/login')
|
||||||
cy.get('.LoginPage').contains('Log in')
|
cy.get('.LoginPage').contains('Log in')
|
||||||
cy.get('#login-username').should('exist')
|
cy.get('#login-username').should('exist')
|
||||||
@ -40,7 +38,7 @@ describe('Login actions', () => {
|
|||||||
// User should not be logged in automatically
|
// User should not be logged in automatically
|
||||||
cy.log('**User should not be logged in automatically**')
|
cy.log('**User should not be logged in automatically**')
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.location('pathname').should('eq', '/error')
|
cy.location('pathname').should('eq', '/login')
|
||||||
|
|
||||||
// Can log in registered user
|
// Can log in registered user
|
||||||
cy.log('**Can log in registered user**')
|
cy.log('**Can log in registered user**')
|
||||||
|
@ -695,7 +695,20 @@ exports[`components/centerPanel return centerPanel and press touch 1 with readon
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="shareButtonWrapper"
|
class="shareButtonWrapper"
|
||||||
/>
|
>
|
||||||
|
<div
|
||||||
|
class="ShareBoardLoginButton"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title="Login"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Login
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ViewHeader"
|
class="ViewHeader"
|
||||||
|
@ -970,7 +970,20 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="shareButtonWrapper"
|
class="shareButtonWrapper"
|
||||||
/>
|
>
|
||||||
|
<div
|
||||||
|
class="ShareBoardLoginButton"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title="Login"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Login
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ViewHeader"
|
class="ViewHeader"
|
||||||
@ -2174,7 +2187,20 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="shareButtonWrapper"
|
class="shareButtonWrapper"
|
||||||
/>
|
>
|
||||||
|
<div
|
||||||
|
class="ShareBoardLoginButton"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title="Login"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Login
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ViewHeader"
|
class="ViewHeader"
|
||||||
|
@ -38,6 +38,7 @@ import {UserConfigPatch} from '../user'
|
|||||||
import octoClient from '../octoClient'
|
import octoClient from '../octoClient'
|
||||||
|
|
||||||
import ShareBoardButton from './shareBoard/shareBoardButton'
|
import ShareBoardButton from './shareBoard/shareBoardButton'
|
||||||
|
import ShareBoardLoginButton from './shareBoard/shareBoardLoginButton'
|
||||||
|
|
||||||
import CardDialog from './cardDialog'
|
import CardDialog from './cardDialog'
|
||||||
import RootPortal from './rootPortal'
|
import RootPortal from './rootPortal'
|
||||||
@ -334,6 +335,9 @@ const CenterPanel = (props: Props) => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}, [selectedCardIds, props.activeView, props.cards, showCard])
|
}, [selectedCardIds, props.activeView, props.cards, showCard])
|
||||||
|
|
||||||
|
const showShareButton = !props.readonly && me?.id !== 'single-user'
|
||||||
|
const showShareLoginButton = props.readonly && me?.id !== 'single-user'
|
||||||
|
|
||||||
const {groupByProperty, activeView, board, views, cards} = props
|
const {groupByProperty, activeView, board, views, cards} = props
|
||||||
const {visible: visibleGroups, hidden: hiddenGroups} = useMemo(
|
const {visible: visibleGroups, hidden: hiddenGroups} = useMemo(
|
||||||
() => getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty),
|
() => getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty),
|
||||||
@ -369,13 +373,14 @@ const CenterPanel = (props: Props) => {
|
|||||||
readonly={props.readonly}
|
readonly={props.readonly}
|
||||||
/>
|
/>
|
||||||
<div className='shareButtonWrapper'>
|
<div className='shareButtonWrapper'>
|
||||||
{!props.readonly &&
|
{showShareButton &&
|
||||||
(
|
<ShareBoardButton
|
||||||
<ShareBoardButton
|
boardId={props.board.id}
|
||||||
boardId={props.board.id}
|
enableSharedBoards={props.clientConfig?.enablePublicSharedBoards || false}
|
||||||
enableSharedBoards={props.clientConfig?.enablePublicSharedBoards || false}
|
/>
|
||||||
/>
|
}
|
||||||
)
|
{showShareLoginButton &&
|
||||||
|
<ShareBoardLoginButton/>
|
||||||
}
|
}
|
||||||
<ShareBoardTourStep/>
|
<ShareBoardTourStep/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`src/components/shareBoard/shareBoardLoginButton should match snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="ShareBoardLoginButton"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="Button emphasis--primary size--medium"
|
||||||
|
title="Login"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Login
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,4 @@
|
|||||||
|
.ShareBoardLoginButton {
|
||||||
|
margin-top: 38px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import {render} from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||||
|
import {wrapDNDIntl} from '../../testUtils'
|
||||||
|
|
||||||
|
import ShareBoardLoginButton from './shareBoardLoginButton'
|
||||||
|
jest.useFakeTimers()
|
||||||
|
|
||||||
|
const boardId = '1'
|
||||||
|
|
||||||
|
const board = TestBlockFactory.createBoard()
|
||||||
|
board.id = boardId
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => {
|
||||||
|
const originalModule = jest.requireActual('react-router-dom')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
useRouteMatch: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
teamId: 'team1',
|
||||||
|
boardId: 'boardId1',
|
||||||
|
viewId: 'viewId1',
|
||||||
|
cardId: 'cardId1',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('src/components/shareBoard/shareBoardLoginButton', () => {
|
||||||
|
const savedLocation = window.location
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.location = savedLocation
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should match snapshot', async () => {
|
||||||
|
// delete window.location
|
||||||
|
window.location = Object.assign(new URL('https://example.org/mattermost'))
|
||||||
|
const result = render(
|
||||||
|
wrapDNDIntl(
|
||||||
|
<ShareBoardLoginButton/>,
|
||||||
|
))
|
||||||
|
const renderer = result.container
|
||||||
|
|
||||||
|
expect(renderer).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
51
webapp/src/components/shareBoard/shareBoardLoginButton.tsx
Normal file
51
webapp/src/components/shareBoard/shareBoardLoginButton.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react'
|
||||||
|
import {FormattedMessage} from 'react-intl'
|
||||||
|
import {generatePath, useRouteMatch, useHistory} from 'react-router-dom'
|
||||||
|
|
||||||
|
import Button from '../../widgets/buttons/button'
|
||||||
|
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
|
||||||
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
|
import './shareBoardLoginButton.scss'
|
||||||
|
|
||||||
|
const ShareBoardLoginButton = () => {
|
||||||
|
const match = useRouteMatch<{teamId: string, boardId: string, viewId?: string, cardId?: string}>()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
let redirectQueryParam = 'r=' + encodeURIComponent(generatePath('/:boardId?/:viewId?/:cardId?', match.params))
|
||||||
|
if (Utils.isFocalboardLegacy()) {
|
||||||
|
redirectQueryParam = 'redirect_to=' + encodeURIComponent(generatePath('/boards/team/:teamId/:boardId?/:viewId?/:cardId?', match.params))
|
||||||
|
}
|
||||||
|
const loginPath = '/login?' + redirectQueryParam
|
||||||
|
|
||||||
|
const onLoginClick = useCallback(() => {
|
||||||
|
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ShareBoardLogin)
|
||||||
|
if (Utils.isFocalboardLegacy()) {
|
||||||
|
location.assign(loginPath)
|
||||||
|
} else {
|
||||||
|
history.push(loginPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='ShareBoardLoginButton'>
|
||||||
|
<Button
|
||||||
|
title='Login'
|
||||||
|
size='medium'
|
||||||
|
emphasis='primary'
|
||||||
|
onClick={() => onLoginClick()}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='CenterPanel.Login'
|
||||||
|
defaultMessage='Login'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(ShareBoardLoginButton)
|
@ -23,10 +23,6 @@ test('OctoClient: get blocks', async () => {
|
|||||||
let boards = await octoClient.getBlocksWithType('card')
|
let boards = await octoClient.getBlocksWithType('card')
|
||||||
expect(boards.length).toBe(blocks.length)
|
expect(boards.length).toBe(blocks.length)
|
||||||
|
|
||||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify(blocks)))
|
|
||||||
boards = await octoClient.getSubtree()
|
|
||||||
expect(boards.length).toBe(blocks.length)
|
|
||||||
|
|
||||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify(blocks)))
|
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify(blocks)))
|
||||||
const response = await octoClient.exportArchive()
|
const response = await octoClient.exportArchive()
|
||||||
expect(response.status).toBe(200)
|
expect(response.status).toBe(200)
|
||||||
|
@ -201,20 +201,6 @@ class OctoClient {
|
|||||||
return (await this.getJson(response, {})) as Record<string, string>
|
return (await this.getJson(response, {})) as Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubtree(boardId?: string, levels = 2, teamID?: string): Promise<Block[]> {
|
|
||||||
let path = this.teamPath(teamID) + `/blocks/${encodeURIComponent(boardId || '')}/subtree?l=${levels}`
|
|
||||||
const readToken = Utils.getReadToken()
|
|
||||||
if (readToken) {
|
|
||||||
path += `&read_token=${readToken}`
|
|
||||||
}
|
|
||||||
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
|
|
||||||
if (response.status !== 200) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const blocks = (await this.getJson(response, [])) as Block[]
|
|
||||||
return this.fixBlocks(blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no boardID is provided, it will export the entire archive
|
// If no boardID is provided, it will export the entire archive
|
||||||
async exportArchive(boardID = ''): Promise<Response> {
|
async exportArchive(boardID = ''): Promise<Response> {
|
||||||
const path = `/api/v1/boards/${boardID}/archive/export`
|
const path = `/api/v1/boards/${boardID}/archive/export`
|
||||||
|
@ -10,6 +10,7 @@ import Button from '../widgets/buttons/button'
|
|||||||
import './errorPage.scss'
|
import './errorPage.scss'
|
||||||
|
|
||||||
import {errorDefFromId, ErrorId} from '../errors'
|
import {errorDefFromId, ErrorId} from '../errors'
|
||||||
|
import {Utils} from '../utils'
|
||||||
|
|
||||||
const ErrorPage = () => {
|
const ErrorPage = () => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
@ -45,6 +46,10 @@ const ErrorPage = () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!Utils.isFocalboardPlugin() && errid === ErrorId.NotLoggedIn) {
|
||||||
|
handleButtonClick(errorDef.button1Redirect)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='ErrorPage'>
|
<div className='ErrorPage'>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {useHistory, Link} from 'react-router-dom'
|
|
||||||
|
import {Link, useLocation, useHistory} from 'react-router-dom'
|
||||||
import {FormattedMessage} from 'react-intl'
|
import {FormattedMessage} from 'react-intl'
|
||||||
|
|
||||||
import {useAppDispatch} from '../store/hooks'
|
import {useAppDispatch} from '../store/hooks'
|
||||||
@ -15,14 +16,19 @@ const LoginPage = () => {
|
|||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
const history = useHistory()
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const queryParams = new URLSearchParams(useLocation().search)
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
const handleLogin = async (): Promise<void> => {
|
const handleLogin = async (): Promise<void> => {
|
||||||
const logged = await client.login(username, password)
|
const logged = await client.login(username, password)
|
||||||
if (logged) {
|
if (logged) {
|
||||||
await dispatch(fetchMe())
|
await dispatch(fetchMe())
|
||||||
history.push('/')
|
if (queryParams) {
|
||||||
|
history.push(queryParams.get('r') || '/')
|
||||||
|
} else {
|
||||||
|
history.push('/')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage('Login failed')
|
setErrorMessage('Login failed')
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ export const TelemetryActions = {
|
|||||||
AddTemplateFromCard: 'addTemplateFromCard',
|
AddTemplateFromCard: 'addTemplateFromCard',
|
||||||
ViewSharedBoard: 'viewSharedBoard',
|
ViewSharedBoard: 'viewSharedBoard',
|
||||||
ShareBoardOpenModal: 'shareBoard_openModal',
|
ShareBoardOpenModal: 'shareBoard_openModal',
|
||||||
|
ShareBoardLogin: 'shareBoard_login',
|
||||||
ShareLinkPublicCopy: 'shareLinkPublic_copy',
|
ShareLinkPublicCopy: 'shareLinkPublic_copy',
|
||||||
ShareLinkInternalCopy: 'shareLinkInternal_copy',
|
ShareLinkInternalCopy: 'shareLinkInternal_copy',
|
||||||
ImportArchive: 'settings_importArchive',
|
ImportArchive: 'settings_importArchive',
|
||||||
|
@ -513,7 +513,7 @@ class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getFrontendBaseURL(absolute?: boolean): string {
|
static getFrontendBaseURL(absolute?: boolean): string {
|
||||||
let frontendBaseURL = window.frontendBaseURL || Utils.getBaseURL(absolute)
|
let frontendBaseURL = window.frontendBaseURL || Utils.getBaseURL()
|
||||||
frontendBaseURL = frontendBaseURL.replace(/\/+$/, '')
|
frontendBaseURL = frontendBaseURL.replace(/\/+$/, '')
|
||||||
if (frontendBaseURL.indexOf('/') === 0) {
|
if (frontendBaseURL.indexOf('/') === 0) {
|
||||||
frontendBaseURL = frontendBaseURL.slice(1)
|
frontendBaseURL = frontendBaseURL.slice(1)
|
||||||
|
Reference in New Issue
Block a user