You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/main' into compliance-history-export
This commit is contained in:
		| @@ -31,6 +31,7 @@ func init() { | ||||
| 			product.ChannelKey:       {}, | ||||
| 			product.UserKey:          {}, | ||||
| 			product.PostKey:          {}, | ||||
| 			product.PermissionsKey:   {}, | ||||
| 			product.BotKey:           {}, | ||||
| 			product.ClusterKey:       {}, | ||||
| 			product.ConfigKey:        {}, | ||||
| @@ -44,6 +45,7 @@ func init() { | ||||
| 			product.StoreKey:         {}, | ||||
| 			product.SystemKey:        {}, | ||||
| 			product.PreferencesKey:   {}, | ||||
| 			product.HooksKey:         {}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
| @@ -73,127 +75,150 @@ type boardsProduct struct { | ||||
| } | ||||
|  | ||||
| func newBoardsProduct(services map[product.ServiceKey]interface{}) (product.Product, error) { | ||||
| 	boards := &boardsProduct{} | ||||
| 	boardsProd := &boardsProduct{} | ||||
|  | ||||
| 	if err := populateServices(boardsProd, services); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	boardsProd.logger.Info("Creating boards service") | ||||
|  | ||||
| 	adapter := newServiceAPIAdapter(boardsProd) | ||||
| 	boardsApp, err := boards.NewBoardsApp(adapter) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create Boards service: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	boardsProd.boardsApp = boardsApp | ||||
|  | ||||
| 	// Add the Boards services API to the services map so other products can access Boards functionality. | ||||
| 	boardsAPI := boards.NewBoardsServiceAPI(boardsApp) | ||||
| 	services[product.BoardsKey] = boardsAPI | ||||
|  | ||||
| 	return boardsProd, nil | ||||
| } | ||||
|  | ||||
| // populateServices populates the boardProduct with all the services needed from the suite. | ||||
| func populateServices(boardsProd *boardsProduct, services map[product.ServiceKey]interface{}) error { | ||||
| 	for key, service := range services { | ||||
| 		switch key { | ||||
| 		case product.TeamKey: | ||||
| 			teamService, ok := service.(product.TeamService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.teamService = teamService | ||||
| 			boardsProd.teamService = teamService | ||||
| 		case product.ChannelKey: | ||||
| 			channelService, ok := service.(product.ChannelService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.channelService = channelService | ||||
| 			boardsProd.channelService = channelService | ||||
| 		case product.UserKey: | ||||
| 			userService, ok := service.(product.UserService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.userService = userService | ||||
| 			boardsProd.userService = userService | ||||
| 		case product.PostKey: | ||||
| 			postService, ok := service.(product.PostService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.postService = postService | ||||
| 			boardsProd.postService = postService | ||||
| 		case product.PermissionsKey: | ||||
| 			permissionsService, ok := service.(product.PermissionService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.permissionsService = permissionsService | ||||
| 			boardsProd.permissionsService = permissionsService | ||||
| 		case product.BotKey: | ||||
| 			botService, ok := service.(product.BotService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.botService = botService | ||||
| 			boardsProd.botService = botService | ||||
| 		case product.ClusterKey: | ||||
| 			clusterService, ok := service.(product.ClusterService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.clusterService = clusterService | ||||
| 			boardsProd.clusterService = clusterService | ||||
| 		case product.ConfigKey: | ||||
| 			configService, ok := service.(product.ConfigService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.configService = configService | ||||
| 			boardsProd.configService = configService | ||||
| 		case product.LogKey: | ||||
| 			logger, ok := service.(mlog.LoggerIFace) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.logger = logger.With(mlog.String("product", boardsProductName)) | ||||
| 			boardsProd.logger = logger.With(mlog.String("product", boardsProductName)) | ||||
| 		case product.LicenseKey: | ||||
| 			licenseService, ok := service.(product.LicenseService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.licenseService = licenseService | ||||
| 			boardsProd.licenseService = licenseService | ||||
| 		case product.FilestoreKey: | ||||
| 			filestoreService, ok := service.(product.FilestoreService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.filestoreService = filestoreService | ||||
| 			boardsProd.filestoreService = filestoreService | ||||
| 		case product.FileInfoStoreKey: | ||||
| 			fileInfoStoreService, ok := service.(product.FileInfoStoreService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.fileInfoStoreService = fileInfoStoreService | ||||
| 			boardsProd.fileInfoStoreService = fileInfoStoreService | ||||
| 		case product.RouterKey: | ||||
| 			routerService, ok := service.(product.RouterService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.routerService = routerService | ||||
| 			boardsProd.routerService = routerService | ||||
| 		case product.CloudKey: | ||||
| 			cloudService, ok := service.(product.CloudService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.cloudService = cloudService | ||||
| 			boardsProd.cloudService = cloudService | ||||
| 		case product.KVStoreKey: | ||||
| 			kvStoreService, ok := service.(product.KVStoreService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.kvStoreService = kvStoreService | ||||
| 			boardsProd.kvStoreService = kvStoreService | ||||
| 		case product.StoreKey: | ||||
| 			storeService, ok := service.(product.StoreService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.storeService = storeService | ||||
| 			boardsProd.storeService = storeService | ||||
| 		case product.SystemKey: | ||||
| 			systemService, ok := service.(product.SystemService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.systemService = systemService | ||||
| 			boardsProd.systemService = systemService | ||||
| 		case product.PreferencesKey: | ||||
| 			preferencesService, ok := service.(product.PreferencesService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.preferencesService = preferencesService | ||||
| 			boardsProd.preferencesService = preferencesService | ||||
| 		case product.HooksKey: | ||||
| 			hooksService, ok := service.(product.HooksService) | ||||
| 			if !ok { | ||||
| 				return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 				return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) | ||||
| 			} | ||||
| 			boards.hooksService = hooksService | ||||
| 			boardsProd.hooksService = hooksService | ||||
| 		} | ||||
| 	} | ||||
| 	return boards, nil | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (bp *boardsProduct) Start() error { | ||||
|   | ||||
							
								
								
									
										79
									
								
								mattermost-plugin/server/boards/boards_service_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								mattermost-plugin/server/boards/boards_service_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| package boards | ||||
|  | ||||
| import ( | ||||
| 	"github.com/mattermost/focalboard/server/app" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
|  | ||||
| 	mm_model "github.com/mattermost/mattermost-server/v6/model" | ||||
| 	"github.com/mattermost/mattermost-server/v6/product" | ||||
| ) | ||||
|  | ||||
| // boardsServiceAPI provides a service API for other products such as Channels. | ||||
| type boardsServiceAPI struct { | ||||
| 	app *app.App | ||||
| } | ||||
|  | ||||
| func NewBoardsServiceAPI(app *BoardsApp) *boardsServiceAPI { | ||||
| 	return &boardsServiceAPI{ | ||||
| 		app: app.server.App(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) GetTemplates(teamID string, userID string) ([]*model.Board, error) { | ||||
| 	return bs.app.GetTemplateBoards(teamID, userID) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) GetBoard(boardID string) (*model.Board, error) { | ||||
| 	return bs.app.GetBoard(boardID) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) CreateBoard(board *model.Board, userID string, addmember bool) (*model.Board, error) { | ||||
| 	return bs.app.CreateBoard(board, userID, addmember) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) PatchBoard(boardPatch *model.BoardPatch, boardID string, userID string) (*model.Board, error) { | ||||
| 	return bs.app.PatchBoard(boardPatch, boardID, userID) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) DeleteBoard(boardID string, userID string) error { | ||||
| 	return bs.app.DeleteBoard(boardID, userID) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) SearchBoards(searchTerm string, searchField model.BoardSearchField, | ||||
| 	userID string, includePublicBoards bool) ([]*model.Board, error) { | ||||
| 	return bs.app.SearchBoardsForUser(searchTerm, searchField, userID, includePublicBoards) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) LinkBoardToChannel(boardID string, channelID string, userID string) (*model.Board, error) { | ||||
| 	patch := &model.BoardPatch{ | ||||
| 		ChannelID: &channelID, | ||||
| 	} | ||||
| 	return bs.app.PatchBoard(patch, boardID, userID) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) GetCards(boardID string) ([]*model.Card, error) { | ||||
| 	return bs.app.GetCardsForBoard(boardID, 0, 0) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) GetCard(cardID string) (*model.Card, error) { | ||||
| 	return bs.app.GetCardByID(cardID) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) CreateCard(card *model.Card, boardID string, userID string) (*model.Card, error) { | ||||
| 	return bs.app.CreateCard(card, boardID, userID, false) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) PatchCard(cardPatch *model.CardPatch, cardID string, userID string) (*model.Card, error) { | ||||
| 	return bs.app.PatchCard(cardPatch, cardID, userID, false) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) DeleteCard(cardID string, userID string) error { | ||||
| 	return bs.app.DeleteBlock(cardID, userID) | ||||
| } | ||||
|  | ||||
| func (bs *boardsServiceAPI) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool { | ||||
| 	return bs.app.HasPermissionToBoard(userID, boardID, permission) | ||||
| } | ||||
|  | ||||
| // Ensure boardsServiceAPI implements product.BoardsService interface. | ||||
| var _ product.BoardsService = (*boardsServiceAPI)(nil) | ||||
| @@ -84,6 +84,7 @@ func NewBoardsApp(api model.ServicesAPI) (*BoardsApp, error) { | ||||
| 			return cluster.NewMutex(&mutexAPIAdapter{api: api}, name) | ||||
| 		}, | ||||
| 		ServicesAPI: api, | ||||
| 		ConfigFn:    api.GetConfig, | ||||
| 	} | ||||
|  | ||||
| 	var db store.Store | ||||
|   | ||||
| @@ -3,12 +3,12 @@ | ||||
|     align-items: center; | ||||
|     overflow: hidden; | ||||
|     flex-direction: row; | ||||
|     padding: 10px 0; | ||||
|     margin: 0 35px; | ||||
|     padding: 10px 35px 10px 0; | ||||
|  | ||||
|     .BoardSelectorItem-info { | ||||
|         flex: 1; | ||||
|         overflow: hidden; | ||||
|         padding-left: 35px; | ||||
|     } | ||||
|  | ||||
|     .icon { | ||||
|   | ||||
| @@ -64,6 +64,7 @@ type App struct { | ||||
| 	metrics             *metrics.Metrics | ||||
| 	notifications       *notify.Service | ||||
| 	logger              mlog.LoggerIFace | ||||
| 	permissions         permissions.PermissionsService | ||||
| 	blockChangeNotifier *utils.CallbackQueue | ||||
| 	servicesAPI         servicesAPI | ||||
|  | ||||
| @@ -90,6 +91,7 @@ func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) | ||||
| 		metrics:             services.Metrics, | ||||
| 		notifications:       services.Notifications, | ||||
| 		logger:              services.Logger, | ||||
| 		permissions:         services.Permissions, | ||||
| 		blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger), | ||||
| 		servicesAPI:         services.ServicesAPI, | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										9
									
								
								server/app/permissions.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/app/permissions.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	mm_model "github.com/mattermost/mattermost-server/v6/model" | ||||
| ) | ||||
|  | ||||
| func (a *App) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool { | ||||
| 	return a.permissions.HasPermissionToBoard(userID, boardID, permission) | ||||
| } | ||||
| @@ -105,6 +105,21 @@ type Board struct { | ||||
| 	DeleteAt int64 `json:"deleteAt"` | ||||
| } | ||||
|  | ||||
| // GetPropertyString returns the value of the specified property as a string, | ||||
| // or error if the property does not exist or is not of type string. | ||||
| func (b *Board) GetPropertyString(propName string) (string, error) { | ||||
| 	val, ok := b.Properties[propName] | ||||
| 	if !ok { | ||||
| 		return "", NewErrNotFound(propName) | ||||
| 	} | ||||
|  | ||||
| 	s, ok := val.(string) | ||||
| 	if !ok { | ||||
| 		return "", ErrInvalidPropertyValueType | ||||
| 	} | ||||
| 	return s, nil | ||||
| } | ||||
|  | ||||
| // BoardPatch is a patch for modify boards | ||||
| // swagger:model | ||||
| type BoardPatch struct { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import ( | ||||
|  | ||||
| 	sq "github.com/Masterminds/squirrel" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| 	"github.com/mattermost/mattermost-server/v6/store/sqlstore" | ||||
|  | ||||
| @@ -59,14 +60,14 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	db, err := sql.Open(s.dbType, connectionString) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	var settings mmModel.SqlSettings | ||||
| 	settings.SetDefaults(false) | ||||
| 	if s.configFn != nil { | ||||
| 		settings = s.configFn().SqlSettings | ||||
| 	} | ||||
| 	*settings.DriverName = s.dbType | ||||
|  | ||||
| 	if err = db.Ping(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	db := sqlstore.SetupConnection("master", connectionString, &settings) | ||||
|  | ||||
| 	return db, nil | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,7 @@ type Params struct { | ||||
| 	NewMutexFn       MutexFactory | ||||
| 	ServicesAPI      servicesAPI | ||||
| 	SkipMigrations   bool | ||||
| 	ConfigFn         func() *mmModel.Config | ||||
| } | ||||
|  | ||||
| func (p Params) CheckValid() error { | ||||
|   | ||||
| @@ -29,6 +29,7 @@ type SQLStore struct { | ||||
| 	servicesAPI      servicesAPI | ||||
| 	isBinaryParam    bool | ||||
| 	schemaName       string | ||||
| 	configFn         func() *mmModel.Config | ||||
| } | ||||
|  | ||||
| // MutexFactory is used by the store in plugin mode to generate | ||||
| @@ -53,6 +54,7 @@ func New(params Params) (*SQLStore, error) { | ||||
| 		isSingleUser:     params.IsSingleUser, | ||||
| 		NewMutexFn:       params.NewMutexFn, | ||||
| 		servicesAPI:      params.ServicesAPI, | ||||
| 		configFn:         params.ConfigFn, | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|   "BoardMember.unlinkChannel": "Verknüpfung aufheben", | ||||
|   "BoardPage.newVersion": "Eine neue Version von Boards ist verfügbar, klicke hier, um neu zu laden.", | ||||
|   "BoardPage.syncFailed": "Das Board kann gelöscht oder der Zugang entzogen werden.", | ||||
|   "BoardTemplateSelector.add-template": "Neue Vorlage", | ||||
|   "BoardTemplateSelector.add-template": "Neue Vorlage erstellen", | ||||
|   "BoardTemplateSelector.create-empty-board": "Leeres Board erstellen", | ||||
|   "BoardTemplateSelector.delete-template": "Löschen", | ||||
|   "BoardTemplateSelector.description": "Füge ein Board hinzu, indem du eine der unten definierten Vorlagen verwendest oder ganz neu beginnst.", | ||||
|   | ||||
| @@ -15,9 +15,15 @@ | ||||
|   "BoardPage.newVersion": "Có một phiên bản mới của bảng, click vào đây để nạp lại.", | ||||
|   "Calculations.Options.average.displayName": "Trung bình", | ||||
|   "Calculations.Options.average.label": "Trung bình", | ||||
|   "TableComponent.add-icon": "Thêm icon", | ||||
|   "TableComponent.name": "Tên", | ||||
|   "TableComponent.plus-new": "+ Mới", | ||||
|   "TableHeaderMenu.delete": "Xóa", | ||||
|   "share-board.publish": "Công khai", | ||||
|   "share-board.share": "Chia sẻ", | ||||
|   "shareBoard.channels-select-group": "Kênh", | ||||
|   "shareBoard.members-select-group": "Thành viên", | ||||
|   "tutorial_tip.finish_tour": "Xong" | ||||
|   "tutorial_tip.finish_tour": "Xong", | ||||
|   "tutorial_tip.got_it": "Đã hiểu", | ||||
|   "tutorial_tip.ok": "Tiếp theo" | ||||
| } | ||||
|   | ||||
| @@ -171,6 +171,7 @@ | ||||
|   "OnboardingTour.CopyLink.Title": "複製連結", | ||||
|   "OnboardingTour.OpenACard.Body": "打開卡片查看看板可以幫助你組織工作的優秀方法", | ||||
|   "OnboardingTour.OpenACard.Title": "瀏覽卡片", | ||||
|   "OnboardingTour.ShareBoard.Body": "您可以在內部、團隊內部分享看板,或公開發布讓組織外部查看。", | ||||
|   "OnboardingTour.ShareBoard.Title": "分享看板", | ||||
|   "PersonProperty.board-members": "看版成員", | ||||
|   "PersonProperty.non-board-members": "不是看板成員", | ||||
| @@ -190,25 +191,33 @@ | ||||
|   "PropertyType.Phone": "電話號碼", | ||||
|   "PropertyType.Select": "選取", | ||||
|   "PropertyType.Text": "文字框", | ||||
|   "PropertyType.Unknown": "未知", | ||||
|   "PropertyType.UpdatedBy": "最後更新者", | ||||
|   "PropertyType.UpdatedTime": "最後更新時間", | ||||
|   "PropertyType.Url": "網址", | ||||
|   "PropertyValueElement.empty": "空白", | ||||
|   "RegistrationLink.confirmRegenerateToken": "此動作將使先前分享的連結無效。確定要進行嗎?", | ||||
|   "RegistrationLink.copiedLink": "已複製!", | ||||
|   "RegistrationLink.copyLink": "複製連結", | ||||
|   "RegistrationLink.description": "將此連結分享給他人以建立帳號:", | ||||
|   "RegistrationLink.regenerateToken": "重新產生 token", | ||||
|   "RegistrationLink.tokenRegenerated": "已重新產生註冊鏈結", | ||||
|   "ShareBoard.PublishDescription": "發布只能讀取的連結", | ||||
|   "ShareBoard.PublishTitle": "發布至網路", | ||||
|   "ShareBoard.ShareInternal": "內部分享", | ||||
|   "ShareBoard.ShareInternalDescription": "擁有權限的使用者才能使用此連結", | ||||
|   "ShareBoard.Title": "分享看板", | ||||
|   "ShareBoard.confirmRegenerateToken": "此動作將使先前分享的鏈結無效。確定要進行嗎?", | ||||
|   "ShareBoard.copiedLink": "已複製!", | ||||
|   "ShareBoard.copyLink": "複製連結", | ||||
|   "ShareBoard.regenerate": "重新產生權杖", | ||||
|   "ShareBoard.searchPlaceholder": "查詢人和頻道", | ||||
|   "ShareBoard.teamPermissionsText": "在{teamName}的所有人", | ||||
|   "ShareBoard.tokenRegenrated": "已重新產生權杖", | ||||
|   "ShareBoard.userPermissionsRemoveMemberText": "移除成員", | ||||
|   "ShareBoard.userPermissionsYouText": "(你)", | ||||
|   "ShareTemplate.Title": "分享範本", | ||||
|   "ShareTemplate.searchPlaceholder": "查詢人", | ||||
|   "Sidebar.about": "關於 Focalboard", | ||||
|   "Sidebar.add-board": "+ 新增看板", | ||||
|   "Sidebar.changePassword": "變更密碼", | ||||
| @@ -219,14 +228,22 @@ | ||||
|   "Sidebar.import-archive": "匯入打包檔", | ||||
|   "Sidebar.invite-users": "邀請使用者", | ||||
|   "Sidebar.logout": "登出", | ||||
|   "Sidebar.no-boards-in-category": "沒有看板在裡面", | ||||
|   "Sidebar.product-tour": "產品導覽", | ||||
|   "Sidebar.random-icons": "隨機圖示", | ||||
|   "Sidebar.set-language": "設定語言", | ||||
|   "Sidebar.set-theme": "設定佈景主題", | ||||
|   "Sidebar.settings": "設定", | ||||
|   "Sidebar.template-from-board": "新的看板模板", | ||||
|   "Sidebar.untitled-board": "(無標題版面)", | ||||
|   "SidebarCategories.BlocksMenu.Move": "移動至…", | ||||
|   "SidebarCategories.CategoryMenu.CreateNew": "新增分類", | ||||
|   "SidebarCategories.CategoryMenu.Delete": "刪除分類", | ||||
|   "SidebarCategories.CategoryMenu.DeleteModal.Title": "刪除這個分類?", | ||||
|   "SidebarCategories.CategoryMenu.Update": "重新命名分類", | ||||
|   "SidebarTour.ManageCategories.Title": "管理分類", | ||||
|   "SidebarTour.SearchForBoards.Title": "查詢看板", | ||||
|   "SidebarTour.SidebarCategories.Link": "更多", | ||||
|   "TableComponent.add-icon": "加入圖示", | ||||
|   "TableComponent.name": "姓名", | ||||
|   "TableComponent.plus-new": "+ 新增", | ||||
| @@ -243,7 +260,16 @@ | ||||
|   "URLProperty.copiedLink": "已複製!", | ||||
|   "URLProperty.copy": "複製", | ||||
|   "URLProperty.edit": "編輯", | ||||
|   "UndoRedoHotKeys.canRedo": "重新執行", | ||||
|   "UndoRedoHotKeys.canRedo-with-description": "撤銷{description}", | ||||
|   "UndoRedoHotKeys.canUndo": "撤銷", | ||||
|   "UndoRedoHotKeys.canUndo-with-description": "重新執行 {description}", | ||||
|   "UndoRedoHotKeys.cannotRedo": "沒有可以重寫的", | ||||
|   "UndoRedoHotKeys.cannotUndo": "沒有可以取消的", | ||||
|   "ValueSelector.noOptions": "沒有選項.開始輸入第一個字!", | ||||
|   "ValueSelector.valueSelector": "值選擇器", | ||||
|   "ValueSelectorLabel.openMenu": "開啟選單", | ||||
|   "VersionMessage.help": "查看這個版本有什麼新功能.", | ||||
|   "View.AddView": "新增視圖", | ||||
|   "View.Board": "版面", | ||||
|   "View.DeleteView": "刪除視圖", | ||||
| @@ -336,5 +362,6 @@ | ||||
|   "tutorial_tip.finish_tour": "完成", | ||||
|   "tutorial_tip.got_it": "了解", | ||||
|   "tutorial_tip.ok": "下一步", | ||||
|   "tutorial_tip.out": "不接受這個提示." | ||||
|   "tutorial_tip.out": "不接受這個提示.", | ||||
|   "tutorial_tip.seen": "以前有見過嗎?" | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user