diff --git a/.eslintrc.js b/.eslintrc.js index 7051e50d2..53722eb4e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -74,6 +74,14 @@ module.exports = { 'prefer-template': 'error', 'prefer-destructuring': ['error', { array: false, object: true }], 'default-case': 'error', + 'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }], + 'react/self-closing-comp': [ + 'error', + { + component: true, + html: true, + }, + ], }, overrides: [ { @@ -85,5 +93,11 @@ module.exports = { 'dot-notation': 'off', }, }, + { + files: ['examples/sn-react-component-docs/**/*.{ts,tsx}'], + rules: { + 'require-jsdoc': 'off', + }, + }, ], } diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..32b6e4958 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +node \ No newline at end of file diff --git a/README.md b/README.md index 0147f04b7..b57bb1115 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ # sensenet client +[![Build Status](https://travis-ci.org/SenseNet/sn-client.svg?branch=master)](https://travis-ci.org/SenseNet/sn-client) +[![Coverage](https://img.shields.io/codecov/c/github/SenseNet/sn-client.svg?style=flat)](https://codecov.io/gh/SenseNet/sn-client) +[![Gitter chat](https://img.shields.io/gitter/room/SenseNet/sensenet.svg?style=flat)](https://gitter.im/SenseNet/sensenet) + This repository is a monorepo that we manage using [Lerna](https://github.com/lerna/lerna). That means that we actually publish [several packages](/packages) to npm from the same codebase, including: | Package | Version | @@ -20,6 +24,7 @@ This repository is a monorepo that we manage using [Lerna](https://github.com/le | [@sensenet/document-viewer-react](/packages/sn-document-viewer-react) | [![npm](https://img.shields.io/npm/v/@sensenet/document-viewer-react.svg?maxAge=3600)](https://www.npmjs.com/package/@sensenet/document-viewer-react) | | [@sensenet/icons-react](/packages/sn-icons-react) | [![npm](https://img.shields.io/npm/v/@sensenet/icons-react.svg?maxAge=3600)](https://www.npmjs.com/package/@sensenet/icons-react) | | [@sensenet/list-controls-react](/packages/sn-list-controls-react) | [![npm](https://img.shields.io/npm/v/@sensenet/list-controls-react.svg?maxAge=3600)](https://www.npmjs.com/package/@sensenet/list-controls-react) | +| [@sensenet/pickers-react](/packages/sn-pickers-react) | [![npm](https://img.shields.io/npm/v/@sensenet/pickers-react.svg?style=flat)](https://www.npmjs.com/package/@sensenet/pickers-react) | | [@sensenet/query](/packages/sn-query) | [![npm](https://img.shields.io/npm/v/@sensenet/query.svg?maxAge=3600)](https://www.npmjs.com/package/@sensenet/query) | | [@sensenet/redux-promise-middleware](/packages/sn-redux-promise-middleware) | [![npm](https://img.shields.io/npm/v/@sensenet/redux-promise-middleware.svg?maxAge=3600)](https://www.npmjs.com/package/@sensenet/redux-promise-middleware) | | [@sensenet/redux](/packages/sn-redux) | [![npm](https://img.shields.io/npm/v/@sensenet/redux.svg?maxAge=3600)](https://www.npmjs.com/package/@sensenet/redux) | diff --git a/apps/sensenet/package.json b/apps/sensenet/package.json index 391a84710..b58e44eb9 100644 --- a/apps/sensenet/package.json +++ b/apps/sensenet/package.json @@ -1,6 +1,6 @@ { "name": "@sensenet/sn-app", - "version": "0.4.0", + "version": "1.0.0", "main": "dist/index.js", "files": [ "dist", @@ -27,18 +27,18 @@ "homepage": "https://sensenet.com", "devDependencies": { "@types/autosuggest-highlight": "^3.1.0", - "@types/react": "^16.8.19", + "@types/react": "^16.8.23", "@types/react-autosuggest": "^9.3.8", - "@types/react-dom": "^16.8.4", - "@types/react-redux": "^7.0.9", + "@types/react-dom": "^16.8.5", + "@types/react-redux": "^7.1.1", "@types/react-responsive": "^3.0.3", - "@types/react-router": "^5.0.1", + "@types/react-router": "^5.0.3", "@types/react-router-dom": "^4.3.3", - "@types/uuid": "^3.4.4", - "autoprefixer": "^9.5.1", + "@types/uuid": "^3.4.5", + "autoprefixer": "^9.6.1", "awesome-typescript-loader": "^5.2.1", - "css-loader": "^2.1.0", - "file-loader": "^3.0.1", + "css-loader": "^3.1.0", + "file-loader": "^4.1.0", "git-revision-webpack-plugin": "^3.0.3", "html-webpack-plugin": "^3.2.0", "monaco-editor-webpack-plugin": "^1.7.0", @@ -48,38 +48,42 @@ "react-router-dom": "^5.0.0", "source-map-loader": "^0.2.4", "style-loader": "^0.23.1", - "ts-config-webpack-plugin": "^1.3.1", - "url-loader": "^1.1.2", - "webpack": "^4.32.2", - "webpack-bundle-analyzer": "^3.3.2", - "webpack-cli": "^3.3.2", + "ts-config-webpack-plugin": "^1.4.0", + "url-loader": "^2.1.0", + "webpack": "^4.38.0", + "webpack-bundle-analyzer": "^3.4.1", + "webpack-cli": "^3.3.6", "webpack-dev-server": "^3.4.1" }, "dependencies": { - "@furystack/inject": "^3.0.7", - "@furystack/logging": "^1.1.6", - "@material-ui/core": "^4.0.1", + "@furystack/inject": "^4.0.2", + "@furystack/logging": "^2.0.2", + "@material-ui/core": "^4.3.1", "@material-ui/icons": "^4.0.1", - "@sensenet/client-core": "^2.1.0", - "@sensenet/client-utils": "^1.6.2", - "@sensenet/controls-react": "^2.8.0", - "@sensenet/default-content-types": "^1.2.2", - "@sensenet/document-viewer-react": "^1.1.2", - "@sensenet/list-controls-react": "^1.3.8", - "@sensenet/pickers-react": "^1.2.1", - "@sensenet/query": "^1.1.7", - "@sensenet/redux": "^5.1.10", - "@sensenet/repository-events": "^1.4.2", + "@sensenet/authentication-google": "^2.0.11", + "@sensenet/authentication-jwt": "^1.0.15", + "@sensenet/client-core": "^2.2.0", + "@sensenet/client-utils": "^1.6.4", + "@sensenet/controls-react": "^3.1.0", + "@sensenet/default-content-types": "^2.0.0", + "@sensenet/document-viewer-react": "^1.2.0", + "@sensenet/icons-react": "^1.2.12", + "@sensenet/list-controls-react": "^1.3.10", + "@sensenet/pickers-react": "^1.2.3", + "@sensenet/query": "^1.1.8", + "@sensenet/redux": "^5.1.12", + "@sensenet/repository-events": "^1.4.4", "autosuggest-highlight": "^3.1.1", + "moment": "^2.24.0", "monaco-editor": "^0.17.0", "react": "^16.8.2", "react-autosuggest": "^9.4.3", "react-dom": "^16.8.2", "react-markdown": "^4.0.8", - "react-monaco-editor": "^0.26.1", + "react-monaco-editor": "^0.28.0", "react-redux": "^7.0.3", - "react-responsive": "^6.1.2", - "redux": "^4.0.1", + "react-responsive": "^7.0.0", + "redux": "^4.0.4", "redux-di-middleware": "^4.0.1", "semaphore-async-await": "^1.5.1", "uuid": "^3.3.2" diff --git a/apps/sensenet/src/assets/favicon.ico b/apps/sensenet/src/assets/favicon.ico new file mode 100644 index 000000000..fc0450626 Binary files /dev/null and b/apps/sensenet/src/assets/favicon.ico differ diff --git a/apps/sensenet/src/components/Breadcrumbs.tsx b/apps/sensenet/src/components/Breadcrumbs.tsx index 3994cdaa1..83baa4d4a 100644 --- a/apps/sensenet/src/components/Breadcrumbs.tsx +++ b/apps/sensenet/src/components/Breadcrumbs.tsx @@ -30,9 +30,9 @@ const Breadcrumbs: React.FunctionComponent - + {props.content.map((item, key) => ( - + + ) diff --git a/apps/sensenet/src/components/DropFileArea.tsx b/apps/sensenet/src/components/DropFileArea.tsx index 47bc7e804..6527120d5 100644 --- a/apps/sensenet/src/components/DropFileArea.tsx +++ b/apps/sensenet/src/components/DropFileArea.tsx @@ -1,12 +1,15 @@ import CloudUploadTwoTone from '@material-ui/icons/CloudUploadTwoTone' import { GenericContent } from '@sensenet/default-content-types' -import React, { useState, useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { UploadProgressInfo } from '@sensenet/client-core' import { ObservableValue } from '@sensenet/client-utils' import { useInjector, useRepository, useTheme } from '../hooks' import { UploadTracker } from '../services/UploadTracker' -export const DropFileArea: React.FunctionComponent<{ parent: GenericContent; style?: React.CSSProperties }> = props => { +export const DropFileArea: React.FunctionComponent<{ + parentContent: GenericContent + style?: React.CSSProperties +}> = props => { const [isDragOver, setDragOver] = useState(false) const injector = useInjector() @@ -55,7 +58,7 @@ export const DropFileArea: React.FunctionComponent<{ parent: GenericContent; sty createFolders: true, event: new DragEvent('drop', { dataTransfer: ev.dataTransfer }), overwrite: false, - parentPath: props.parent ? props.parent.Path : '', + parentPath: props.parentContent ? props.parentContent.Path : '', progressObservable, }) }}> diff --git a/apps/sensenet/src/components/Icon.tsx b/apps/sensenet/src/components/Icon.tsx index 6c9368eb6..e664883df 100644 --- a/apps/sensenet/src/components/Icon.tsx +++ b/apps/sensenet/src/components/Icon.tsx @@ -1,29 +1,51 @@ import { Injector } from '@furystack/inject' import { LogLevel } from '@furystack/logging' -import Avatar from '@material-ui/core/Avatar' -import BugReport from '@material-ui/icons/BugReport' -import CodeTwoTone from '@material-ui/icons/CodeTwoTone' -import CommentTwoTone from '@material-ui/icons/CommentTwoTone' -import DeleteTwoTone from '@material-ui/icons/DeleteTwoTone' -import Error from '@material-ui/icons/Error' -import FolderTwoTone from '@material-ui/icons/FolderTwoTone' -import FormatPaintTwoTone from '@material-ui/icons/FormatPaintTwoTone' -import GroupTwoTone from '@material-ui/icons/GroupTwoTone' -import Info from '@material-ui/icons/Info' -import InsertDriveFileTwoTone from '@material-ui/icons/InsertDriveFileTwoTone' -import ListAltTwoTone from '@material-ui/icons/ListAltTwoTone' -import PublicTwoTone from '@material-ui/icons/PublicTwoTone' -import SearchTwoTone from '@material-ui/icons/SearchTwoTone' -import SettingsTwoTone from '@material-ui/icons/SettingsTwoTone' -import Warning from '@material-ui/icons/Warning' -import WebAssetTwoTone from '@material-ui/icons/WebAssetTwoTone' +import { + AllInboxTwoTone, + AssignmentTwoTone, + BallotTwoTone, + BugReport, + CalendarTodayTwoTone, + CodeTwoTone, + CommentTwoTone, + DeleteTwoTone, + DescriptionTwoTone, + DomainTwoTone, + ErrorTwoTone, + EventTwoTone, + FolderOutlined, + FolderTwoTone, + FormatPaintTwoTone, + GridOnTwoTone, + GroupTwoTone, + Info, + InsertDriveFileTwoTone, + LanguageTwoTone, + LibraryBooksTwoTone, + LinkTwoTone, + ListAltTwoTone, + PersonTwoTone, + PhotoLibraryTwoTone, + PhotoTwoTone, + PictureAsPdfTwoTone, + PresentToAllTwoTone, + PublicTwoTone, + SearchTwoTone, + SettingsTwoTone, + TextFormat, + Warning, + WebAssetTwoTone, + WidgetsTwoTone, +} from '@material-ui/icons' import { PathHelper } from '@sensenet/client-utils' -import { File as SnFile, GenericContent, Schema, User } from '@sensenet/default-content-types' +import { GenericContent, File as SnFile, User } from '@sensenet/default-content-types' import React from 'react' import { Repository } from '@sensenet/client-core' +import { Avatar } from '@material-ui/core' import { useInjector, useRepository } from '../hooks' import { EventLogEntry } from '../services/EventService' import { isContentFromType } from '../utils/isContentFromType' +import { tuple } from '../utils/tuple' import { UserAvatar } from './UserAvatar' export interface IconOptions { @@ -93,6 +115,47 @@ export const defaultContentResolvers: Array> = [ { get: (item, options) => (item.Type === 'Search' ? : null) }, { get: (item, options) => (item.Type === 'Comment' ? : null) }, { get: (item, options) => (item.Type === 'EventLog' ? : null) }, + { get: (item, options) => (item.Type === 'ImageLibrary' ? : null) }, + { get: (item, options) => (item.Type === 'Image' ? : null) }, + { get: (item, options) => (item.Type === 'EventList' ? : null) }, + { get: (item, options) => (item.Type === 'CalendarEvent' ? : null) }, + { get: (item, options) => (item.Type === 'DocumentLibrary' ? : null) }, + { + get: (item, options) => + item.Type === 'File' && item.Icon === 'excel' ? : null, + }, + { + get: (item, options) => + item.Type === 'File' && item.Icon === 'word' ? : null, + }, + { + get: (item, options) => + item.Type === 'File' && item.Icon === 'powerpoint' ? : null, + }, + { + get: (item, options) => + item.Type === 'File' && item.Icon === 'adobe' ? : null, + }, + { get: (item, options) => (item.Type === 'LinkList' ? : null) }, + { get: (item, options) => (item.Type === 'Link' ? : null) }, + { get: (item, options) => (item.Type === 'MemoList' ? : null) }, + { get: (item, options) => (item.Type === 'Memo' ? : null) }, + { get: (item, options) => (item.Type === 'TaskList' ? : null) }, + { get: (item, options) => (item.Type === 'Task' ? : null) }, + { get: (item, options) => (item.Type === 'Domain' ? : null) }, + { get: (item, options) => (item.Type === 'User' ? : null) }, + { get: (item, options) => (item.Type === 'Group' ? : null) }, + { get: (item, options) => (item.Type === 'SystemFolder' ? : null) }, + { get: (item, options) => (item.Type === 'Resources' ? : null) }, + { get: (item, options) => (item.Type === 'Resource' ? : null) }, + { get: (item, options) => (item.Type === 'ContentType' ? : null) }, + { + get: (item, options) => (item.Type === 'OrganizationalUnit' ? : null), + }, + { + get: (item, options) => + item.Type && item.Type.indexOf('Workspace') > -1 ? : null, + }, { get: (item, options) => item.Type && item.Type.indexOf('Settings') !== -1 ? : null, @@ -117,16 +180,78 @@ export const defaultContentResolvers: Array> = [ }, ] -export const defaultSchemaResolvers: Array> = [ +export const wellKnownIconNames = tuple( + 'Folder', + 'File', + 'ImageLibrary', + 'EventList', + 'CalendarEvent', + 'DocumentLibrary', + 'LinkList', + 'Link', + 'MemoList', + 'Memo', + 'TaskList', + 'Task', + 'User', + 'Group', + 'ContentType', + 'SystemFolder', + 'Resource', + 'OrganizationalUnit', + 'Workspace', +) + +export const defaultSchemaResolvers: Array> = [ { get: (item, options) => { - return item.ContentTypeName === 'Folder' ? : null + switch (item.ContentTypeName) { + case 'Folder': + return + case 'File': + return + case 'ImageLibrary': + return + case 'EventList': + return + case 'CalendarEvent': + return + case 'DocumentLibrary': + return + case 'LinkList': + return + case 'Link': + return + case 'MemoList': + return + case 'Memo': + return + case 'TaskList': + return + case 'Task': + return + case 'User': + return + case 'Group': + return + case 'ContentType': + return + case 'SystemFolder': + return + case 'Resource': + return + case 'OrganizationalUnit': + return + default: + return null + } }, }, { - get: (item, options) => { - return item.ContentTypeName === 'File' ? : null - }, + get: (item, options) => + item.ContentTypeName && item.ContentTypeName.indexOf('Workspace') > -1 ? ( + + ) : null, }, ] @@ -134,7 +259,7 @@ export const defaultNotificationResolvers: Array { get: (item, options) => { return item.level === LogLevel.Fatal || item.level === LogLevel.Error ? ( - + ) : null }, }, diff --git a/apps/sensenet/src/components/Login.tsx b/apps/sensenet/src/components/Login.tsx deleted file mode 100644 index 2bee68ab5..000000000 --- a/apps/sensenet/src/components/Login.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import Button from '@material-ui/core/Button' -import CircularProgress from '@material-ui/core/CircularProgress' -import Divider from '@material-ui/core/Divider' -import Paper from '@material-ui/core/Paper' -import TextField from '@material-ui/core/TextField' -import Typography from '@material-ui/core/Typography' -import { ConstantContent, FormsAuthenticationService } from '@sensenet/client-core' -import { Retrier, sleepAsync } from '@sensenet/client-utils' -import React, { useEffect, useState } from 'react' -import { RouteComponentProps, withRouter } from 'react-router' -import { useInjector, useLocalization, usePersonalSettings, useRepository, useSession, useTheme } from '../hooks' -import { PersonalSettings } from '../services/PersonalSettings' -import { UserAvatar } from './UserAvatar' - -export const Login: React.FunctionComponent = props => { - const injector = useInjector() - const repo = useRepository() - const theme = useTheme() - const personalSettings = usePersonalSettings() - const session = useSession() - const settingsManager = injector.getInstance(PersonalSettings) - - const logger = injector.logger.withScope('LoginComponent') - - const existingRepo = personalSettings.repositories.find(r => r.url === repo.configuration.repositoryUrl) - - const [userName, setUserName] = useState((existingRepo && existingRepo.loginName) || '') - const [password, setPassword] = useState('') - const [url, setUrl] = useState(repo.configuration.repositoryUrl) - const [isInProgress, setIsInProgress] = useState(false) - - const [success, setSuccess] = useState(false) - const [progressValue, setProgressValue] = useState(0) - - const [error, setError] = useState() - - const localization = useLocalization().login - - useEffect(() => { - setUrl(repo.configuration.repositoryUrl) - existingRepo && existingRepo.loginName && setUserName(existingRepo.loginName) - }, [existingRepo, repo.configuration.repositoryUrl]) - - const handleSubmit = async (ev: React.FormEvent) => { - ev.preventDefault() - const repoToLogin = injector.getRepository(url) - personalSettings.lastRepository = url - try { - setIsInProgress(true) - const result = await repoToLogin.authentication.login(userName, password) - setSuccess(result) - if (result) { - setError(undefined) - const existing = personalSettings.repositories.find(i => i.url === url) - if (!existing) { - personalSettings.repositories.push({ url, loginName: userName }) - } else { - personalSettings.repositories = personalSettings.repositories.map(r => { - if (r.url === url) { - r.loginName = userName - } - return r - }) - } - ;(repoToLogin.authentication as FormsAuthenticationService).getCurrentUser() - settingsManager.setValue(personalSettings) - await Retrier.create( - async () => repoToLogin.authentication.currentUser.getValue().Id !== ConstantContent.VISITOR_USER.Id, - ) - .setup({ - timeoutMs: 60 * 1000, - RetryIntervalMs: 100, - }) - .run() - for (let index = 0; index < 100; index++) { - await sleepAsync(index / 50) - setProgressValue(index) - } - logger.information({ - message: localization.loginSuccessNotification.replace('{0}', userName).replace('{1}', url), - data: { - relatedContent: repoToLogin.authentication.currentUser.getValue(), - relatedRepository: repoToLogin.configuration.repositoryUrl, - }, - }) - if (props.match.path === '/login') { - await sleepAsync(1800) - props.history.push(`/${btoa(repoToLogin.configuration.repositoryUrl)}`) - } - } else { - setIsInProgress(false) - setError(localization.loginFailed) - logger.warning({ - message: localization.loginFailedNotification.replace('{0}', userName).replace('{1}', url), - }) - } - } catch (err) { - logger.error({ - message: localization.loginErrorNotification.replace('{0}', userName).replace('{1}', url), - data: { - details: { error: err }, - }, - }) - } - } - - return ( -
- - {localization.loginTitle} - {isInProgress ? ( -
-
- - {success ? ( - - ) : null} - - {success ? ( - <> - {localization.greetings.replace( - '{0}', - session.currentUser.DisplayName || session.currentUser.LoginName || session.currentUser.Name, - )} - - ) : ( - <>{localization.loggingInTo.replace('{0}', url)} - )} - -
-
- ) : ( -
- - { - setUserName(ev.target.value) - }} - /> - { - setPassword(ev.target.value) - }} - /> - { - setUrl(ev.target.value) - }} - /> - {error ? {error} : null} -
- -
- - )} -
-
- ) -} - -export default withRouter(Login) diff --git a/apps/sensenet/src/components/LogoutButton.tsx b/apps/sensenet/src/components/LogoutButton.tsx index 9082ed92b..1e1ca5d38 100644 --- a/apps/sensenet/src/components/LogoutButton.tsx +++ b/apps/sensenet/src/components/LogoutButton.tsx @@ -5,7 +5,6 @@ import DialogActions from '@material-ui/core/DialogActions' import DialogContent from '@material-ui/core/DialogContent' import DialogContentText from '@material-ui/core/DialogContentText' import DialogTitle from '@material-ui/core/DialogTitle' -import IconButton from '@material-ui/core/IconButton' import Tooltip from '@material-ui/core/Tooltip' import Typography from '@material-ui/core/Typography' import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew' @@ -39,12 +38,13 @@ export const LogoutButton: React.FunctionComponent<{
{session.state !== LoginState.Authenticated ? null : ( - { setShowLogout(true) - }}> + }} + style={{ minWidth: 20, paddingTop: 4, paddingBottom: 4, borderRadius: 0 }}> - + )} setShowLogout(false)}> diff --git a/apps/sensenet/src/components/MainRouter.tsx b/apps/sensenet/src/components/MainRouter.tsx index c517e1729..4707e14af 100644 --- a/apps/sensenet/src/components/MainRouter.tsx +++ b/apps/sensenet/src/components/MainRouter.tsx @@ -1,5 +1,5 @@ import { LoginState } from '@sensenet/client-core' -import React, { lazy, Suspense } from 'react' +import React, { lazy, Suspense, useEffect, useRef } from 'react' import { Route, RouteComponentProps, Switch, withRouter } from 'react-router' import { LoadSettingsContextProvider, RepositoryContext } from '../context' import { usePersonalSettings, useSession } from '../hooks' @@ -16,7 +16,7 @@ const SavedQueriesComponent = lazy( const IamComponent = lazy(async () => await import(/* webpackChunkName: "iam" */ './iam')) const SetupComponent = lazy(async () => await import(/* webpackChunkName: "setup" */ './setup')) -const LoginComponent = lazy(async () => await import(/* webpackChunkName: "Login" */ './Login')) +const LoginComponent = lazy(async () => await import(/* webpackChunkName: "Login" */ './login/Login')) const EditBinary = lazy(async () => await import(/* webpackChunkName: "editBinary" */ './edit/EditBinary')) const EditProperties = lazy(async () => await import(/* webpackChunkName: "editProperties" */ './edit/EditProperties')) const DocumentViewerComponent = lazy(async () => await import(/* webpackChunkName: "DocViewer" */ './DocViewer')) @@ -28,9 +28,27 @@ const PersonalSettingsEditor = lazy( async () => await import(/* webpackChunkName: "PersonalSettingsEditor" */ './edit/PersonalSettingsEditor'), ) -const MainRouter: React.StatelessComponent = () => { +const MainRouter: React.StatelessComponent = props => { const sessionContext = useSession() const personalSettings = usePersonalSettings() + const previousLocation = useRef() + + useEffect(() => { + const listen = props.history.listen(location => { + /** + * Do not add preview locations to previousLocation + * this way the user can go back to the location where she + * opened the viewer. + * */ + if (location.pathname.includes('/Preview')) { + return + } + previousLocation.current = location.pathname + }) + return () => { + listen() + } + }, [props.history]) return ( @@ -70,17 +88,17 @@ const MainRouter: React.StatelessComponent = () => { ) : sessionContext.state === LoginState.Authenticated ? ( { - return + path="/:repo/browse/:browseData?" + render={routeProps => { + return }} /> { + path="/:repo/search/:queryData?" + render={routeProps => { return ( - + ) }} @@ -129,26 +147,36 @@ const MainRouter: React.StatelessComponent = () => { { - return + return }} /> + { + return ( + + {repo => } + + ) + }} + /> { + render={routeParams => { return ( - {repo => } + {repo => } ) }} /> { - return + render={routeParams => { + return }} /> diff --git a/apps/sensenet/src/components/NotificationComponent.tsx b/apps/sensenet/src/components/NotificationComponent.tsx index 73e1ee680..b1f74e303 100644 --- a/apps/sensenet/src/components/NotificationComponent.tsx +++ b/apps/sensenet/src/components/NotificationComponent.tsx @@ -6,7 +6,7 @@ import Snackbar from '@material-ui/core/Snackbar' import Close from '@material-ui/icons/Close' import { sleepAsync } from '@sensenet/client-utils' import React, { useContext, useEffect, useState } from 'react' -import { ResponsiveContext, RepositoryContext } from '../context' +import { RepositoryContext, ResponsiveContext } from '../context' import { useInjector } from '../hooks' import { EventLogEntry, EventService } from '../services/EventService' import { RepositoryManager } from '../services/RepositoryManager' diff --git a/apps/sensenet/src/components/SecondaryActionsMenu.tsx b/apps/sensenet/src/components/SecondaryActionsMenu.tsx index fafac4d10..64196d6a6 100644 --- a/apps/sensenet/src/components/SecondaryActionsMenu.tsx +++ b/apps/sensenet/src/components/SecondaryActionsMenu.tsx @@ -1,44 +1,43 @@ import IconButton from '@material-ui/core/IconButton' import MoreHoriz from '@material-ui/icons/MoreHoriz' -import React, { useContext, useState } from 'react' -import { CurrentContentContext } from '../context' +import React, { useCallback, useRef, useState } from 'react' import { ContentContextMenu } from './ContentContextMenu' export const SecondaryActionsMenu: React.FunctionComponent<{ style?: React.CSSProperties }> = props => { - const content = useContext(CurrentContentContext) const [isOpened, setIsOpened] = useState(false) - const [ref, setRef] = useState(null) + const buttonRef = useRef(null) + + const onButtonClick = useCallback((ev: React.MouseEvent) => { + ev.preventDefault() + ev.stopPropagation() + setIsOpened(true) + }, []) + + const close = useCallback(() => setIsOpened(false), []) + const open = useCallback(() => setIsOpened(true), []) + const preventDefault = useCallback((ev: React.SyntheticEvent) => ev.preventDefault(), []) return (
- setRef(r)} - onClick={ev => { - ev.preventDefault() - ev.stopPropagation() - setRef(ev.currentTarget) - setIsOpened(true) - }}> + - - setIsOpened(true)} - onClose={() => setIsOpened(false)} - menuProps={{ - anchorEl: ref, - disablePortal: true, - BackdropProps: { - onClick: () => setIsOpened(false), - onContextMenu: ev => ev.preventDefault(), - }, - }} - drawerProps={{}} - /> - +
) } diff --git a/apps/sensenet/src/components/SelectionControl.tsx b/apps/sensenet/src/components/SelectionControl.tsx index d4dbcfd48..7a37e31e1 100644 --- a/apps/sensenet/src/components/SelectionControl.tsx +++ b/apps/sensenet/src/components/SelectionControl.tsx @@ -7,11 +7,8 @@ export const SelectionControl: React.FunctionComponent<{ isSelected: boolean; co isSelected, content, }) => ( -
- {isSelected ? ( - - ) : ( - - )} +
+ +
) diff --git a/apps/sensenet/src/components/UserAvatar.tsx b/apps/sensenet/src/components/UserAvatar.tsx index 17cabe4fb..091ef5791 100644 --- a/apps/sensenet/src/components/UserAvatar.tsx +++ b/apps/sensenet/src/components/UserAvatar.tsx @@ -21,6 +21,8 @@ export const UserAvatar: React.StatelessComponent<{ ) } return ( - {(props.user.DisplayName && props.user.DisplayName[0]) || props.user.Name[0]} + + {(props.user.DisplayName && props.user.DisplayName[0]) || (props.user.Name && props.user.Name[0]) || 'U'} + ) } diff --git a/apps/sensenet/src/components/appbar/DesktopAppBar.tsx b/apps/sensenet/src/components/appbar/DesktopAppBar.tsx index d0bc7c6e7..8ad70e716 100644 --- a/apps/sensenet/src/components/appbar/DesktopAppBar.tsx +++ b/apps/sensenet/src/components/appbar/DesktopAppBar.tsx @@ -3,24 +3,18 @@ import IconButton from '@material-ui/core/IconButton' import Toolbar from '@material-ui/core/Toolbar' import Menu from '@material-ui/icons/Menu' import React, { useContext } from 'react' -import { connect } from 'react-redux' import { ResponsiveContext, ResponsivePersonalSetttings } from '../../context' -import { useTheme } from '../../hooks' -import { rootStateType } from '../../store' +import { useCommandPalette, useTheme } from '../../hooks' import { CommandPalette } from '../command-palette/CommandPalette' import { RepositorySelector } from '../RepositorySelector' -const mapStateToProps = (state: rootStateType) => ({ - commandPaletteOpened: state.commandPalette.isOpened, -}) - -const DesktopAppBar: React.StatelessComponent< - ReturnType & { openDrawer?: () => void } -> = props => { +export const DesktopAppBar: React.FunctionComponent<{ openDrawer?: () => void }> = props => { const device = useContext(ResponsiveContext) const theme = useTheme() const personalSettings = useContext(ResponsivePersonalSetttings) + const commandPalette = useCommandPalette() + return ( @@ -31,7 +25,7 @@ const DesktopAppBar: React.StatelessComponent< textDecoration: 'none', overflow: 'hidden', alignItems: 'center', - flexGrow: props.commandPaletteOpened ? 0 : 1, + flexGrow: commandPalette.isOpened ? 0 : 1, }}> {personalSettings.drawer.type === 'temporary' ? ( ) : null} - {device !== 'desktop' && props.commandPaletteOpened ? null : } + {device !== 'desktop' && commandPalette.isOpened ? null : }
- {personalSettings.commandPalette.enabled ? :
} + {personalSettings.commandPalette.enabled ? :
} ) } - -const connectedComponent = connect(mapStateToProps)(DesktopAppBar) - -export { connectedComponent as DesktopAppBar } diff --git a/apps/sensenet/src/components/command-palette/CommandPalette.tsx b/apps/sensenet/src/components/command-palette/CommandPalette.tsx index 32e13ab8c..aaa84fc76 100644 --- a/apps/sensenet/src/components/command-palette/CommandPalette.tsx +++ b/apps/sensenet/src/components/command-palette/CommandPalette.tsx @@ -10,37 +10,14 @@ import Autosuggest, { SuggestionSelectedEventData, SuggestionsFetchRequestedParams, } from 'react-autosuggest' -import { connect } from 'react-redux' import { RouteComponentProps, withRouter } from 'react-router' import { LocalizationContext, RepositoryContext, ThemeContext } from '../../context' -import { rootStateType } from '../../store' -import { - clearItems, - close, - CommandPaletteItem, - open, - setInputValue, - updateItemsFromTerm, -} from '../../store/CommandPalette' +import { CommandPaletteItem, useCommandPalette } from '../../hooks' import { CommandPaletteHitsContainer } from './CommandPaletteHitsContainer' import { CommandPaletteSuggestion } from './CommandPaletteSuggestion' -const mapStateToProps = (state: rootStateType) => ({ - isOpened: state.commandPalette.isOpened, - items: state.commandPalette.items, - inputValue: state.commandPalette.inputValue, -}) - -const mapDispatchToProps = { - open, - close, - setInputValue, - updateItemsFromTerm, - clearItems, -} - export class CommandPaletteComponent extends React.Component< - ReturnType & typeof mapDispatchToProps & RouteComponentProps, + RouteComponentProps & ReturnType, { delayedOpened: boolean } > { private containerRef?: HTMLDivElement @@ -55,14 +32,14 @@ export class CommandPaletteComponent extends React.Component< ev.preventDefault() if (ev.shiftKey) { this.props.setInputValue('>') - this.props.open() + this.props.setIsOpened(true) } else { this.props.setInputValue('') - this.props.open() + this.props.setIsOpened(true) } } else { if (ev.key === 'Escape') { - this.props.close() + this.props.setIsOpened(false) } } } @@ -76,7 +53,8 @@ export class CommandPaletteComponent extends React.Component< } private handleSuggestionsFetchRequested = debounce((options: SuggestionsFetchRequestedParams, repo: Repository) => { - this.props.updateItemsFromTerm(options.value, repo) + this.props.setRepository(repo) + this.props.setInputValue(options.value) }, 200) public componentDidMount() { @@ -103,7 +81,7 @@ export class CommandPaletteComponent extends React.Component< } else { this.props.history.push(suggestion.suggestion.url) } - this.props.close() + this.props.setIsOpened(false) } private setDelayedOpenedState = debounce((value: boolean) => { @@ -131,11 +109,11 @@ export class CommandPaletteComponent extends React.Component< id: 'CommandBoxInput', autoFocus: true, spellCheck: false, - onBlur: this.props.close, + onBlur: () => this.props.setIsOpened(false), } return ( - + this.props.setIsOpened(false)}> {theme => (
{localization => ( - + this.props.setIsOpened(true)} style={{ padding: undefined }}> {'_'} @@ -194,7 +172,7 @@ export class CommandPaletteComponent extends React.Component< highlightFirstSuggestion={true} onSuggestionSelected={this.handleSelectSuggestion} onSuggestionsFetchRequested={e => this.handleSuggestionsFetchRequested(e, this.context)} - onSuggestionsClearRequested={this.props.clearItems} + onSuggestionsClearRequested={() => this.props.setItems([])} getSuggestionValue={s => s.primaryText} renderSuggestion={(s, params) => } renderSuggestionsContainer={s => ( @@ -214,10 +192,6 @@ export class CommandPaletteComponent extends React.Component< } } -const connectedComponent = withRouter( - connect( - mapStateToProps, - mapDispatchToProps, - )(CommandPaletteComponent), -) -export { connectedComponent as CommandPalette } +const routed = withRouter(CommandPaletteComponent) + +export { routed as CommandPalette } diff --git a/apps/sensenet/src/components/command-palette/CommandPaletteSuggestion.tsx b/apps/sensenet/src/components/command-palette/CommandPaletteSuggestion.tsx index e182b6868..d0eff8435 100644 --- a/apps/sensenet/src/components/command-palette/CommandPaletteSuggestion.tsx +++ b/apps/sensenet/src/components/command-palette/CommandPaletteSuggestion.tsx @@ -6,8 +6,8 @@ import parse from 'autosuggest-highlight/parse' import React, { useContext } from 'react' import { RenderSuggestionParams } from 'react-autosuggest' import { ResponsiveContext } from '../../context' -import { CommandPaletteItem } from '../../store/CommandPalette' import { Icon } from '../Icon' +import { CommandPaletteItem } from '../../hooks' export const getMatchParts = (hits: string[], term: string) => { const matchValueArr = match(term, hits.join(' ')) diff --git a/apps/sensenet/src/components/content-list/actions-field.tsx b/apps/sensenet/src/components/content-list/actions-field.tsx new file mode 100644 index 000000000..403836347 --- /dev/null +++ b/apps/sensenet/src/components/content-list/actions-field.tsx @@ -0,0 +1,13 @@ +import { IconButton, TableCell } from '@material-ui/core' +import React from 'react' +import MoreHoriz from '@material-ui/icons/MoreHoriz' + +export const ActionsField: React.FC<{ onOpen: (ev: React.MouseEvent) => void }> = ({ onOpen }) => { + return ( + + + + + + ) +} diff --git a/apps/sensenet/src/components/content-list/boolean-field.tsx b/apps/sensenet/src/components/content-list/boolean-field.tsx new file mode 100644 index 000000000..e8b2641d8 --- /dev/null +++ b/apps/sensenet/src/components/content-list/boolean-field.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { TableCell } from '@material-ui/core' +import Check from '@material-ui/icons/Check' +import Close from '@material-ui/icons/Close' + +export const BooleanField: React.FC<{ value?: boolean }> = ({ value }) => { + if (value === true) { + return ( + + + + ) + } else if (value === false) { + return ( + + + + ) + } + return null +} diff --git a/apps/sensenet/src/components/content-list/date-field.tsx b/apps/sensenet/src/components/content-list/date-field.tsx new file mode 100644 index 000000000..d4d1f15d0 --- /dev/null +++ b/apps/sensenet/src/components/content-list/date-field.tsx @@ -0,0 +1,9 @@ +import { TableCell } from '@material-ui/core' +import React from 'react' +import moment from 'moment' + +export const DateField: React.FC<{ date: string | Date }> = ({ date }) => ( + +
{moment(date).fromNow()}
+
+) diff --git a/apps/sensenet/src/components/content-list/description-field.tsx b/apps/sensenet/src/components/content-list/description-field.tsx new file mode 100644 index 000000000..9b239b01a --- /dev/null +++ b/apps/sensenet/src/components/content-list/description-field.tsx @@ -0,0 +1,8 @@ +import { TableCell } from '@material-ui/core' +import React from 'react' + +export const DescriptionField: React.FC<{ text: string }> = ({ text }) => ( + +
{text ? text.replace(/<(.|\n)*?>/g, '') : ''}
+
+) diff --git a/apps/sensenet/src/components/content-list/display-name-field.tsx b/apps/sensenet/src/components/content-list/display-name-field.tsx new file mode 100644 index 000000000..67bb592fa --- /dev/null +++ b/apps/sensenet/src/components/content-list/display-name-field.tsx @@ -0,0 +1,29 @@ +import { GenericContent } from '@sensenet/default-content-types' +import { TableCell } from '@material-ui/core' +import React from 'react' +import { CurrentContentContext, ResponsivePlatforms } from '../../context' +import { SecondaryActionsMenu } from '../SecondaryActionsMenu' + +export const DisplayNameComponent: React.FunctionComponent<{ + content: GenericContent + device: ResponsivePlatforms + isActive: boolean +}> = ({ content, device, isActive }) => { + return ( + +
+ {content.DisplayName || content.Name} + {device === 'mobile' && isActive ? ( + + + + ) : null} +
+
+ ) +} diff --git a/apps/sensenet/src/components/content-list/email-field.tsx b/apps/sensenet/src/components/content-list/email-field.tsx new file mode 100644 index 000000000..3a2411793 --- /dev/null +++ b/apps/sensenet/src/components/content-list/email-field.tsx @@ -0,0 +1,8 @@ +import { TableCell } from '@material-ui/core' +import React from 'react' + +export const EmailField: React.FC<{ mail: string }> = ({ mail }) => ( + + {mail} + +) diff --git a/apps/sensenet/src/components/content-list/icon-field.tsx b/apps/sensenet/src/components/content-list/icon-field.tsx new file mode 100644 index 000000000..c388436ed --- /dev/null +++ b/apps/sensenet/src/components/content-list/icon-field.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { TableCell } from '@material-ui/core' +import { GenericContent } from '@sensenet/default-content-types' +import { Icon } from '../Icon' + +export const IconField: React.FC<{ content: GenericContent }> = props => { + return ( + + + + ) +} diff --git a/apps/sensenet/src/components/content-list/index.tsx b/apps/sensenet/src/components/content-list/index.tsx new file mode 100644 index 000000000..c7750c16f --- /dev/null +++ b/apps/sensenet/src/components/content-list/index.tsx @@ -0,0 +1,376 @@ +import { debounce } from '@sensenet/client-utils' +import { GenericContent } from '@sensenet/default-content-types' +import { ContentList, DefaultCell } from '@sensenet/list-controls-react' +import React, { useCallback, useContext, useEffect, useState } from 'react' +import { Repository } from '@sensenet/client-core' +import { + CurrentAncestorsContext, + CurrentChildrenContext, + CurrentContentContext, + LoadSettingsContext, + ResponsiveContext, + ResponsivePersonalSetttings, +} from '../../context' +import { useRepository } from '../../hooks' +import { ContentBreadcrumbs } from '../ContentBreadcrumbs' +import { ContentContextMenu } from '../ContentContextMenu' +import { DeleteContentDialog } from '../dialogs' +import { DropFileArea } from '../DropFileArea' +import { SelectionControl } from '../SelectionControl' +import { IconField } from './icon-field' +import { EmailField } from './email-field' +import { PhoneField } from './phone-field' +import { DisplayNameComponent } from './display-name-field' +import { ActionsField } from './actions-field' +import { ReferenceField } from './reference-field' +import { BooleanField } from './boolean-field' +import { DateField } from './date-field' +import { DescriptionField } from './description-field' + +export interface CollectionComponentProps { + enableBreadcrumbs?: boolean + hideHeader?: boolean + disableSelection?: boolean + parentIdOrPath: number | string + onParentChange: (newParent: GenericContent) => void + onTabRequest: () => void + onActiveItemChange?: (item: GenericContent) => void + onActivateItem: (item: GenericContent) => void + style?: React.CSSProperties + containerRef?: (r: HTMLDivElement | null) => void + requestReload?: () => void + fieldsToDisplay?: Array + onSelectionChange?: (sel: GenericContent[]) => void + onFocus?: () => void + containerProps?: React.DetailedHTMLProps, HTMLDivElement> +} + +export const isReferenceField = (fieldName: string, repo: Repository) => { + const refWhiteList = ['AllowedChildTypes'] + const setting = repo.schemas.getSchemaByName('GenericContent').FieldSettings.find(f => f.Name === fieldName) + return refWhiteList.indexOf(fieldName) !== -1 || (setting && setting.Type === 'ReferenceFieldSetting') || false +} + +export const CollectionComponent: React.FunctionComponent = props => { + const parentContent = useContext(CurrentContentContext) + const children = useContext(CurrentChildrenContext) + const ancestors = useContext(CurrentAncestorsContext) + const device = useContext(ResponsiveContext) + const personalSettings = useContext(ResponsivePersonalSetttings) + const [activeContent, setActiveContent] = useState(children[0]) + const [selected, setSelected] = useState([]) + const [isFocused, setIsFocused] = useState(true) + const [isContextMenuOpened, setIsContextMenuOpened] = useState(false) + const [contextMenuAnchor, setContextMenuAnchor] = useState<{ top: number; left: number }>({ + top: 0, + left: 0, + }) + + const [showDelete, setShowDelete] = useState(false) + const repo = useRepository() + const loadSettings = useContext(LoadSettingsContext) + + const [currentOrder, setCurrentOrder] = useState( + ((loadSettings.loadChildrenSettings.orderby && + loadSettings.loadChildrenSettings.orderby[0][0]) as keyof GenericContent) || 'DisplayName', + ) + const [currentDirection, setCurrentDirection] = useState<'asc' | 'desc'>( + (loadSettings.loadChildrenSettings.orderby && (loadSettings.loadChildrenSettings.orderby[0][1] as any)) || 'asc', + ) + + useEffect(() => { + props.onActiveItemChange && props.onActiveItemChange(activeContent) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeContent]) + + useEffect(() => { + isFocused && props.onFocus && props.onFocus() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFocused]) + + useEffect(() => { + props.onSelectionChange && props.onSelectionChange(selected) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selected]) + + useEffect(() => { + loadSettings.setLoadChildrenSettings({ + ...loadSettings.loadChildrenSettings, + orderby: [[currentOrder as any, currentDirection as any]], + }) + // loadSettings can NOT be added :( + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [parentContent, currentOrder, currentDirection, personalSettings.content.fields, repo.schemas, isReferenceField]) + + useEffect(() => { + setSelected([]) + }, [parentContent.Id]) + + useEffect(() => { + setIsContextMenuOpened(false) + }, [children, activeContent, selected]) + + const [searchString, setSearchString] = useState('') + const runSearch = useCallback( + debounce(() => { + const child = children.find( + c => + c.Name.toLocaleLowerCase().indexOf(searchString) === 0 || + (c.DisplayName && c.DisplayName.toLocaleLowerCase().indexOf(searchString)) === 0, + ) + child && setActiveContent(child) + setSearchString('') + }, 500), + [], + ) + + const handleActivateItem = useCallback( + (item: GenericContent) => { + if (item.IsFolder) { + props.onParentChange(item) + } else { + props.onActivateItem(item) + } + }, + [props], + ) + + const handleItemClick = useCallback( + (ev: React.MouseEvent, content: GenericContent) => { + if (device !== 'desktop' && activeContent && activeContent.Id === content.Id) { + handleActivateItem(content) + return + } + if (ev.ctrlKey) { + if (selected.find(s => s.Id === content.Id)) { + setSelected(selected.filter(s => s.Id !== content.Id)) + } else { + setSelected([...selected, content]) + } + } else if (ev.shiftKey) { + const activeIndex = (activeContent && children.findIndex(s => s.Id === activeContent.Id)) || 0 + const clickedIndex = children.findIndex(s => s.Id === content.Id) + const newSelection = Array.from( + new Set([ + ...selected, + ...[...children].slice(Math.min(activeIndex, clickedIndex), Math.max(activeIndex, clickedIndex) + 1), + ]), + ) + setSelected(newSelection) + } else if (!selected.length || (selected.length === 1 && selected[0].Id !== content.Id)) { + setSelected([content]) + } + }, + [activeContent, children, device, handleActivateItem, selected], + ) + + const handleKeyDown = useCallback( + (ev: React.KeyboardEvent) => { + if (!activeContent) { + setActiveContent(children[0]) + } + switch (ev.key) { + case 'Home': + setActiveContent(children[0]) + break + case 'End': + setActiveContent(children[children.length - 1]) + break + case 'ArrowUp': + setActiveContent( + activeContent && children[Math.max(0, children.findIndex(c => c.Id === activeContent.Id) - 1)], + ) + break + case 'ArrowDown': + setActiveContent( + activeContent && + children[Math.min(children.findIndex(c => c.Id === activeContent.Id) + 1, children.length - 1)], + ) + break + case ' ': { + ev.preventDefault() + activeContent && selected.findIndex(s => s.Id === activeContent.Id) !== -1 + ? setSelected([...selected.filter(s => s.Id !== activeContent.Id)]) + : activeContent && setSelected([...selected, activeContent]) + break + } + case 'Insert': { + activeContent && selected.findIndex(s => s.Id === activeContent.Id) !== -1 + ? setSelected([...selected.filter(s => s.Id !== activeContent.Id)]) + : activeContent && setSelected([...selected, activeContent]) + activeContent && + setActiveContent( + children[Math.min(children.findIndex(c => c.Id === activeContent.Id) + 1, children.length)], + ) + break + } + case '*': { + if (selected.length === children.length) { + setSelected([]) + } else { + setSelected(children) + } + break + } + case 'Enter': { + activeContent && handleActivateItem(activeContent) + break + } + case 'Backspace': { + ancestors.length && props.onParentChange(ancestors[ancestors.length - 1]) + break + } + case 'Delete': { + setShowDelete(true) + break + } + case 'Tab': + ev.preventDefault() + props.onTabRequest() + break + default: + if (ev.key.length === 1) { + setSearchString(searchString + ev.key) + runSearch() + } + } + }, + [activeContent, ancestors, children, handleActivateItem, props, runSearch, searchString, selected], + ) + + return ( +
+ {props.enableBreadcrumbs ? props.onParentChange(i.content)} /> : null} + +
{ + setIsFocused(true) + }} + onBlur={ev => { + if (!ev.currentTarget.contains((ev as any).relatedTarget)) { + // Skip blurring on child focus + setIsFocused(false) + } + }} + ref={props.containerRef} + onKeyDown={handleKeyDown}> + { + setCurrentOrder(field) + setCurrentDirection(dir) + }} + onItemClick={handleItemClick} + onItemDoubleClick={(_ev, item) => handleActivateItem(item)} + displayRowCheckbox={!props.disableSelection} + getSelectionControl={(isSelected, content) => } + onItemContextMenu={(ev, item) => { + ev.preventDefault() + setActiveContent(item) + setContextMenuAnchor({ top: ev.clientY, left: ev.clientX }) + setIsContextMenuOpened(true) + }} + fieldComponent={fieldOptions => { + // eslint-disable-next-line default-case + switch (fieldOptions.field) { + case 'Icon': + return + case 'Email' as any: + return + case 'Phone' as any: + return + case 'DisplayName': + return ( + + ) + case 'Description': + return + case 'Actions': + return ( + { + ev.preventDefault() + ev.stopPropagation() + setActiveContent(fieldOptions.content) + setContextMenuAnchor({ top: ev.clientY, left: ev.clientX }) + setIsContextMenuOpened(true) + }} + /> + ) + // no default + } + if ( + fieldOptions.fieldSetting && + fieldOptions.fieldSetting.FieldClassName === 'SenseNet.ContentRepository.Fields.DateTimeField' + ) { + return + } + + if ( + typeof fieldOptions.content[fieldOptions.field] === 'object' && + isReferenceField(fieldOptions.field, repo) + ) { + const expectedContent = fieldOptions.content[fieldOptions.field] as GenericContent + if ( + expectedContent && + expectedContent.Id && + expectedContent.Type && + expectedContent.Name && + expectedContent.Path + ) { + return + } + return null + } + if (typeof fieldOptions.content[fieldOptions.field] === 'boolean') { + return + } + return + }} + fieldsToDisplay={props.fieldsToDisplay || personalSettings.content.fields || ['DisplayName']} + selected={selected} + onRequestSelectionChange={setSelected} + icons={{}} + /> + {activeContent ? ( + + setIsContextMenuOpened(false), + onContextMenu: ev => ev.preventDefault(), + }, + }} + isOpened={isContextMenuOpened} + onClose={() => setIsContextMenuOpened(false)} + onOpen={() => setIsContextMenuOpened(true)} + /> + + ) : null} +
+
+ setShowDelete(false) }} /> +
+ ) +} diff --git a/apps/sensenet/src/components/content-list/phone-field.tsx b/apps/sensenet/src/components/content-list/phone-field.tsx new file mode 100644 index 000000000..0feeeeddf --- /dev/null +++ b/apps/sensenet/src/components/content-list/phone-field.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { TableCell } from '@material-ui/core' + +export const PhoneField: React.FC<{ phoneNo: string }> = ({ phoneNo }) => ( + + {phoneNo} + +) diff --git a/apps/sensenet/src/components/content-list/reference-field.tsx b/apps/sensenet/src/components/content-list/reference-field.tsx new file mode 100644 index 000000000..1e1b4262c --- /dev/null +++ b/apps/sensenet/src/components/content-list/reference-field.tsx @@ -0,0 +1,17 @@ +import { TableCell } from '@material-ui/core' +import React from 'react' +import { GenericContent } from '@sensenet/default-content-types' +import { Icon } from '../Icon' + +export const ReferenceField: React.FC<{ content: GenericContent }> = ({ content }) => { + return ( + + {content.Name !== 'Somebody' ? ( +
+ +
{content.DisplayName || content.Name}
+
+ ) : null} +
+ ) +} diff --git a/apps/sensenet/src/components/content/Commander.tsx b/apps/sensenet/src/components/content/Commander.tsx index 69e3bef2f..b496fc047 100644 --- a/apps/sensenet/src/components/content/Commander.tsx +++ b/apps/sensenet/src/components/content/Commander.tsx @@ -1,7 +1,6 @@ import { ConstantContent } from '@sensenet/client-core' import { GenericContent } from '@sensenet/default-content-types' import React, { useEffect, useState } from 'react' -import { matchPath, RouteComponentProps, withRouter } from 'react-router' import { CurrentAncestorsProvider, CurrentChildrenProvider, @@ -9,69 +8,33 @@ import { CurrentContentProvider, LoadSettingsContextProvider, } from '../../context' -import { useContentRouting, useRepository, useSelectionService } from '../../hooks' +import { useRepository, useSelectionService } from '../../hooks' import { AddButton } from '../AddButton' -import { CollectionComponent } from '../ContentListPanel' +import { CollectionComponent } from '../content-list' import { AddDialog, CopyMoveDialog } from '../dialogs' -export interface CommanderRouteParams { - folderId?: string - rightParent?: string +export interface CommanderComponentProps { + leftParent: number | string + rightParent: number | string + onNavigateLeft: (newParent: GenericContent) => void + onNavigateRight: (newParent: GenericContent) => void + onActivateItem: (item: GenericContent) => void + fieldsToDisplay?: Array + + rootPath: string } -export const CommanderComponent: React.FunctionComponent> = props => { - const contentRouter = useContentRouting() +export const CommanderComponent: React.FunctionComponent = props => { const repo = useRepository() const selectionService = useSelectionService() - const getLeftFromPath = (params: CommanderRouteParams) => - parseInt(params.folderId as string, 10) || ConstantContent.PORTAL_ROOT.Id - const getRightFromPath = (params: CommanderRouteParams) => - parseInt(params.rightParent as string, 10) || ConstantContent.PORTAL_ROOT.Id - - const [leftParentId, setLeftParentId] = useState(getLeftFromPath(props.match.params)) - const [rightParentId, setRightParentId] = useState(getRightFromPath(props.match.params)) - const [_leftPanelRef, setLeftPanelRef] = useState(null) const [_rightPanelRef, setRightPanelRef] = useState(null) const [activePanel, setActivePanel] = useState<'left' | 'right'>('left') const [activeParent, setActiveParent] = useState(null as any) - useEffect(() => { - const historyChangeListener = props.history.listen(location => { - const match = matchPath(location.pathname, props.match.path) - if (match) { - if (getLeftFromPath(match.params) !== leftParentId) { - setLeftParentId(getLeftFromPath(match.params)) - } - if (getRightFromPath(match.params) !== rightParentId) { - setRightParentId(getRightFromPath(match.params)) - } - } - }) - return () => { - historyChangeListener() - } - }, [leftParentId, props.history, props.match.path, rightParentId]) - - useEffect(() => { - if ( - props.match.params.folderId !== leftParentId.toString() || - props.match.params.rightParent !== rightParentId.toString() - ) { - props.history.push(`/${btoa(repo.configuration.repositoryUrl)}/browse/${leftParentId}/${rightParentId}`) - } - }, [ - leftParentId, - props.history, - props.match.params.folderId, - props.match.params.rightParent, - repo.configuration.repositoryUrl, - rightParentId, - ]) - const [isCopyOpened, setIsCopyOpened] = useState(false) const [copyMoveOperation, setCopyMoveOperation] = useState<'copy' | 'move'>('copy') const [copySelection, setCopySelection] = useState([ConstantContent.PORTAL_ROOT]) @@ -114,7 +77,7 @@ export const CommanderComponent: React.FunctionComponent - + {lp => { setLeftParent(lp) @@ -122,32 +85,29 @@ export const CommanderComponent: React.FunctionComponent - + { setActivePanel('left') }} enableBreadcrumbs={true} - onActivateItem={item => { - props.history.push(contentRouter.getPrimaryActionUrl(item)) - }} + onActivateItem={props.onActivateItem} containerRef={r => setLeftPanelRef(r)} style={{ width: '100%', maxHeight: '100%' }} - parentId={leftParentId} - onParentChange={p => { - setLeftParentId(p.Id) - }} + parentIdOrPath={props.leftParent} + onParentChange={props.onNavigateLeft} onSelectionChange={sel => { setLeftSelection(sel) selectionService.selection.setValue(sel) }} onTabRequest={() => _rightPanelRef && _rightPanelRef.focus()} onActiveItemChange={item => selectionService.activeContent.setValue(item)} + fieldsToDisplay={props.fieldsToDisplay} /> - + {rp => { setRightParent(rp) @@ -161,21 +121,18 @@ export const CommanderComponent: React.FunctionComponent { setActivePanel('right') }} - onActivateItem={item => { - props.history.push(contentRouter.getPrimaryActionUrl(item)) - }} + onActivateItem={props.onActivateItem} containerRef={r => setRightPanelRef(r)} - parentId={rightParentId} + parentIdOrPath={props.rightParent} style={{ width: '100%', borderLeft: '1px solid rgba(255,255,255,0.3)', maxHeight: '100%' }} - onParentChange={p2 => { - setRightParentId(p2.Id) - }} + onParentChange={props.onNavigateRight} onSelectionChange={sel => { setRightSelection(sel) selectionService.selection.setValue(sel) }} onTabRequest={() => _leftPanelRef && _leftPanelRef.focus()} onActiveItemChange={item => selectionService.activeContent.setValue(item)} + fieldsToDisplay={props.fieldsToDisplay} /> @@ -210,4 +167,4 @@ export const CommanderComponent: React.FunctionComponent> = props => { - const getLeftFromPath = (params: CommanderRouteParams) => - parseInt(params.folderId as string, 10) || ConstantContent.PORTAL_ROOT.Id +export interface ExploreComponentProps { + parent: number | string + onNavigate: (newParent: GenericContent) => void + onActivateItem: (item: GenericContent) => void + fieldsToDisplay?: Array + rootPath?: string +} +export const Explore: React.FunctionComponent = props => { const selectionService = useSelectionService() - const contentRouter = useContentRouting() - const [leftParentId, setLeftParentId] = useState(getLeftFromPath(props.match.params)) - useEffect(() => { - const historyChangeListener = props.history.listen(location => { - const match = matchPath(location.pathname, props.match.path) - if (match) { - if (getLeftFromPath(match.params) !== leftParentId) { - setLeftParentId(getLeftFromPath(match.params)) - } - } - }) - return () => { - historyChangeListener() - } - }, [leftParentId, props.history, props.match.path]) - return (
- + - - + +
+ props.onNavigate(i.content)} /> +
{ selectionService.activeContent.setValue(item) - setLeftParentId(item.Id) - props.history.push(contentRouter.getPrimaryActionUrl(item)) + props.onNavigate(item) }} - activeItemId={leftParentId} + activeItemIdOrPath={props.parent} /> { - props.history.push(contentRouter.getPrimaryActionUrl(item)) - }} + onActivateItem={props.onActivateItem} style={{ flexGrow: 7, flexShrink: 0, maxHeight: '100%' }} - onParentChange={p => { - setLeftParentId(p.Id) - props.history.push(contentRouter.getPrimaryActionUrl(p)) - }} + onParentChange={props.onNavigate} onSelectionChange={sel => { selectionService.selection.setValue(sel) }} - parentId={leftParentId} + parentIdOrPath={props.parent} onTabRequest={() => { /** */ }} + fieldsToDisplay={props.fieldsToDisplay} onActiveItemChange={item => selectionService.activeContent.setValue(item)} /> @@ -92,7 +77,3 @@ export const ExploreComponent: React.FunctionComponent ) } - -const routed = withRouter(ExploreComponent) - -export { routed as Explore } diff --git a/apps/sensenet/src/components/content/Simple.tsx b/apps/sensenet/src/components/content/Simple.tsx index 8de878cdf..d2a71e9f1 100644 --- a/apps/sensenet/src/components/content/Simple.tsx +++ b/apps/sensenet/src/components/content/Simple.tsx @@ -1,55 +1,38 @@ -import { ConstantContent } from '@sensenet/client-core' -import React, { useEffect, useState } from 'react' -import { matchPath, RouteComponentProps, withRouter } from 'react-router' +import React from 'react' +import { GenericContent } from '@sensenet/default-content-types' import { CurrentAncestorsProvider, CurrentChildrenProvider, CurrentContentProvider, LoadSettingsContextProvider, } from '../../context' -import { useContentRouting, useSelectionService } from '../../hooks' +import { useSelectionService } from '../../hooks' import { AddButton } from '../AddButton' -import { CollectionComponent } from '../ContentListPanel' -import { CommanderRouteParams } from './Commander' +import { CollectionComponent } from '../content-list' -export const SimpleListComponent: React.FunctionComponent> = props => { - const getLeftFromPath = (params: CommanderRouteParams) => - parseInt(params.folderId as string, 10) || ConstantContent.PORTAL_ROOT.Id - const [leftParentId, setLeftParentId] = useState(getLeftFromPath(props.match.params)) - const contentRouter = useContentRouting() - const selectionService = useSelectionService() +export interface SimpleListComponentProps { + parent: number | string + onNavigate: (newParent: GenericContent) => void + onActivateItem: (item: GenericContent) => void + fieldsToDisplay?: Array + rootPath?: string +} - useEffect(() => { - const historyChangeListener = props.history.listen(location => { - const match = matchPath(location.pathname, props.match.path) - if (match) { - if (getLeftFromPath(match.params) !== leftParentId) { - setLeftParentId(getLeftFromPath(match.params)) - } - } - }) - return () => { - historyChangeListener() - } - }, [leftParentId, props.history, props.match.path]) +export const SimpleList: React.FunctionComponent = props => { + const selectionService = useSelectionService() return (
- + - + { - props.history.push(contentRouter.getPrimaryActionUrl(item)) - }} + onActivateItem={props.onActivateItem} style={{ flexGrow: 1, flexShrink: 0, maxHeight: '100%', width: '100%' }} - onParentChange={p => { - setLeftParentId(p.Id) - props.history.push(contentRouter.getPrimaryActionUrl(p)) - }} - parentId={leftParentId} + onParentChange={props.onNavigate} + parentIdOrPath={props.parent} onTabRequest={() => { /** */ }} @@ -57,6 +40,7 @@ export const SimpleListComponent: React.FunctionComponent selectionService.activeContent.setValue(item)} + fieldsToDisplay={props.fieldsToDisplay} /> @@ -66,6 +50,3 @@ export const SimpleListComponent: React.FunctionComponent ) } - -const connected = withRouter(SimpleListComponent) -export { connected as SimpleList } diff --git a/apps/sensenet/src/components/content/index.tsx b/apps/sensenet/src/components/content/index.tsx index 21499187a..ecaf25518 100644 --- a/apps/sensenet/src/components/content/index.tsx +++ b/apps/sensenet/src/components/content/index.tsx @@ -1,20 +1,119 @@ -import React, { useContext } from 'react' +import React, { useCallback, useContext, useEffect, useState } from 'react' +import { RouteComponentProps } from 'react-router' +import { GenericContent } from '@sensenet/default-content-types' +import { ConstantContent } from '@sensenet/client-core' import { ResponsivePersonalSetttings } from '../../context' +import { useContentRouting, useLogger, useRepository } from '../../hooks' +import { tuple } from '../../utils/tuple' import Commander from './Commander' import { Explore } from './Explore' import { SimpleList } from './Simple' -export const Content: React.FunctionComponent = () => { - const personalSettings = useContext(ResponsivePersonalSetttings) - - if (personalSettings.content.browseType === 'commander') { - return - } else if (personalSettings.content.browseType === 'explorer') { - return - } else if (personalSettings.content.browseType === 'simple') { - return - } - return null +export const BrowseType = tuple('commander', 'explorer', 'simple') + +export interface BrowseData { + type?: typeof BrowseType[number] + root?: string + currentContent?: number | string + secondaryContent?: number | string // right parent + fieldsToDisplay?: Array +} + +export const encodeBrowseData = (data: BrowseData) => encodeURIComponent(btoa(JSON.stringify(data))) +export const decodeBrowseData = (encoded: string) => JSON.parse(atob(decodeURIComponent(encoded))) as BrowseData + +export const Content: React.FunctionComponent> = props => { + const repo = useRepository() + const settings = useContext(ResponsivePersonalSetttings) + const logger = useLogger('Browse view') + + const [browseData, setBrowseData] = useState({ + type: settings.content.browseType, + }) + const contentRouter = useContentRouting() + + useEffect(() => { + try { + const data = decodeBrowseData(props.match.params.browseData) + setBrowseData({ + ...browseData, + ...data, + }) + } catch (error) { + logger.warning({ message: 'Wrong link :(' }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [logger, props.match.params.browseData]) + + const refreshUrl = useCallback( + (data: BrowseData) => { + props.history.push(`/${btoa(repo.configuration.repositoryUrl)}/browse/${encodeBrowseData(data)}`) + }, + [props.history, repo.configuration.repositoryUrl], + ) + + const navigate = useCallback( + (itm: GenericContent) => { + const newBrowseData = { + ...browseData, + currentContent: itm.Id, + } + setBrowseData(newBrowseData) + refreshUrl(newBrowseData) + }, + [browseData, refreshUrl], + ) + + const navigateSecondary = useCallback( + (itm: GenericContent) => { + const newBrowseData = { + ...browseData, + secondaryContent: itm.Id, + } + setBrowseData(newBrowseData) + refreshUrl(newBrowseData) + }, + [browseData, refreshUrl], + ) + + const openItem = useCallback( + (itm: GenericContent) => { + props.history.push(contentRouter.getPrimaryActionUrl(itm)) + }, + [contentRouter, props.history], + ) + + return ( + <> + {browseData.type === 'commander' ? ( + + ) : browseData.type === 'explorer' ? ( + + ) : ( + + )} + + ) } export default Content diff --git a/apps/sensenet/src/components/dashboard/index.tsx b/apps/sensenet/src/components/dashboard/index.tsx index 3dd521893..0f770157a 100644 --- a/apps/sensenet/src/components/dashboard/index.tsx +++ b/apps/sensenet/src/components/dashboard/index.tsx @@ -1,10 +1,13 @@ import Paper from '@material-ui/core/Paper' -import React from 'react' +import React, { useContext, useState } from 'react' import { Repository } from '@sensenet/client-core' +import { RouteComponentProps } from 'react-router-dom' import { useWidgets } from '../../hooks' +import { ResponsiveContext } from '../../context' import { ErrorWidget } from './error-widget' import { QueryWidget } from './query-widget' import { MarkdownWidget } from './markdown-widget' +import { UpdatesWidget } from './updates-widget' export interface DashboardProps { repository?: Repository @@ -19,27 +22,40 @@ export const getWidgetComponent = (widget: ReturnType[0], rep return } return + case 'updates': + if (!repo) { + return + } + return default: return } } -const Dashboard: React.FunctionComponent = ({ repository }) => { - const widgets = useWidgets(repository) +const Dashboard: React.FunctionComponent> = ({ + repository, + match, +}) => { + const widgets = useWidgets(repository, match.params.dashboardName) + const platform = useContext(ResponsiveContext) + const [defaultMinWidth] = useState(250) return (
{widgets.map((widget, i) => { const widgetComponent = getWidgetComponent(widget, repository) + const width = widget.minWidth + ? widget.minWidth[platform] || widget.minWidth.default || defaultMinWidth + : defaultMinWidth return ( {widgetComponent} diff --git a/apps/sensenet/src/components/dashboard/markdown-widget.tsx b/apps/sensenet/src/components/dashboard/markdown-widget.tsx index 443a587c3..b9f198998 100644 --- a/apps/sensenet/src/components/dashboard/markdown-widget.tsx +++ b/apps/sensenet/src/components/dashboard/markdown-widget.tsx @@ -2,8 +2,12 @@ import React from 'react' import { Typography } from '@material-ui/core' import ReactMarkdown from 'react-markdown' import { MarkdownWidget as MarkdownWidgetModel } from '../../services/PersonalSettings' +import { useStringReplace } from '../../hooks' export const MarkdownWidget: React.FunctionComponent = props => { + const replacedContent = useStringReplace(props.settings.content) + const replacedTitle = useStringReplace(props.title) + return (
= prop title={props.title} gutterBottom={true} style={{ whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}> - {props.title} + {replacedTitle}
- +
) diff --git a/apps/sensenet/src/components/dashboard/query-widget.tsx b/apps/sensenet/src/components/dashboard/query-widget.tsx index 31ef027ae..b2e2a7e34 100644 --- a/apps/sensenet/src/components/dashboard/query-widget.tsx +++ b/apps/sensenet/src/components/dashboard/query-widget.tsx @@ -1,91 +1,180 @@ -import React, { useState, useEffect } from 'react' -import { Typography } from '@material-ui/core' +import React, { useEffect, useState } from 'react' +import { IconButton, Tooltip, Typography } from '@material-ui/core' +import Refresh from '@material-ui/icons/RefreshTwoTone' +import OpenInNewTwoTone from '@material-ui/icons/OpenInNewTwoTone' import { GenericContent } from '@sensenet/default-content-types' import { ConstantContent, ODataParams } from '@sensenet/client-core' import { RouteComponentProps, withRouter } from 'react-router' import { QueryWidget as QueryWidgetModel } from '../../services/PersonalSettings' -import { useRepository, useContentRouting } from '../../hooks' -import { CollectionComponent, isReferenceField } from '../ContentListPanel' +import { useContentRouting, useLocalization, useRepository, useSelectionService, useStringReplace } from '../../hooks' +import { CollectionComponent, isReferenceField } from '../content-list' import { - CurrentContentContext, - CurrentChildrenContext, CurrentAncestorsContext, + CurrentChildrenContext, + CurrentContentContext, LoadSettingsContext, } from '../../context' +import { encodeQueryData } from '../search' const QueryWidget: React.FunctionComponent & RouteComponentProps> = props => { const [items, setItems] = useState([]) const [loadChildrenSettings, setLoadChildrenSettings] = useState>({}) + const [error, setError] = useState('') + const [refreshToken, setRefreshToken] = useState(Math.random()) + const [count, setCount] = useState(0) const repo = useRepository() const contentRouter = useContentRouting() + const replacedTitle = useStringReplace(props.title) + const localization = useLocalization().dashboard + const selectionService = useSelectionService() useEffect(() => { setLoadChildrenSettings({ query: props.settings.query, - top: props.settings.top, + top: props.settings.countOnly ? 1 : props.settings.top, + inlinecount: 'allpages', select: ['Actions', ...props.settings.columns], expand: ['Actions', ...props.settings.columns.filter(f => isReferenceField(f, repo))], }) - }, [props.settings.columns, props.settings.query, props.settings.top, repo]) + }, [props.settings.columns, props.settings.countOnly, props.settings.query, props.settings.top, repo]) useEffect(() => { + const ac = new AbortController() if (loadChildrenSettings.query) { ;(async () => { /** */ - const result = await repo.loadCollection({ - path: ConstantContent.PORTAL_ROOT.Path, - oDataOptions: loadChildrenSettings, - }) - setItems(result.d.results) + try { + setError('') + const result = await repo.loadCollection({ + path: ConstantContent.PORTAL_ROOT.Path, + oDataOptions: loadChildrenSettings, + requestInit: { + signal: ac.signal, + }, + }) + setCount(result.d.__count) + setItems(result.d.results) + } catch (e) { + if (!ac.signal.aborted) { + setError(e.toString()) + } + } })() + return () => ac.abort() } - }, [repo, props.settings.query, props.settings.top, loadChildrenSettings]) + }, [repo, loadChildrenSettings, refreshToken]) return ( -
- - {props.title} - - - - - - { - setLoadChildrenSettings({ - ...loadChildrenSettings, - orderby: newSettings.orderby, - }) - }, - setLoadSettings: () => ({}), - setLoadAncestorsSettings: () => ({}), - }}> - { - // props.history.push(contentRouter.getPrimaryActionUrl(p)) - }} - onActivateItem={p => { - props.history.push(contentRouter.getPrimaryActionUrl(p)) - }} - onTabRequest={() => { - /** */ - }} - /> - - - - +
+
+ {replacedTitle ? ( + + + {replacedTitle} + + + ) : null} +
+ {props.settings.showRefresh ? ( + + setRefreshToken(Math.random())} style={{ padding: '0', margin: '0 0 0 1em' }}> + + + + ) : null} + {props.settings.showOpenInSearch ? ( + + + props.history.push( + `/${btoa(repo.configuration.repositoryUrl)}/search/${encodeQueryData({ + term: props.settings.query, + })}`, + ) + }> + + + + ) : null} +
+ {props.settings.countOnly ? ( +
+ {count} +
+ ) : ( + + + + { + setLoadChildrenSettings({ + ...loadChildrenSettings, + orderby: newSettings.orderby, + }) + }, + setLoadSettings: () => ({}), + setLoadAncestorsSettings: () => ({}), + }}> + { + // props.history.push(contentRouter.getPrimaryActionUrl(p)) + }} + onActivateItem={p => { + props.history.push(contentRouter.getPrimaryActionUrl(p)) + }} + onTabRequest={() => { + /** */ + }} + onSelectionChange={sel => { + selectionService.selection.setValue(sel) + }} + onActiveItemChange={item => { + selectionService.activeContent.setValue(item) + }} + /> + {error ? {error} : null} + {items && items.length === 0 && props.settings.emptyPlaceholderText ? ( +
+ {props.settings.emptyPlaceholderText} +
+ ) : null} +
+
+
+
+ )}
) } diff --git a/apps/sensenet/src/components/dashboard/updates-widget.tsx b/apps/sensenet/src/components/dashboard/updates-widget.tsx new file mode 100644 index 000000000..a6b0b5e38 --- /dev/null +++ b/apps/sensenet/src/components/dashboard/updates-widget.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import WbSunnyTwoTone from '@material-ui/icons/WbSunnyTwoTone' +import { + Button, + List, + ListItem, + ListItemAvatar, + ListItemSecondaryAction, + ListItemText, + Typography, +} from '@material-ui/core' +import { Widget } from '../../services/PersonalSettings' +import { useLocalization, useStringReplace, useTheme, useVersionInfo } from '../../hooks' + +export const UpdatesWidget: React.FunctionComponent> = props => { + const replacedTitle = useStringReplace(props.title) + const { hasUpdates, versionInfo } = useVersionInfo() + const localization = useLocalization().dashboard.updates + const theme = useTheme() + + return ( +
+ + {replacedTitle} + +
+ {hasUpdates ? ( + + {versionInfo && + versionInfo.Components.filter(v => v.IsUpdateAvailable).map(info => ( + + +
+ + + + + + + + + ))} + + ) : ( +
+ + {localization.allUpToDate} + + +
+ )} +
+
+ ) +} diff --git a/apps/sensenet/src/components/dialogs/add.tsx b/apps/sensenet/src/components/dialogs/add.tsx index fe446f295..befb8d57a 100644 --- a/apps/sensenet/src/components/dialogs/add.tsx +++ b/apps/sensenet/src/components/dialogs/add.tsx @@ -27,9 +27,7 @@ export const AddDialog: React.FunctionComponent = ({ dialogProps handleCancel={handleClose} repository={repo} contentTypeName={schema.ContentTypeName} - schema={schema} path={parent.Path} - title="" onSubmit={async (parentPath, content) => { try { const created = await repo.post({ diff --git a/apps/sensenet/src/components/dialogs/content-info.tsx b/apps/sensenet/src/components/dialogs/content-info.tsx index 8bc79c771..4ee8a79b3 100644 --- a/apps/sensenet/src/components/dialogs/content-info.tsx +++ b/apps/sensenet/src/components/dialogs/content-info.tsx @@ -2,52 +2,26 @@ import Dialog, { DialogProps } from '@material-ui/core/Dialog' import DialogContent from '@material-ui/core/DialogContent' import DialogTitle from '@material-ui/core/DialogTitle' import Drawer from '@material-ui/core/Drawer' -import Typography from '@material-ui/core/Typography' import { GenericContent } from '@sensenet/default-content-types' import React, { useContext } from 'react' +import { BrowseView } from '@sensenet/controls-react' import { ResponsiveContext } from '../../context' -import { useLocalization } from '../../hooks' +import { useLocalization, useRepository } from '../../hooks' export const ContentInfoDialog: React.FunctionComponent<{ dialogProps: DialogProps content: GenericContent }> = props => { const device = useContext(ResponsiveContext) - const itemStyle: React.CSSProperties = { padding: '0.3em' } - const localization = useLocalization().contentInfoDialog - - const dialogContent = ( - <> -
- {localization.type} - {localization.owner} - {localization.path} - {localization.created} -
-
- {props.content.Type} - - {(props.content.Owner && - ((typeof props.content.Owner === 'object' && (props.content.Owner as GenericContent).DisplayName) || - (props.content.Owner as GenericContent).Name)) || - localization.unknownOwner} - - {props.content.Path} - {props.content.CreationDate} -
- - ) + const repo = useRepository() if (device === 'mobile') { return ( -
{dialogContent}
+
+ +
) } @@ -57,7 +31,9 @@ export const ContentInfoDialog: React.FunctionComponent<{ {localization.dialogTitle.replace('{0}', props.content.DisplayName || props.content.Name)} - {dialogContent} + + +
) } diff --git a/apps/sensenet/src/components/dialogs/copy-move.tsx b/apps/sensenet/src/components/dialogs/copy-move.tsx index 8d6d4167a..12a4ddc83 100644 --- a/apps/sensenet/src/components/dialogs/copy-move.tsx +++ b/apps/sensenet/src/components/dialogs/copy-move.tsx @@ -42,7 +42,9 @@ export const CopyMoveDialog: React.FunctionComponent = prop useEffect(() => { props.dialogProps.open === true && list.navigateTo(props.currentParent) - }, [list, props.currentParent, props.dialogProps.open]) + list.setSelectedItem(props.currentParent) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.currentParent.Path, props.dialogProps.open]) const logger = useLogger('CopyDialog') @@ -53,6 +55,7 @@ export const CopyMoveDialog: React.FunctionComponent = prop return ( ev.stopPropagation()} onDoubleClick={ev => ev.stopPropagation()}> diff --git a/apps/sensenet/src/components/dialogs/delete.tsx b/apps/sensenet/src/components/dialogs/delete.tsx index 0b030d169..8c9a10969 100644 --- a/apps/sensenet/src/components/dialogs/delete.tsx +++ b/apps/sensenet/src/components/dialogs/delete.tsx @@ -72,23 +72,50 @@ export const DeleteContentDialog: React.FunctionComponent<{ onClick={async ev => { try { setIsDeleteInProgress(true) - await repo.delete({ + const result = await repo.delete({ idOrPath: props.content.map(c => c.Path), permanent, }) - logger.information({ - message: - props.content.length > 1 - ? localization.deleteMultipleSuccessNotification.replace('{0}', props.content.length.toString()) - : localization.deleteSuccessNotification.replace( - '{0}', - props.content[0].DisplayName || props.content[0].Name, - ), - data: { - relatedContent: props.content.length > 1 ? undefined : props.content[0], - relatedRepository: repo.configuration.repositoryUrl, - }, - }) + if (result.d.results.length) { + logger.information({ + message: + result.d.results.length > 1 + ? localization.deleteMultipleSuccessNotification.replace( + '{0}', + result.d.results.length.toString(), + ) + : localization.deleteSuccessNotification.replace('{0}', result.d.results[0].Name), + data: { + relatedContent: props.content.length > 1 ? undefined : props.content[0], + relatedRepository: repo.configuration.repositoryUrl, + }, + }) + } + if (result.d.errors.length) { + logger.warning({ + message: + result.d.errors.length > 1 + ? localization.deleteMultipleContentFailedNotification.replace( + '{0}', + result.d.errors.length.toString(), + ) + : localization.deleteSingleContentFailedNotification + .replace( + '{0}', + (props.content.find(c => c.Id == result.d.errors[0].content.Id) as GenericContent) + .DisplayName || + (props.content.find(c => c.Id == result.d.errors[0].content.Id) as GenericContent) + .Name || + result.d.errors[0].content.Name, + ) + .replace('{1}', result.d.errors[0].error.message.value), + data: { + relatedContent: props.content.length > 1 ? undefined : props.content[0], + details: result.d.errors, + relatedRepository: repo.configuration.repositoryUrl, + }, + }) + } } catch (error) { logger.error({ message: localization.deleteFailedNotification, diff --git a/apps/sensenet/src/components/dialogs/edit-properties-dialog-body.tsx b/apps/sensenet/src/components/dialogs/edit-properties-dialog-body.tsx new file mode 100644 index 000000000..930ed1a4a --- /dev/null +++ b/apps/sensenet/src/components/dialogs/edit-properties-dialog-body.tsx @@ -0,0 +1,82 @@ +import React from 'react' +import { DialogContent, DialogTitle } from '@material-ui/core' +import { EditView } from '@sensenet/controls-react' +import { isExtendedError } from '@sensenet/client-core/dist/Repository/Repository' +import { DialogProps } from '@material-ui/core/Dialog' +import { GenericContent } from '@sensenet/default-content-types' +import { ConstantContent } from '@sensenet/client-core' +import { useLocalization, useLogger, useRepository, useSelectionService } from '../../hooks' +import { CurrentContentContext, CurrentContentProvider } from '../../context' + +const EditPropertiesDialogBody: React.FunctionComponent<{ + contentId: number + dialogProps: DialogProps +}> = props => { + const selectionService = useSelectionService() + const repo = useRepository() + const localization = useLocalization().editPropertiesDialog + const logger = useLogger('EditPropertiesDialog') + + const onSubmit = async (id: number, content: GenericContent) => { + try { + await repo.patch({ + idOrPath: id, + content, + }) + props.dialogProps.onClose && props.dialogProps.onClose(null as any, 'backdropClick') + logger.information({ + message: localization.saveSuccessNotification.replace( + '{0}', + content.DisplayName || content.Name || content.DisplayName || content.Name, + ), + data: { + relatedContent: content, + content, + relatedRepository: repo.configuration.repositoryUrl, + }, + }) + } catch (error) { + logger.error({ + message: localization.saveFailedNotification.replace( + '{0}', + content.DisplayName || content.Name || content.DisplayName || content.Name, + ), + data: { + relatedContent: content, + content, + relatedRepository: repo.configuration.repositoryUrl, + error: isExtendedError(error) ? repo.getErrorFromResponse(error.response) : error, + }, + }) + } + } + + return ( + selectionService.activeContent.setValue(c)} + oDataOptions={{ select: 'all' }}> + + {content => + content.Id !== ConstantContent.PORTAL_ROOT.Id && ( + <> + {localization.dialogTitle.replace('{0}', content.DisplayName || content.Name)} + + + props.dialogProps.onClose && props.dialogProps.onClose(null as any, 'backdropClick') + } + onSubmit={onSubmit} + /> + + + ) + } + + + ) +} + +export default EditPropertiesDialogBody diff --git a/apps/sensenet/src/components/dialogs/edit-properties.tsx b/apps/sensenet/src/components/dialogs/edit-properties.tsx index 6b8e599eb..e1a43557e 100644 --- a/apps/sensenet/src/components/dialogs/edit-properties.tsx +++ b/apps/sensenet/src/components/dialogs/edit-properties.tsx @@ -1,67 +1,15 @@ import Dialog, { DialogProps } from '@material-ui/core/Dialog' -import DialogContent from '@material-ui/core/DialogContent' -import DialogTitle from '@material-ui/core/DialogTitle' -import { isExtendedError } from '@sensenet/client-core/dist/Repository/Repository' -import { EditView } from '@sensenet/controls-react' import { GenericContent } from '@sensenet/default-content-types' import React from 'react' -import { useLocalization, useLogger, useRepository } from '../../hooks' +import EditPropertiesDialogBody from './edit-properties-dialog-body' export const EditPropertiesDialog: React.FunctionComponent<{ dialogProps: DialogProps content: GenericContent }> = props => { - const repo = useRepository() - const localization = useLocalization().editPropertiesDialog - const logger = useLogger('EditPropertiesDialog') - return ( - - - {localization.dialogTitle.replace('{0}', props.content.DisplayName || props.content.Name)}{' '} - - - props.dialogProps.onClose && props.dialogProps.onClose(null as any, 'backdropClick')} - onSubmit={async (id, content) => { - try { - await repo.patch({ - idOrPath: id, - content, - }) - props.dialogProps.onClose && props.dialogProps.onClose(null as any, 'backdropClick') - logger.information({ - message: localization.saveSuccessNotification.replace( - '{0}', - content.DisplayName || content.Name || props.content.DisplayName || props.content.Name, - ), - data: { - relatedContent: props.content, - content, - relatedRepository: repo.configuration.repositoryUrl, - }, - }) - } catch (error) { - logger.error({ - message: localization.saveFailedNotification.replace( - '{0}', - content.DisplayName || content.Name || props.content.DisplayName || props.content.Name, - ), - data: { - relatedContent: props.content, - content, - relatedRepository: repo.configuration.repositoryUrl, - error: isExtendedError(error) ? repo.getErrorFromResponse(error.response) : error, - }, - }) - } - }} - /> - + + ) } diff --git a/apps/sensenet/src/components/drawer/Items.tsx b/apps/sensenet/src/components/drawer/Items.tsx deleted file mode 100644 index 078ed95fb..000000000 --- a/apps/sensenet/src/components/drawer/Items.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import BuildTwoTone from '@material-ui/icons/BuildTwoTone' -import InfoTwoTone from '@material-ui/icons/InfoTwoTone' -import LanguageTwoTone from '@material-ui/icons/LanguageTwoTone' -import PeopleTwoTone from '@material-ui/icons/PeopleTwoTone' -import PublicTwoTone from '@material-ui/icons/PublicTwoTone' -import SearchTwoTone from '@material-ui/icons/SearchTwoTone' -import WidgetsTwoTone from '@material-ui/icons/WidgetsTwoTone' - -import { Group } from '@sensenet/default-content-types' -import React from 'react' -import DefaultLocalization from '../../localization/default' - -export interface DrawerItem { - name: string - primaryText: keyof (typeof DefaultLocalization.drawer) - secondaryText: keyof (typeof DefaultLocalization.drawer) - url: string - icon: JSX.Element - requiredGroupPath: string -} - -export const defaultDrawerItems: DrawerItem[] = [ - { - name: 'Search', - primaryText: 'searchTitle', - secondaryText: 'searchSecondaryText', - url: '/saved-queries', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/Operators', - }, - { - name: 'Content', - primaryText: 'contentTitle', - secondaryText: 'contentSecondaryText', - url: '/browse', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/ContentExplorers', - }, - { - name: 'Users and Groups', - primaryText: 'usersAndGroupsTitle', - secondaryText: 'usersAndGroupsSecondaryText', - url: '/iam', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/Operators', - }, - { - name: 'Content Types', - primaryText: 'contentTypesTitle', - secondaryText: 'contentTypesSecondaryText', - url: '/content-types', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/Administrators', - }, - { - name: 'Localization', - primaryText: 'localizationTitle', - secondaryText: 'localizationSecondaryText', - url: '/localization', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/Administrators', - }, - { - name: 'Trash', - primaryText: 'trashTitle', - secondaryText: 'trashSecondaryText', - url: '/trash', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/Administrators', - }, - { - name: 'Setup', - primaryText: 'setupTitle', - secondaryText: 'setupSecondaryText', - url: '/setup', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/Administrators', - }, - { - name: 'Version info', - primaryText: 'versionInfoTitle', - secondaryText: 'versionInfoSecondaryText', - url: '/info', - icon: , - requiredGroupPath: '/Root/IMS/BuiltIn/Portal/Administrators', - }, -] - -export const getAllowedDrawerItems = (groups: Group[]) => { - return defaultDrawerItems.filter( - i => i.requiredGroupPath && (groups.find(g => g.Path === i.requiredGroupPath) ? true : false), - ) -} diff --git a/apps/sensenet/src/components/drawer/PermanentDrawer.tsx b/apps/sensenet/src/components/drawer/PermanentDrawer.tsx index 56c3f69e3..91c25c244 100644 --- a/apps/sensenet/src/components/drawer/PermanentDrawer.tsx +++ b/apps/sensenet/src/components/drawer/PermanentDrawer.tsx @@ -13,11 +13,10 @@ import { PathHelper } from '@sensenet/client-utils' import React, { useContext, useEffect, useState } from 'react' import { withRouter } from 'react-router' import { Link, matchPath, NavLink, RouteComponentProps } from 'react-router-dom' -import { ResponsivePersonalSetttings } from '../../context' -import { useLocalization, usePersonalSettings, useRepository, useSession, useTheme } from '../../hooks' +import { ResponsiveContext, ResponsivePersonalSetttings } from '../../context' +import { useDrawerItems, useLocalization, usePersonalSettings, useRepository, useSession, useTheme } from '../../hooks' import { LogoutButton } from '../LogoutButton' import { UserAvatar } from '../UserAvatar' -import { getAllowedDrawerItems } from './Items' const PermanentDrawer: React.FunctionComponent = props => { const settings = useContext(ResponsivePersonalSetttings) @@ -25,9 +24,10 @@ const PermanentDrawer: React.FunctionComponent = props => { const theme = useTheme() const session = useSession() const repo = useRepository() + const device = useContext(ResponsiveContext) const [opened, setOpened] = useState(settings.drawer.type === 'permanent') - const [items, setItems] = useState(getAllowedDrawerItems(session.groups)) + const items = useDrawerItems() const localization = useLocalization().drawer const [currentRepoEntry, setCurrentRepoEntry] = useState( @@ -42,8 +42,6 @@ const PermanentDrawer: React.FunctionComponent = props => { [personalSettings, repo], ) - useEffect(() => setItems(getAllowedDrawerItems(session.groups)), [session.groups]) - if (!settings.drawer.enabled) { return null } @@ -51,7 +49,6 @@ const PermanentDrawer: React.FunctionComponent = props => { return ( = props => { justifyContent: 'space-between', flexDirection: 'column', backgroundColor: theme.palette.background.default, // '#222', - paddingTop: '1em', transition: 'width 100ms ease-in-out', }}> -
- {items - .filter(i => settings.drawer.items && settings.drawer.items.indexOf(i.name) !== -1) - .map(item => { - const isActive = matchPath(props.location.pathname, item.url) - return isActive ? ( - +
+ {items.map((item, index) => { + const isActive = matchPath(props.location.pathname, `/:repositoryId${item.url}`) + return isActive ? ( + + + {item.primaryText}
{item.secondaryText} + + } + placement="right"> + {item.icon} +
+ {opened ? : null} +
+ ) : ( + + - {localization[item.primaryText]}
{localization[item.secondaryText]} + {item.primaryText}
{item.secondaryText} } placement="right"> {item.icon}
- {opened ? ( - - ) : null} + {opened ? : null}
- ) : ( - - - - {localization[item.primaryText]}
{localization[item.secondaryText]} - - } - placement="right"> - {item.icon} -
- {opened ? ( - - ) : null} -
-
- ) - })} +
+ ) + })}
{opened ? ( @@ -135,11 +119,13 @@ const PermanentDrawer: React.FunctionComponent = props => { secondary={(currentRepoEntry && currentRepoEntry.displayName) || repo.configuration.repositoryUrl} /> - - - - - + {device === 'mobile' ? null : ( + + + + + + )} diff --git a/apps/sensenet/src/components/drawer/TemporaryDrawer.tsx b/apps/sensenet/src/components/drawer/TemporaryDrawer.tsx index 809e0344d..e1aef7fd2 100644 --- a/apps/sensenet/src/components/drawer/TemporaryDrawer.tsx +++ b/apps/sensenet/src/components/drawer/TemporaryDrawer.tsx @@ -11,23 +11,24 @@ import Settings from '@material-ui/icons/Settings' import { PathHelper } from '@sensenet/client-utils' import React, { useContext, useEffect, useState } from 'react' import { withRouter } from 'react-router' -import { matchPath, NavLink, RouteComponentProps, Link } from 'react-router-dom' +import { Link, matchPath, NavLink, RouteComponentProps } from 'react-router-dom' -import { ResponsivePersonalSetttings } from '../../context' -import { useLocalization, usePersonalSettings, useRepository, useSession, useTheme } from '../../hooks' +import { ResponsiveContext, ResponsivePersonalSetttings } from '../../context' +import { useDrawerItems, useLocalization, usePersonalSettings, useRepository, useSession, useTheme } from '../../hooks' import { LogoutButton } from '../LogoutButton' import { UserAvatar } from '../UserAvatar' -import { getAllowedDrawerItems } from './Items' const TemporaryDrawer: React.FunctionComponent< RouteComponentProps & { isOpened: boolean; onClose: () => void; onOpen: () => void } > = props => { const settings = useContext(ResponsivePersonalSetttings) + const device = useContext(ResponsiveContext) const personalSettings = usePersonalSettings() const repo = useRepository() const theme = useTheme() const session = useSession() - const [items, setItems] = useState(getAllowedDrawerItems(session.groups)) + const items = useDrawerItems() + const [currentRepoEntry, setCurrentRepoEntry] = useState( personalSettings.repositories.find(r => r.url === PathHelper.trimSlashes(repo.configuration.repositoryUrl)), ) @@ -42,8 +43,6 @@ const TemporaryDrawer: React.FunctionComponent< [personalSettings, repo], ) - useEffect(() => setItems(getAllowedDrawerItems(session.groups)), [session.groups]) - if (!settings.drawer.enabled) { return null } @@ -70,48 +69,43 @@ const TemporaryDrawer: React.FunctionComponent< transition: 'width 100ms ease-in-out', }}>
- {items - .filter(i => settings.drawer.items && settings.drawer.items.indexOf(i.name) !== -1) - .map(item => { - const isActive = matchPath(props.location.pathname, item.url) - return isActive ? ( - + {items.map((item, index) => { + const isActive = matchPath(props.location.pathname, { path: `/:repositoryId${item.url}`, exact: true }) + return isActive ? ( + + + {item.primaryText}
{item.secondaryText} + + } + placement="right"> + {item.icon} +
+ +
+ ) : ( + props.onClose()} + to={`/${btoa(repo.configuration.repositoryUrl)}${item.url}`} + activeStyle={{ opacity: 1 }} + style={{ textDecoration: 'none', opacity: 0.54 }} + key={index}> + - {localization[item.primaryText]}
{localization[item.secondaryText]} + {item.primaryText}
{item.secondaryText} } placement="right"> {item.icon}
- +
- ) : ( - props.onClose()} - to={`/${btoa(repo.configuration.repositoryUrl)}${item.url}`} - activeStyle={{ opacity: 1 }} - style={{ textDecoration: 'none', opacity: 0.54 }} - key={item.name}> - - - {localization[item.primaryText]}
{localization[item.secondaryText]} - - } - placement="right"> - {item.icon} -
- -
-
- ) - })} +
+ ) + })}
@@ -124,11 +118,13 @@ const TemporaryDrawer: React.FunctionComponent< secondaryTypographyProps={{ style: { overflow: 'hidden', textOverflow: 'ellipsis' } }} /> - props.onClose()}> - - - - + {device === 'mobile' ? null : ( + props.onClose()}> + + + + + )} props.onClose()} /> diff --git a/apps/sensenet/src/components/edit/EditProperties.tsx b/apps/sensenet/src/components/edit/EditProperties.tsx index 283e497ed..bc5881f8c 100644 --- a/apps/sensenet/src/components/edit/EditProperties.tsx +++ b/apps/sensenet/src/components/edit/EditProperties.tsx @@ -16,7 +16,10 @@ const GenericContentEditor: React.FunctionComponent - selectionService.activeContent.setValue(c)}> + selectionService.activeContent.setValue(c)} + oDataOptions={{ select: 'all' }}> @@ -24,10 +27,8 @@ const GenericContentEditor: React.FunctionComponent {content && content.Id === contentId ? ( { repo .patch({ diff --git a/apps/sensenet/src/components/edit/PersonalSettingsEditor.tsx b/apps/sensenet/src/components/edit/PersonalSettingsEditor.tsx index 2989be2db..68f0d0006 100644 --- a/apps/sensenet/src/components/edit/PersonalSettingsEditor.tsx +++ b/apps/sensenet/src/components/edit/PersonalSettingsEditor.tsx @@ -1,17 +1,34 @@ -import { deepMerge } from '@sensenet/client-utils' +import { deepMerge, sleepAsync } from '@sensenet/client-utils' import React, { useContext, useEffect, useState } from 'react' -import { CurrentContentContext, LocalizationContext } from '../../context' -import { useInjector, usePersonalSettings, useRepository } from '../../hooks' +import { + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControlLabel, + Switch, + Typography, +} from '@material-ui/core' +import MonacoEditor from 'react-monaco-editor' +import { CurrentContentContext, LocalizationContext, ResponsiveContext } from '../../context' +import { useEventService, useInjector, useLogger, useRepository, useTheme } from '../../hooks' import { setupModel } from '../../services/MonacoModels/PersonalSettingsModel' import { defaultSettings, PersonalSettings } from '../../services/PersonalSettings' +import { RepositoryManager } from '../../services/RepositoryManager' import { TextEditor } from './TextEditor' const SettingsEditor: React.FunctionComponent = () => { const injector = useInjector() const service = injector.getInstance(PersonalSettings) - const settings = usePersonalSettings() + const settings = service.userValue.getValue() const localization = useContext(LocalizationContext) const repo = useRepository() + const theme = useTheme() + const platform = useContext(ResponsiveContext) + const eventService = useEventService() const [editorContent] = useState({ Type: 'PersonalSettings', Name: `PersonalSettings`, @@ -21,16 +38,106 @@ const SettingsEditor: React.FunctionComponent = () => { setupModel(localization.values, repo) }, [localization.values, repo]) + const [showDefaults, setShowDefaults] = useState(false) + const [showRestoreDialog, setShowRestoreDialog] = useState(false) + const [isResetting, setIsResetting] = useState(false) + + const logger = useLogger('PersonalSettingsEditor') + return ( - JSON.stringify(settings, undefined, 3)} - saveContent={async (_c, v) => { - await service.setValue(deepMerge({ ...defaultSettings }, JSON.parse(v))) - }} - /> + !isResetting && setShowRestoreDialog(false)}> + {localization.values.personalSettings.restoreDialogTitle} + <> + + {!isResetting ? ( + {localization.values.personalSettings.restoreDialogTText} + ) : ( + + )} + + + + + + + +
+
+ + {localization.values.personalSettings.defaults} + + +
+
+ JSON.stringify(settings, undefined, 3)} + additionalButtons={ + <> + setShowDefaults(!showDefaults)} />} + label={localization.values.personalSettings.showDefaults} + /> + + + } + saveContent={async (_c, v) => { + await service.setPersonalSettingsValue(deepMerge(JSON.parse(v))) + }} + /> +
+
) } diff --git a/apps/sensenet/src/components/edit/TextEditor.tsx b/apps/sensenet/src/components/edit/TextEditor.tsx index 1ac1a6c21..83203f154 100644 --- a/apps/sensenet/src/components/edit/TextEditor.tsx +++ b/apps/sensenet/src/components/edit/TextEditor.tsx @@ -1,6 +1,6 @@ import Button from '@material-ui/core/Button' import { PathHelper } from '@sensenet/client-utils' -import { ActionModel, File as SnFile, GenericContent, Settings } from '@sensenet/default-content-types' +import { ActionModel, GenericContent, Settings, File as SnFile } from '@sensenet/default-content-types' import { Uri } from 'monaco-editor' import React, { useContext, useEffect, useState } from 'react' import MonacoEditor from 'react-monaco-editor' @@ -31,6 +31,7 @@ export interface TextEditorProps { content: SnFile loadContent?: (content: SnFile) => Promise saveContent?: (content: SnFile, value: string) => Promise + additionalButtons?: JSX.Element } export const TextEditor: React.FunctionComponent = props => { @@ -141,7 +142,12 @@ export const TextEditor: React.FunctionComponent = props => { }}>
-
+
+ {props.additionalButtons ? props.additionalButtons : null} @@ -162,6 +168,7 @@ export const TextEditor: React.FunctionComponent = props => { value={textValue} onChange={v => setTextValue(v)} options={{ + readOnly: platform === 'mobile', automaticLayout: true, minimap: { enabled: platform === 'desktop' ? true : false, diff --git a/apps/sensenet/src/components/event-list/details.tsx b/apps/sensenet/src/components/event-list/details.tsx index 201833f94..95b70ddfd 100644 --- a/apps/sensenet/src/components/event-list/details.tsx +++ b/apps/sensenet/src/components/event-list/details.tsx @@ -1,10 +1,10 @@ -import { ILeveledLogEntry } from '@furystack/logging' +import { LeveledLogEntry } from '@furystack/logging' import React, { useContext } from 'react' import MonacoEditor, { MonacoDiffEditor } from 'react-monaco-editor' import { ResponsiveContext } from '../../context' import { useTheme } from '../../hooks' -export const EventDetails: React.FunctionComponent<{ event: ILeveledLogEntry }> = ({ event }) => { +export const EventDetails: React.FunctionComponent<{ event: LeveledLogEntry }> = ({ event }) => { const theme = useTheme() const platform = useContext(ResponsiveContext) if (event.data.compare) { diff --git a/apps/sensenet/src/components/event-list/index.tsx b/apps/sensenet/src/components/event-list/index.tsx index e91574bc5..287887d5d 100644 --- a/apps/sensenet/src/components/event-list/index.tsx +++ b/apps/sensenet/src/components/event-list/index.tsx @@ -1,4 +1,4 @@ -import { ILeveledLogEntry } from '@furystack/logging' +import { LeveledLogEntry } from '@furystack/logging' import Button from '@material-ui/core/Button' import Typography from '@material-ui/core/Typography' import KeyboardBackspace from '@material-ui/icons/KeyboardBackspace' @@ -16,13 +16,13 @@ const EventList: React.FunctionComponent | undefined + let currentEvent: LeveledLogEntry | undefined if (props.match.params.eventGuid) { currentEvent = eventService.values.getValue().find(ev => ev.data.guid === props.match.params.eventGuid) } - const [events, setEvents] = useState>>(eventService.values.getValue()) + const [events, setEvents] = useState>>(eventService.values.getValue()) useEffect(() => { const observable = eventService.values.subscribe(values => setEvents(values)) diff --git a/apps/sensenet/src/components/event-list/list.tsx b/apps/sensenet/src/components/event-list/list.tsx index 58bbc29da..c74b53512 100644 --- a/apps/sensenet/src/components/event-list/list.tsx +++ b/apps/sensenet/src/components/event-list/list.tsx @@ -1,4 +1,4 @@ -import { ILeveledLogEntry, LogLevel } from '@furystack/logging' +import { LeveledLogEntry, LogLevel } from '@furystack/logging' import IconButton from '@material-ui/core/IconButton' import Table from '@material-ui/core/Table' import TableBody from '@material-ui/core/TableBody' @@ -21,11 +21,11 @@ import { Icon } from '../Icon' import { EventListFilterContext } from './filter-context' export const List: React.FunctionComponent<{ - values: Array> + values: Array> style?: React.CSSProperties }> = props => { const { filter } = useContext(EventListFilterContext) - const [effectiveValues, setEffectiveValues] = useState>>([]) + const [effectiveValues, setEffectiveValues] = useState>>([]) const localization = useLocalization().eventList.list diff --git a/apps/sensenet/src/components/layout/DesktopLayout.tsx b/apps/sensenet/src/components/layout/DesktopLayout.tsx index 6ad0f0744..78471899a 100644 --- a/apps/sensenet/src/components/layout/DesktopLayout.tsx +++ b/apps/sensenet/src/components/layout/DesktopLayout.tsx @@ -1,5 +1,5 @@ import CssBaseline from '@material-ui/core/CssBaseline' -import React, { useState, useContext } from 'react' +import React, { useContext, useState } from 'react' import snLogo from '../../assets/sensenet_logo_transparent.png' import { ResponsivePersonalSetttings } from '../../context' diff --git a/apps/sensenet/src/components/login/GoogleAuthButton.tsx b/apps/sensenet/src/components/login/GoogleAuthButton.tsx new file mode 100644 index 000000000..b3afffa9a --- /dev/null +++ b/apps/sensenet/src/components/login/GoogleAuthButton.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { OAuthButton } from './OAuthButton' + +export const GoogleAuthButton = () => { + // Mixing JWT with Forms is not safe at the moment. + + // const repository = useRepository() + // const googleOauthProvider = useRef() + // useEffect(() => { + // const jwt = new JwtService(repository) + // googleOauthProvider.current = addGoogleAuth(jwt, { + // clientId: '', // We are going to add this later + // }) + // return () => { + // jwt.dispose() + // googleOauthProvider.current && googleOauthProvider.current.dispose() + // } + // }, [repository]) + // const onClickHandler = () => { + // googleOauthProvider.current && googleOauthProvider.current.login() + // } + // return + return +} diff --git a/apps/sensenet/src/components/login/Login.tsx b/apps/sensenet/src/components/login/Login.tsx new file mode 100644 index 000000000..1115f58d6 --- /dev/null +++ b/apps/sensenet/src/components/login/Login.tsx @@ -0,0 +1,310 @@ +import Button from '@material-ui/core/Button' +import CircularProgress from '@material-ui/core/CircularProgress' +import TextField from '@material-ui/core/TextField' +import Typography from '@material-ui/core/Typography' +import { ConstantContent, FormsAuthenticationService } from '@sensenet/client-core' +import { Retrier, sleepAsync } from '@sensenet/client-utils' +import React, { useEffect, useState } from 'react' +import { RouteComponentProps, withRouter } from 'react-router' +import { Container, createStyles, Grid, Link, makeStyles, Theme } from '@material-ui/core' +import { useInjector, useLocalization, useRepository, useSession, useTheme } from '../../hooks' +import { PersonalSettings, PersonalSettingsType } from '../../services/PersonalSettings' +import { UserAvatar } from '../UserAvatar' +import { GoogleAuthButton } from './GoogleAuthButton' +import { OAuthButton } from './OAuthButton' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + paper: { + marginTop: theme.spacing(10), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + marginTopBottom: { + margin: theme.spacing(2, 0, 2), + }, + signUp: { + marginTop: theme.spacing(3), + paddingRight: theme.spacing(3), + }, + title: { + margin: theme.spacing(2, 0, 3), + }, + middleText: { + margin: theme.spacing(2, 0, 3), + }, + link: { + margin: theme.spacing(1), + color: '#26a69a !important', + }, + }), +) + +export const Login: React.FunctionComponent = props => { + const injector = useInjector() + const repo = useRepository() + const theme = useTheme() + const classes = useStyles() + const personalSettings = injector.getInstance(PersonalSettings).userValue.getValue() + const session = useSession() + const settingsManager = injector.getInstance(PersonalSettings) + + const logger = injector.logger.withScope('LoginComponent') + + const repositories: PersonalSettingsType['repositories'] = personalSettings.repositories || [] + + const existingRepo = repositories.find(r => r.url === repo.configuration.repositoryUrl) + + const [userName, setUserName] = useState((existingRepo && existingRepo.loginName) || '') + const [password, setPassword] = useState('') + const [url, setUrl] = useState(repo.configuration.repositoryUrl) + const [isInProgress, setIsInProgress] = useState(false) + + const [success, setSuccess] = useState(false) + const [progressValue, setProgressValue] = useState(0) + const [inputState, setInputState] = useState({ + userName: { isValid: true, errorMessage: '' }, + password: { isValid: true, errorMessage: '' }, + repository: { isValid: true, errorMessage: '' }, + }) + + const [error, setError] = useState() + + const localization = useLocalization().login + + useEffect(() => { + setUrl(repo.configuration.repositoryUrl) + existingRepo && existingRepo.loginName && setUserName(existingRepo.loginName) + }, [existingRepo, repo.configuration.repositoryUrl]) + + const handleSubmit = async (ev: React.FormEvent) => { + ev.preventDefault() + const repoToLogin = injector.getRepository(url) + personalSettings.lastRepository = url + try { + setIsInProgress(true) + const result = await repoToLogin.authentication.login(userName, password) + setSuccess(result) + if (result) { + setError(undefined) + const existing = repositories.find(i => i.url === url) + if (!existing) { + repositories.push({ url, loginName: userName }) + } else { + personalSettings.repositories = repositories.map(r => { + if (r.url === url) { + r.loginName = userName + } + return r + }) + } + ;(repoToLogin.authentication as FormsAuthenticationService).getCurrentUser() + settingsManager.setPersonalSettingsValue({ ...personalSettings, repositories }) + await Retrier.create( + async () => repoToLogin.authentication.currentUser.getValue().Id !== ConstantContent.VISITOR_USER.Id, + ) + .setup({ + timeoutMs: 60 * 1000, + RetryIntervalMs: 100, + }) + .run() + for (let index = 0; index < 100; index++) { + await sleepAsync(index / 50) + setProgressValue(index) + } + logger.information({ + message: localization.loginSuccessNotification.replace('{0}', userName).replace('{1}', url), + data: { + relatedContent: repoToLogin.authentication.currentUser.getValue(), + relatedRepository: repoToLogin.configuration.repositoryUrl, + }, + }) + if (props.match.path === '/login') { + await sleepAsync(1800) + props.history.push(`/${btoa(repoToLogin.configuration.repositoryUrl)}`) + } + } else { + setIsInProgress(false) + setError(localization.loginFailed) + logger.warning({ + message: localization.loginFailedNotification.replace('{0}', userName).replace('{1}', url), + }) + } + } catch (err) { + logger.error({ + message: localization.loginErrorNotification.replace('{0}', userName).replace('{1}', url), + data: { + details: { error: err }, + }, + }) + } + } + + const handleInvalid = (ev: React.ChangeEvent) => { + ev.preventDefault() + setInputState({ + ...inputState, + [ev.target.name]: { + isValid: ev.target.validity.valid, + errorMessage: ev.target.validationMessage, + }, + }) + } + + const clearInputError = (ev: React.ChangeEvent) => { + setInputState({ ...inputState, [ev.target.name]: { isValid: true, errorMessage: '' } }) + } + + return ( + <> + + + {localization.newToSensenet}{' '} + + {localization.signUp} + + + + +
+ + {localization.loginTitle} + + {isInProgress ? ( +
+
+ + {success ? ( + + ) : null} + + {success ? ( + <> + {localization.greetings.replace( + '{0}', + session.currentUser.DisplayName || session.currentUser.LoginName || session.currentUser.Name, + )} + + ) : ( + <>{localization.loggingInTo.replace('{0}', url)} + )} + +
+
+ ) : ( +
+ { + clearInputError(ev) + setUserName(ev.target.value) + }} + /> + { + clearInputError(ev) + setPassword(ev.target.value) + }} + /> + { + clearInputError(ev) + setUrl(ev.target.value) + }} + /> + {error ? {error} : null} + + + )} + + {localization.youCanLogInWith} + + + + + + + + + + + + + + + {localization.logInWithSso} + + + {localization.resetPassword} + + + {localization.help} + + + {localization.contactUs} + + +
+
+ + ) +} + +export default withRouter(Login) diff --git a/apps/sensenet/src/components/login/OAuthButton.tsx b/apps/sensenet/src/components/login/OAuthButton.tsx new file mode 100644 index 000000000..6d6fff76e --- /dev/null +++ b/apps/sensenet/src/components/login/OAuthButton.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { makeStyles } from '@material-ui/styles' +import { createStyles, Theme } from '@material-ui/core' +import { Icon, iconType } from '@sensenet/icons-react' +import Button, { ButtonProps } from '@material-ui/core/Button' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + oAuthButton: { + borderRadius: 0, + }, + leftIcon: { + marginRight: theme.spacing(1), + }, + }), +) + +interface Props { + iconName: string + buttonText?: string + buttonProps?: ButtonProps +} + +export const OAuthButton = (props: Props) => { + const classes = useStyles() + return ( + + ) +} diff --git a/apps/sensenet/src/components/search/index.tsx b/apps/sensenet/src/components/search/index.tsx index 24a23d479..f6c1734fc 100644 --- a/apps/sensenet/src/components/search/index.tsx +++ b/apps/sensenet/src/components/search/index.tsx @@ -11,7 +11,7 @@ import Save from '@material-ui/icons/Save' import { ConstantContent, ODataResponse } from '@sensenet/client-core' import { debounce } from '@sensenet/client-utils' import { GenericContent } from '@sensenet/default-content-types' -import React, { useContext, useEffect, useState } from 'react' +import React, { useCallback, useContext, useEffect, useState } from 'react' import { generatePath, RouteComponentProps, withRouter } from 'react-router' import Semaphore from 'semaphore-async-await' import { @@ -22,24 +22,48 @@ import { ResponsivePersonalSetttings, } from '../../context' import { useContentRouting, useLocalization, useLogger, useRepository } from '../../hooks' -import { CollectionComponent } from '../ContentListPanel' +import { CollectionComponent, isReferenceField } from '../content-list' const loadCount = 20 +const searchDebounceTime = 400 +export interface QueryData { + term: string + title?: string + hideSearchBar?: boolean + fieldsToDisplay?: Array +} + +export const encodeQueryData = (data: QueryData) => encodeURIComponent(btoa(JSON.stringify(data))) +export const decodeQueryData = (encoded?: string) => + encoded ? (JSON.parse(atob(decodeURIComponent(encoded))) as QueryData) : { term: '' } -const Search: React.FunctionComponent> = props => { +const Search: React.FunctionComponent> = props => { const repo = useRepository() const contentRouter = useContentRouting() + const logger = useLogger('Search') + const [queryData, setQueryData] = useState(decodeQueryData(props.match.params.queryData)) + const localization = useLocalization().search - const [contentQuery, setContentQuery] = useState(decodeURIComponent(props.match.params.query || '')) - const [reloadToken, setReloadToken] = useState(Math.random()) const [scrollToken, setScrollToken] = useState(Math.random()) - const [scrollLock] = useState(new Semaphore(1)) + const [loadLock] = useState(new Semaphore(1)) + const requestReload = useCallback( + debounce((qd: QueryData, term: string) => { + setQueryData({ ...qd, term }) + }, searchDebounceTime), + [], + ) - const [requestReload] = useState(() => debounce(() => setReloadToken(Math.random()), 250)) - - const logger = useLogger('Search') + useEffect(() => { + try { + const data = decodeQueryData(props.match.params.queryData || '{}') + setQueryData(data) + } catch (error) { + logger.warning({ message: 'Wrong link :(' }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [logger, props.match.params.queryData]) const [requestScroll] = useState(() => debounce((div: HTMLDivElement, total: number, loaded: number, update: (token: number) => void) => { @@ -61,30 +85,50 @@ const Search: React.FunctionComponent> = const [savePublic, setSavePublic] = useState(false) useEffect(() => { - props.history.push( - generatePath(props.match.path, { ...props.match.params, query: encodeURIComponent(contentQuery) || undefined }), - ) - repo - .loadCollection({ - path: ConstantContent.PORTAL_ROOT.Path, - oDataOptions: { - ...loadSettingsContext.loadChildrenSettings, - query: personalSettings.commandPalette.wrapQuery.replace('{0}', contentQuery), - top: loadCount, - }, - }) - .then(r => { + const ac = new AbortController() + ;(async () => { + await loadLock.acquire() + try { + setResult([]) + props.history.push( + generatePath(props.match.path, { ...props.match.params, queryData: encodeQueryData(queryData) }), + ) + + const r = await repo.loadCollection({ + path: ConstantContent.PORTAL_ROOT.Path, + oDataOptions: { + ...loadSettingsContext.loadChildrenSettings, + select: ['Actions', ...(queryData.fieldsToDisplay || [])], + expand: ['Actions', ...(queryData.fieldsToDisplay || []).filter(f => isReferenceField(f, repo))], + query: personalSettings.commandPalette.wrapQuery.replace('{0}', queryData.term), + top: loadCount, + }, + requestInit: { signal: ac.signal }, + }) setError('') setResult(r.d.results) setCount(r.d.__count) - }) - .catch(e => { - setError(e.message) - logger.warning({ message: 'Error executing search', data: { details: { error: e }, isDismissed: true } }) - }) + } catch (e) { + if (!ac.signal.aborted) { + setError(e.message) + setResult([]) + logger.warning({ message: 'Error executing search', data: { details: { error: e }, isDismissed: true } }) + } + } finally { + loadLock.release() + } + })() // loadSettings should be excluded :( // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reloadToken, contentQuery, repo, personalSettings.commandPalette.wrapQuery, logger]) + }, [ + queryData.term, + repo, + personalSettings.commandPalette.wrapQuery, + logger, + loadSettingsContext.loadChildrenSettings.orderby, + loadSettingsContext.loadChildrenSettings.select, + loadSettingsContext.loadChildrenSettings.expand, + ]) useEffect(() => { ;(async () => { @@ -94,7 +138,9 @@ const Search: React.FunctionComponent> = path: ConstantContent.PORTAL_ROOT.Path, oDataOptions: { ...loadSettingsContext.loadChildrenSettings, - query: personalSettings.commandPalette.wrapQuery.replace('{0}', contentQuery), + select: ['Actions', ...(queryData.fieldsToDisplay || [])], + expand: ['Actions', ...(queryData.fieldsToDisplay || []).filter(f => isReferenceField(f, repo))], + query: personalSettings.commandPalette.wrapQuery.replace('{0}', queryData.term), top: loadCount, skip: result.length, }, @@ -105,89 +151,86 @@ const Search: React.FunctionComponent> = scrollLock.release() } })() - // 'result' should be excluded! + // Infinite loader fx, only lock-related stuff should be included as dependency! // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - contentQuery, - loadSettingsContext.loadChildrenSettings, - personalSettings.commandPalette.wrapQuery, - repo, - scrollLock, - scrollToken, - ]) + }, [scrollLock, scrollToken]) return (
- {localization.title} + {queryData.title || localization.title}
-
- { - setContentQuery(ev.target.value) - requestReload() - }} - /> - - setIsSaveOpened(false)}> - {localization.saveQuery} - - setSaveName(ev.currentTarget.value)} - /> -
- setSavePublic(ev.target.checked)} />} - /> -
- - - + setIsSaveOpened(false)}> + {localization.saveQuery} + + setSaveName(ev.currentTarget.value)} + /> +
+ setSavePublic(ev.target.checked)} />} + /> +
+ + + - -
-
+ }} + color="primary"> + {localization.save} + + +
+
+ )} {error ? ( @@ -208,7 +251,8 @@ const Search: React.FunctionComponent> = onScroll: ev => requestScroll(ev.currentTarget, count, result.length, setScrollToken), }} enableBreadcrumbs={false} - parentId={0} + fieldsToDisplay={queryData.fieldsToDisplay} + parentIdOrPath={0} onParentChange={p => { props.history.push(contentRouter.getPrimaryActionUrl(p)) }} diff --git a/apps/sensenet/src/components/search/saved-queries.tsx b/apps/sensenet/src/components/search/saved-queries.tsx index 422fea568..61cdf872e 100644 --- a/apps/sensenet/src/components/search/saved-queries.tsx +++ b/apps/sensenet/src/components/search/saved-queries.tsx @@ -17,7 +17,8 @@ import { LoadSettingsContext, } from '../../context' import { useInjector, useLocalization, useRepository } from '../../hooks' -import { CollectionComponent } from '../ContentListPanel' +import { CollectionComponent } from '../content-list' +import { encodeQueryData } from '.' const Search: React.FunctionComponent = props => { const repo = useRepository() @@ -80,7 +81,7 @@ const Search: React.FunctionComponent = props => { + to={`/${repoToken}/search/${encodeQueryData({ term: '' })}`}> @@ -98,15 +99,15 @@ const Search: React.FunctionComponent = props => { overflow: 'auto', }} enableBreadcrumbs={false} - parentId={0} + parentIdOrPath={0} onParentChange={() => { // ignore, only queries will be listed }} onActivateItem={p => { props.history.push( - `/${btoa(repo.configuration.repositoryUrl)}/search/${encodeURIComponent( - (p as Query).Query || '', - )}`, + `/${btoa(repo.configuration.repositoryUrl)}/search/${encodeQueryData({ + term: (p as Query).Query || '', + })}`, ) }} onTabRequest={() => { diff --git a/apps/sensenet/src/components/setup/index.tsx b/apps/sensenet/src/components/setup/index.tsx index 6a5a09571..c3cb8de76 100644 --- a/apps/sensenet/src/components/setup/index.tsx +++ b/apps/sensenet/src/components/setup/index.tsx @@ -15,6 +15,10 @@ import { CurrentContentContext } from '../../context' import { useContentRouting, useLocalization, useRepository } from '../../hooks' import { ContentContextMenu } from '../ContentContextMenu' +const SETUP_DOCS_URL = 'https://community.sensenet.com/docs/admin-ui/setup/' + +const createAnchorFromName = (displayName: string) => `#${displayName.replace('.', '-').toLocaleLowerCase()}` + const WellKnownContentCard: React.FunctionComponent<{ settings: Settings onContextMenu: (ev: React.MouseEvent) => void @@ -30,7 +34,7 @@ const WellKnownContentCard: React.FunctionComponent<{ }} style={{ width: 330, - height: 250, + height: 320, margin: '0.5em', display: 'flex', flexDirection: 'column', @@ -46,7 +50,11 @@ const WellKnownContentCard: React.FunctionComponent<{ - + + + ) @@ -68,6 +76,7 @@ const Setup: React.StatelessComponent = () => { const response = await repo.loadCollection({ path: ConstantContent.PORTAL_ROOT.Path, oDataOptions: { + orderby: [['Index' as any, 'asc']], query: `${new Query(q => q.typeIs(Settings)).toString()} .AUTOFILTERS:OFF`, }, }) diff --git a/apps/sensenet/src/components/tree/index.tsx b/apps/sensenet/src/components/tree/index.tsx index e408f7c9d..88b8ad4d4 100644 --- a/apps/sensenet/src/components/tree/index.tsx +++ b/apps/sensenet/src/components/tree/index.tsx @@ -4,7 +4,7 @@ import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' import { ODataParams } from '@sensenet/client-core' -import { PathHelper } from '@sensenet/client-utils' +import { PathHelper, sleepAsync } from '@sensenet/client-utils' import { GenericContent } from '@sensenet/default-content-types' import { Created } from '@sensenet/repository-events' import React, { useContext, useEffect, useState } from 'react' @@ -17,7 +17,7 @@ import { Icon } from '../Icon' export interface TreeProps { parentPath: string onItemClick?: (item: GenericContent) => void - activeItemId?: number + activeItemIdOrPath?: number | string loadOptions?: ODataParams style?: React.CSSProperties } @@ -36,6 +36,7 @@ export const Tree: React.FunctionComponent = props => { const [contextMenuAnchor, setContextMenuAnchor] = useState(null) const [isContextMenuOpened, setIsContextMenuOpened] = useState(false) const [error, setError] = useState() + const [isLoading, setIsLoading] = useState(false) const update = () => setReloadToken(Math.random()) @@ -86,6 +87,7 @@ export const Tree: React.FunctionComponent = props => { const ac = new AbortController() ;(async () => { try { + setIsLoading(true) const children = await repo.loadCollection({ path: props.parentPath, requestInit: { @@ -101,6 +103,9 @@ export const Tree: React.FunctionComponent = props => { if (!ac.signal.aborted) { setError(err) } + } finally { + await sleepAsync(300) + if (!ac.signal.aborted) setIsLoading(false) } })() return () => ac.abort() @@ -112,12 +117,12 @@ export const Tree: React.FunctionComponent = props => { return (
- + {items.map(content => { const isOpened = opened.includes(content.Id) || (ancestorPaths && ancestorPaths.includes(content.Path)) return (
- + { setContextMenuItem(content) @@ -126,8 +131,11 @@ export const Tree: React.FunctionComponent = props => { ev.preventDefault() }} button={true} - selected={props.activeItemId === content.Id} + selected={props.activeItemIdOrPath === content.Id || props.activeItemIdOrPath === content.Path} onClick={() => { + if (isLoading) { + return + } props.onItemClick && props.onItemClick(content) setOpened( isOpened ? opened.filter(o => o !== content.Id) : Array.from(new Set([...opened, content.Id])), @@ -136,12 +144,20 @@ export const Tree: React.FunctionComponent = props => { - + {isOpened ? ( - + ) : null}
diff --git a/apps/sensenet/src/components/version-info/component-info.tsx b/apps/sensenet/src/components/version-info/component-info.tsx index 125dd4cf7..1b9ad0938 100644 --- a/apps/sensenet/src/components/version-info/component-info.tsx +++ b/apps/sensenet/src/components/version-info/component-info.tsx @@ -33,7 +33,9 @@ export const ComponentInfo: React.FunctionComponent<{ component: Component; upda
{props.component.Version} - {props.update ? props.update.items[0].upper : '-'} + + {props.update && props.update.items ? props.update.items[0].upper : '-'} + {props.component.Description}
diff --git a/apps/sensenet/src/components/version-info/index.tsx b/apps/sensenet/src/components/version-info/index.tsx index 228e00e36..b2386ec15 100644 --- a/apps/sensenet/src/components/version-info/index.tsx +++ b/apps/sensenet/src/components/version-info/index.tsx @@ -10,47 +10,20 @@ import Tooltip from '@material-ui/core/Tooltip' import Typography from '@material-ui/core/Typography' import ExpandMore from '@material-ui/icons/ExpandMore' import Update from '@material-ui/icons/Update' -import { ConstantContent } from '@sensenet/client-core' -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useState } from 'react' import MonacoEditor from 'react-monaco-editor' import { ResponsiveContext } from '../../context' -import { useLocalization, useRepository, useTheme } from '../../hooks' +import { useLocalization, useTheme, useVersionInfo } from '../../hooks' import { ComponentInfo } from './component-info' -import { VersionInfo as VersionInfoModel } from './version-info-models' export const VersionInfo: React.FunctionComponent = () => { - const repo = useRepository() - const [versionInfo, setVersionInfo] = useState() const theme = useTheme() const localization = useLocalization().versionInfo const device = useContext(ResponsiveContext) const [showRaw, setShowRaw] = useState(false) - const [nugetManifests, setNugetManifests] = useState([]) - useEffect(() => { - ;(async () => { - const result = await repo.executeAction({ - idOrPath: ConstantContent.PORTAL_ROOT.Path, - body: undefined, - method: 'GET', - name: 'GetVersionInfo', - }) - setVersionInfo(result) - - const nugetPromises = result.Components.map(async component => { - const response = await fetch( - `https://api.nuget.org/v3/registration3-gz-semver2/${component.ComponentId.toLowerCase()}/index.json`, - ) - if (response.ok) { - const nugetManifest = await response.json() - return nugetManifest - } - }) - const loadedManifests = await Promise.all(nugetPromises) - setNugetManifests(loadedManifests.filter(m => m)) - })() - }, [repo]) + const { versionInfo } = useVersionInfo() return (
@@ -123,12 +96,8 @@ export const VersionInfo: React.FunctionComponent = () => { {versionInfo.Components.map((component, index) => { - const isUpdateAvailable = nugetManifests.find( - m => - m['@id'] === - `https://api.nuget.org/v3/registration3-gz-semver2/${component.ComponentId.toLocaleLowerCase()}/index.json` && - m.items[0].upper > component.Version, - ) + const isUpdateAvailable = component.IsUpdateAvailable + const nugetManifest = component.NugetManifest return ( { ? '' : localization.updateAvailable .replace('{0}', component.Version) - .replace('{1}', isUpdateAvailable.items[0].upper)} + .replace('{1}', nugetManifest.items[0].upper)} diff --git a/apps/sensenet/src/components/version-info/version-info-models.ts b/apps/sensenet/src/components/version-info/version-info-models.ts index 1b111b12c..ece6fcbf8 100644 --- a/apps/sensenet/src/components/version-info/version-info-models.ts +++ b/apps/sensenet/src/components/version-info/version-info-models.ts @@ -43,6 +43,8 @@ export interface Component { Version: string AcceptableVersion: string Description: string + IsUpdateAvailable?: boolean + NugetManifest?: any } /** diff --git a/apps/sensenet/src/components/widgets/markdown.tsx b/apps/sensenet/src/components/widgets/markdown.tsx deleted file mode 100644 index 8465e1cd6..000000000 --- a/apps/sensenet/src/components/widgets/markdown.tsx +++ /dev/null @@ -1 +0,0 @@ -export const MarkdownWidget = () => {} diff --git a/apps/sensenet/src/components/wopi-page.tsx b/apps/sensenet/src/components/wopi-page.tsx index 449b53a64..a3da3cd9c 100644 --- a/apps/sensenet/src/components/wopi-page.tsx +++ b/apps/sensenet/src/components/wopi-page.tsx @@ -1,9 +1,9 @@ -import React, { useRef, useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { RouteComponentProps, withRouter } from 'react-router' import { ODataWopiResponse } from '@sensenet/client-core' -import { Typography, Button } from '@material-ui/core' +import { Button, Typography } from '@material-ui/core' import { isExtendedError } from '@sensenet/client-core/dist/Repository/Repository' -import { useRepository, useLogger, useLocalization } from '../hooks' +import { useLocalization, useLogger, useRepository } from '../hooks' import { FullScreenLoader } from './FullScreenLoader' const WopiPage: React.FunctionComponent> = props => { diff --git a/apps/sensenet/src/context/CurrentAncestors.tsx b/apps/sensenet/src/context/CurrentAncestors.tsx index 6ebd75b67..4c8195b60 100644 --- a/apps/sensenet/src/context/CurrentAncestors.tsx +++ b/apps/sensenet/src/context/CurrentAncestors.tsx @@ -3,11 +3,11 @@ import { debounce } from '@sensenet/client-utils' import { GenericContent } from '@sensenet/default-content-types' import React, { useContext, useEffect, useState } from 'react' import Semaphore from 'semaphore-async-await' -import { useInjector, useRepository } from '../hooks' +import { useInjector, useLogger, useRepository } from '../hooks' import { CurrentContentContext } from './CurrentContent' export const CurrentAncestorsContext = React.createContext([]) -export const CurrentAncestorsProvider: React.FunctionComponent = props => { +export const CurrentAncestorsProvider: React.FunctionComponent<{ root?: number | string }> = props => { const currentContent = useContext(CurrentContentContext) const [loadLock] = useState(new Semaphore(1)) @@ -17,6 +17,8 @@ export const CurrentAncestorsProvider: React.FunctionComponent = props => { const eventHub = injector.getEventHub(repo.configuration.repositoryUrl) const [reloadToken, setReloadToken] = useState(1) + const logger = useLogger('CurrentAncestorsProvider') + const requestReload = debounce(() => setReloadToken(Math.random()), 100) useEffect(() => { @@ -53,19 +55,24 @@ export const CurrentAncestorsProvider: React.FunctionComponent = props => { ;(async () => { try { await loadLock.acquire() - const ancestorsResult = await repo.executeAction>({ - idOrPath: currentContent.Id, - method: 'GET', - name: 'Ancestors', - body: undefined, - requestInit: { - signal: ac.signal, - }, - oDataOptions: { - orderby: [['Path', 'asc']], - }, - }) - setAncestors(ancestorsResult.d.results) + if ((props.root && currentContent.Id === props.root) || currentContent.Path == props.root) { + setAncestors([]) + } else { + const ancestorsResult = await repo.executeAction>({ + idOrPath: currentContent.Id, + method: 'GET', + name: 'Ancestors', + body: undefined, + requestInit: { + signal: ac.signal, + }, + oDataOptions: { + orderby: [['Path', 'asc']], + }, + }) + const rootIndex = ancestorsResult.d.results.findIndex(a => a.Id === props.root || a.Path === props.root) + setAncestors(rootIndex > 0 ? ancestorsResult.d.results.slice(rootIndex) : ancestorsResult.d.results) + } } catch (err) { if (!ac.signal.aborted) { setError(err) @@ -75,10 +82,13 @@ export const CurrentAncestorsProvider: React.FunctionComponent = props => { } })() return () => ac.abort() - }, [currentContent.Id, loadLock, reloadToken, repo]) + }, [currentContent.Id, currentContent.Path, loadLock, props.root, reloadToken, repo]) if (error) { - throw error + logger.warning({ + message: `Error loading ancestors. ${error.message}`, + data: { details: { error }, relatedContent: currentContent, relatedRepository: repo.configuration.repositoryUrl }, + }) } return {props.children} } diff --git a/apps/sensenet/src/context/CurrentContent.tsx b/apps/sensenet/src/context/CurrentContent.tsx index 65dd77dd6..82dd0e374 100644 --- a/apps/sensenet/src/context/CurrentContent.tsx +++ b/apps/sensenet/src/context/CurrentContent.tsx @@ -1,4 +1,4 @@ -import { ConstantContent } from '@sensenet/client-core' +import { ConstantContent, ODataParams } from '@sensenet/client-core' import { GenericContent } from '@sensenet/default-content-types' import React, { useEffect, useState } from 'react' import Semaphore from 'semaphore-async-await' @@ -8,6 +8,7 @@ export const CurrentContentContext = React.createContext(Constan export const CurrentContentProvider: React.FunctionComponent<{ idOrPath: number | string onContentLoaded?: (content: GenericContent) => void + oDataOptions?: ODataParams }> = props => { const [loadLock] = useState(new Semaphore(1)) const [content, setContent] = useState(ConstantContent.PORTAL_ROOT) @@ -35,7 +36,11 @@ export const CurrentContentProvider: React.FunctionComponent<{ if (props.idOrPath) { ;(async () => { try { - const response = await repo.load({ idOrPath: props.idOrPath, requestInit: { signal: ac.signal } }) + const response = await repo.load({ + idOrPath: props.idOrPath, + requestInit: { signal: ac.signal }, + oDataOptions: props.oDataOptions, + }) setContent(response.d) props.onContentLoaded && props.onContentLoaded(response.d) } catch (err) { @@ -48,7 +53,8 @@ export const CurrentContentProvider: React.FunctionComponent<{ })() } return () => ac.abort() - }, [repo, props.idOrPath, reloadToken, props, loadLock]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [repo, props.idOrPath, reloadToken, loadLock]) if (error) { throw error diff --git a/apps/sensenet/src/context/LoadSettingsContext.tsx b/apps/sensenet/src/context/LoadSettingsContext.tsx index d5ed6f531..fada93602 100644 --- a/apps/sensenet/src/context/LoadSettingsContext.tsx +++ b/apps/sensenet/src/context/LoadSettingsContext.tsx @@ -14,7 +14,7 @@ export const LoadSettingsContext = React.createContext<{ export const LoadSettingsContextProvider: React.FunctionComponent = props => { const [loadSettings, setLoadSettings] = useState>({}) const [loadChildrenSettings, setLoadChildrenSettings] = useState>({ - orderby: [['DisplayName', 'asc'], ['Name', 'asc']], + orderby: [['DisplayName', 'asc']], select: 'all', expand: 'CreatedBy', }) diff --git a/apps/sensenet/src/context/LoggerContext.tsx b/apps/sensenet/src/context/LoggerContext.tsx index 9f4085779..b4fb2543f 100644 --- a/apps/sensenet/src/context/LoggerContext.tsx +++ b/apps/sensenet/src/context/LoggerContext.tsx @@ -1,8 +1,8 @@ -import { ILogger, LoggerCollection } from '@furystack/logging' +import { Logger, LoggerCollection } from '@furystack/logging' import React from 'react' import { useInjector } from '../hooks' -export const LoggerContext = React.createContext(new LoggerCollection()) +export const LoggerContext = React.createContext(new LoggerCollection()) export const LoggerContextProvider: React.FunctionComponent = ({ children }) => { const injector = useInjector() diff --git a/apps/sensenet/src/context/PersonalSettingsContext.tsx b/apps/sensenet/src/context/PersonalSettingsContext.tsx index b975c9f4a..16e37ca51 100644 --- a/apps/sensenet/src/context/PersonalSettingsContext.tsx +++ b/apps/sensenet/src/context/PersonalSettingsContext.tsx @@ -5,11 +5,11 @@ export const PersonalSettingsContext = React.createContext(defaultSettings) export const PersonalSettingsContextProvider: React.StatelessComponent = props => { const di = useInjector() const settingsService = di.getInstance(PersonalSettings) - const [settings, setSettings] = useState(settingsService.currentValue.getValue()) + const [settings, setSettings] = useState(settingsService.effectiveValue.getValue()) useEffect(() => { - settingsService.currentValue.subscribe(s => { + settingsService.effectiveValue.subscribe(s => { setSettings(s) }) - }, [settingsService.currentValue]) + }, [settingsService.effectiveValue]) return {props.children} } diff --git a/apps/sensenet/src/context/RepositoryContext.tsx b/apps/sensenet/src/context/RepositoryContext.tsx index 301539c94..8be84066c 100644 --- a/apps/sensenet/src/context/RepositoryContext.tsx +++ b/apps/sensenet/src/context/RepositoryContext.tsx @@ -32,9 +32,7 @@ export const RepositoryContextProviderComponent: React.FunctionComponent< repoUrl && repoUrl.length && repoUrl !== repo.configuration.repositoryUrl && injector.getRepository(repoUrl) if (newRepo) { logger.debug({ - message: `Swithed from repository ${repo.configuration.repositoryUrl} to ${ - newRepo.configuration.repositoryUrl - }`, + message: `Swithed from repository ${repo.configuration.repositoryUrl} to ${newRepo.configuration.repositoryUrl}`, data: { digestMessage: 'Repository switched {count} times', multiple: true, diff --git a/apps/sensenet/src/hooks/index.ts b/apps/sensenet/src/hooks/index.ts index 338e674df..8cf43e75f 100644 --- a/apps/sensenet/src/hooks/index.ts +++ b/apps/sensenet/src/hooks/index.ts @@ -1,5 +1,7 @@ export * from './use-content-routing' +export * from './use-command-palette' export * from './use-download' +export * from './use-drawer-items' export * from './use-event-service' export * from './use-injector' export * from './use-localization' @@ -8,6 +10,8 @@ export * from './use-personal-settings' export * from './use-repository' export * from './use-selection-service' export * from './use-session' +export * from './use-string-replace' export * from './use-theme' +export * from './use-version-info' export * from './use-widgets' export * from './use-wopi' diff --git a/apps/sensenet/src/hooks/use-command-palette.tsx b/apps/sensenet/src/hooks/use-command-palette.tsx new file mode 100644 index 000000000..3a181afa1 --- /dev/null +++ b/apps/sensenet/src/hooks/use-command-palette.tsx @@ -0,0 +1,52 @@ +import { useContext, useEffect, useState } from 'react' +import { GenericContent } from '@sensenet/default-content-types' +import { CommandProviderManager } from '../services/CommandProviderManager' +import { ResponsiveContext } from '../context' +import { useInjector } from './use-injector' +import { useRepository } from './use-repository' + +export interface CommandPaletteItem { + primaryText: string + secondaryText: string + url: string + hits: string[] + content?: GenericContent + openAction?: () => void +} + +export const useCommandPalette = () => { + const [inputValue, setInputValue] = useState('') + const [isOpened, setIsOpened] = useState(false) + const [items, setItems] = useState([]) + + const defaultRepository = useRepository() + const [repository, setRepository] = useState(defaultRepository) + const device = useContext(ResponsiveContext) + + const injector = useInjector() + + useEffect(() => { + ;(async () => { + const cpm = injector.getInstance(CommandProviderManager) + const foundItems = await cpm.getItems({ term: inputValue, repository, device }) + setItems(foundItems) + })() + }, [device, injector, inputValue, repository]) + + useEffect(() => { + if (!isOpened) { + setItems([]) + setInputValue('') + } + }, [isOpened]) + + return { + inputValue, + setInputValue, + isOpened, + setIsOpened, + items, + setItems, + setRepository, + } +} diff --git a/apps/sensenet/src/hooks/use-drawer-items.tsx b/apps/sensenet/src/hooks/use-drawer-items.tsx new file mode 100644 index 000000000..d183bbf21 --- /dev/null +++ b/apps/sensenet/src/hooks/use-drawer-items.tsx @@ -0,0 +1,180 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react' +import BuildTwoTone from '@material-ui/icons/BuildTwoTone' +import InfoTwoTone from '@material-ui/icons/InfoTwoTone' +import LanguageTwoTone from '@material-ui/icons/LanguageTwoTone' +import PeopleTwoTone from '@material-ui/icons/PeopleTwoTone' +import PublicTwoTone from '@material-ui/icons/PublicTwoTone' +import SearchTwoTone from '@material-ui/icons/SearchTwoTone' +import WidgetsTwoTone from '@material-ui/icons/WidgetsTwoTone' +import { DashboardTwoTone } from '@material-ui/icons' +import { Group, User } from '@sensenet/default-content-types' +import { Query } from '@sensenet/query' +import { Icon } from '../components/Icon' +import { + BuiltinDrawerItem, + ContentDrawerItem, + DashboardDrawerItem, + DrawerItem as DrawerItemSetting, + QueryDrawerItem, +} from '../services/PersonalSettings' +import { ResponsivePersonalSetttings } from '../context' +import { encodeBrowseData } from '../components/content' +import { encodeQueryData } from '../components/search' +import DefaultLocalization from '../localization/default' +import { useSession } from './use-session' +import { useLocalization } from './use-localization' + +export interface DrawerItem { + name: string + primaryText: keyof (typeof DefaultLocalization.drawer.titles) + secondaryText: keyof (typeof DefaultLocalization.drawer.descriptions) + url: string + icon: JSX.Element + requiredGroupPath: string +} + +export const useDrawerItems = () => { + const session = useSession() + const settings = useContext(ResponsivePersonalSetttings) + const localization = useLocalization().drawer + + const [drawerItems, setDrawerItems] = useState([]) + + const getItemNameFromSettings = useCallback( + (item: DrawerItemSetting) => { + return ( + (item.settings && item.settings.title) || + localization.titles[item.itemType as keyof typeof localization.titles] || + '!NO TITLE!' + ) + }, + [localization], + ) + + const getItemDescriptionFromSettings = useCallback( + (item: DrawerItemSetting) => { + return ( + (item.settings && item.settings.description) || + localization.descriptions[item.itemType as keyof typeof localization.titles] + ) + }, + [localization], + ) + + const getIconFromSetting = useCallback( + (item: ContentDrawerItem | QueryDrawerItem | BuiltinDrawerItem | DashboardDrawerItem) => { + switch (item.itemType) { + case 'Search': + return + case 'Content': + return item.settings && item.settings.icon ? ( + + ) : ( + + ) + case 'Users and groups': + return + case 'Content Types': + return + case 'Localization': + return + case 'Trash': + return + case 'Setup': + return + case 'Version info': + return + case 'Dashboard': + return + default: + return ( + + ) + } + }, + [], + ) + + const getUrlFromSetting = useCallback( + (item: ContentDrawerItem | QueryDrawerItem | BuiltinDrawerItem | DashboardDrawerItem) => { + switch (item.itemType) { + case 'Search': + return '/saved-queries' + case 'Content': + return `/browse/${encodeBrowseData({ + type: (item.settings && item.settings.browseType) || settings.content.browseType, + root: (item.settings && item.settings.root) || '/Root/Content', + secondaryContent: (item.settings && item.settings.root) || '/Root/Content', + fieldsToDisplay: (item.settings && item.settings.columns) || settings.content.fields, + })}` + case 'Users and groups': + return `/search/${encodeQueryData({ + title: localization.titles['Users and groups'], + term: new Query(q => + q + .typeIs(User) + .or.typeIs(Group) + .and.inTree('/Root/IMS'), + ).toString(), + hideSearchBar: true, + fieldsToDisplay: ['DisplayName', 'ModificationDate', 'ModifiedBy', 'Actions'], + })}` + case 'Content Types': + return `/search/${encodeQueryData({ + title: localization.titles['Content Types'], + term: "+TypeIs:'ContentType'", + hideSearchBar: true, + fieldsToDisplay: ['DisplayName', 'Description', 'ParentTypeName' as any, 'ModificationDate', 'ModifiedBy'], + })}` + case 'Query': + return `/search/${encodeQueryData({ + term: (item.settings && item.settings.term) || '', + title: item.settings && item.settings.title, + hideSearchBar: true, + fieldsToDisplay: item.settings && item.settings.columns, + })}` + case 'Localization': + return `/search/${encodeQueryData({ + term: "+TypeIs:'Resource'", + title: localization.titles.Localization, + hideSearchBar: true, + })}` + case 'Trash': + return '' // ToDO + case 'Setup': + return '/setup' + case 'Version info': + return '/info' + case 'Dashboard': + return `/dashboard/${encodeURIComponent(item.settings ? item.settings.dashboardName : '')}` + default: + // return '' + break + } + + return '/' + }, + [localization.titles, settings.content.browseType, settings.content.fields], + ) + + const getItemFromSettings = useCallback( + (setting: DrawerItemSetting) => { + const drawerItem: DrawerItem = { + icon: getIconFromSetting(setting), + primaryText: getItemNameFromSettings(setting), + secondaryText: getItemDescriptionFromSettings(setting), + name: setting.itemType, + requiredGroupPath: '', + url: getUrlFromSetting(setting), + } + return drawerItem + }, + [getIconFromSetting, getItemDescriptionFromSettings, getItemNameFromSettings, getUrlFromSetting], + ) + + useEffect(() => { + setDrawerItems(settings.drawer.items.map(item => getItemFromSettings(item))) + }, [getItemFromSettings, session, settings]) + + return drawerItems +} diff --git a/apps/sensenet/src/hooks/use-string-replace.ts b/apps/sensenet/src/hooks/use-string-replace.ts new file mode 100644 index 000000000..e81454034 --- /dev/null +++ b/apps/sensenet/src/hooks/use-string-replace.ts @@ -0,0 +1,39 @@ +import { useContext, useEffect, useState } from 'react' +import { SessionContext } from '../context' +import { useRepository } from './use-repository' +import { usePersonalSettings } from './use-personal-settings' + +export const useStringReplace = (content: string) => { + const [replacedContent, setReplacedContent] = useState('') + const session = useContext(SessionContext) + const repo = useRepository() + const personalSettings = usePersonalSettings() + + useEffect(() => { + const currentRepo = personalSettings.repositories.find(r => r.url === repo.configuration.repositoryUrl) + + const newReplacedContent = content + .replace( + '{currentUserName}', + session.currentUser.FullName || session.currentUser.DisplayName || session.currentUser.Name, + ) + .replace( + '{currentRepositoryName}', + currentRepo && currentRepo.displayName + ? currentRepo.displayName + : repo.configuration.repositoryUrl || repo.configuration.repositoryUrl, + ) + .replace('{currentRepositoryUrl}', repo.configuration.repositoryUrl) + + setReplacedContent(newReplacedContent) + }, [ + personalSettings.repositories, + content, + repo.configuration.repositoryUrl, + session.currentUser.DisplayName, + session.currentUser.FullName, + session.currentUser.Name, + ]) + + return replacedContent +} diff --git a/apps/sensenet/src/hooks/use-version-info.ts b/apps/sensenet/src/hooks/use-version-info.ts new file mode 100644 index 000000000..35278351a --- /dev/null +++ b/apps/sensenet/src/hooks/use-version-info.ts @@ -0,0 +1,61 @@ +import { useEffect, useState } from 'react' +import { ConstantContent } from '@sensenet/client-core' +import { VersionInfo } from '../components/version-info/version-info-models' +import { useRepository } from './use-repository' + +export const useVersionInfo = () => { + const [versionInfo, setVersionInfo] = useState() + const [nugetManifests, setNugetManifests] = useState([]) + const [hasUpdates, setHasUpdates] = useState(false) + const repo = useRepository() + + useEffect(() => { + ;(async () => { + const result = await repo.executeAction({ + idOrPath: ConstantContent.PORTAL_ROOT.Path, + body: undefined, + method: 'GET', + name: 'GetVersionInfo', + }) + + const nugetPromises = result.Components.map(async component => { + try { + const response = await fetch( + `https://api.nuget.org/v3/registration3-gz-semver2/${component.ComponentId.toLowerCase()}/index.json`, + ) + if (response.ok) { + const nugetManifest = await response.json() + return nugetManifest + } + } catch (error) { + return {} + } + }) + const loadedManifests = (await Promise.all(nugetPromises)).filter(m => m) + setNugetManifests(loadedManifests) + + let hasOneUpdate = false + + result.Components = result.Components.map(component => { + const nugetManifest = loadedManifests.find( + m => + m['@id'] === + `https://api.nuget.org/v3/registration3-gz-semver2/${component.ComponentId.toLocaleLowerCase()}/index.json`, + ) + const updateAvailable = nugetManifest ? nugetManifest.items[0].upper > component.Version : false + if (updateAvailable) { + hasOneUpdate = true + } + return { + ...component, + NugetManifest: nugetManifest, + IsUpdateAvailable: updateAvailable, + } + }) + setHasUpdates(hasOneUpdate) + setVersionInfo(result) + })() + }, [repo]) + + return { versionInfo, nugetManifests, hasUpdates } +} diff --git a/apps/sensenet/src/hooks/use-widgets.ts b/apps/sensenet/src/hooks/use-widgets.ts index 902ac9dfa..f7d0461f4 100644 --- a/apps/sensenet/src/hooks/use-widgets.ts +++ b/apps/sensenet/src/hooks/use-widgets.ts @@ -1,11 +1,15 @@ import { Repository } from '@sensenet/client-core' import { usePersonalSettings } from './use-personal-settings' -export const useWidgets = (repository?: Repository) => { +export const useWidgets = (repository?: Repository, dashboardName?: string) => { const personalSettings = usePersonalSettings() if (repository) { const currentRepo = personalSettings.repositories.find(r => r.url === repository.configuration.repositoryUrl) - return currentRepo && currentRepo.dashboard ? currentRepo.dashboard : personalSettings.dashboards.repositoryDefault + return dashboardName && personalSettings.dashboards[dashboardName] + ? personalSettings.dashboards[dashboardName] + : currentRepo && currentRepo.dashboard + ? currentRepo.dashboard + : personalSettings.dashboards.repositoryDefault } return personalSettings.dashboards.globalDefault } diff --git a/apps/sensenet/src/hooks/use-wopi.ts b/apps/sensenet/src/hooks/use-wopi.ts index 51f791193..64be9b986 100644 --- a/apps/sensenet/src/hooks/use-wopi.ts +++ b/apps/sensenet/src/hooks/use-wopi.ts @@ -1,4 +1,4 @@ -import { GenericContent, File, ActionModel } from '@sensenet/default-content-types' +import { ActionModel, File, GenericContent } from '@sensenet/default-content-types' import { isContentFromType } from '../utils/isContentFromType' import { useRepository } from './use-repository' export const useWopi = (content: GenericContent) => { diff --git a/apps/sensenet/src/index.tsx b/apps/sensenet/src/index.tsx index 713c512fe..b5147528c 100644 --- a/apps/sensenet/src/index.tsx +++ b/apps/sensenet/src/index.tsx @@ -14,8 +14,8 @@ import { RepositoryContextProvider, ResponsiveContextProvider, SessionContextProvider, - ThemeProvider, snInjector, + ThemeProvider, } from './context' import { LoggerContextProvider } from './context/LoggerContext' import { CommandProviderManager } from './services/CommandProviderManager' diff --git a/apps/sensenet/src/localization/default.ts b/apps/sensenet/src/localization/default.ts index 3afe4bd48..cba35d38e 100644 --- a/apps/sensenet/src/localization/default.ts +++ b/apps/sensenet/src/localization/default.ts @@ -1,6 +1,13 @@ const values = { dashboard: { errorLoadingWidget: 'Error loading widget :(', + refresh: 'Refresh', + openInSearch: 'Open in Search', + updates: { + title: 'Packages to update', + allUpToDate: 'All packages are up to date', + view: 'View', + }, }, addButton: { tooltip: 'Create or upload content', @@ -54,6 +61,8 @@ const values = { cancelButton: 'Cancel', deleteSuccessNotification: `Content '{0}' has been deleted succesfully`, deleteMultipleSuccessNotification: `{0} content deleted succesfully`, + deleteSingleContentFailedNotification: `There was an error deleting content '{0}': {1}`, + deleteMultipleContentFailedNotification: `There was an error deleting {0} content.`, deleteFailedNotification: `There was an error during content deletion.`, }, copyMoveContentDialog: { @@ -81,28 +90,34 @@ const values = { }, }, drawer: { + titles: { + Content: 'Content', + 'Content Types': 'Content Types', + Localization: 'Localization', + Search: 'Search', + Setup: 'Setup', + Trash: 'Trash', + 'Version info': 'Version Info', + 'Users and groups': 'Users and groups', + }, + descriptions: { + Content: 'Explore and manage your content in the repository', + 'Content Types': 'Manage content types', + Localization: 'Manage string resources', + Search: 'Execute custom searches, build and save queries', + Setup: 'Configure the sensenet system', + Trash: 'Manage deleted items here: restore content or purge them permanently', + 'Version info': 'Detailed version information about the current sensenet installation', + 'Users and groups': 'Manage users and groups, roles and identities', + }, personalSettingsTitle: 'Edit personal settings', personalSettingsSecondaryText: 'Customize the application behavior', - contentTitle: 'Content', - contentSecondaryText: 'Explore and manage your content in the repository', - searchTitle: 'Search', - searchSecondaryText: 'Execute custom searches, build and save queries', - usersAndGroupsTitle: 'Users and groups', - usersAndGroupsSecondaryText: 'Manage users and groups, roles and identities', - setupTitle: 'Setup', - setupSecondaryText: 'Configure the sensenet system', - versionInfoTitle: 'Version Info', - versionInfoSecondaryText: 'Detailed version information about the current sensenet installation', - contentTypesTitle: 'Content Types', - contentTypesSecondaryText: 'Manage content types', - localizationTitle: 'Localization', - localizationSecondaryText: 'Manage string resources', - trashTitle: 'Trash', - trashSecondaryText: 'Manage deleted items here: restore content or purge them permanently', + contentRootDescription: 'The root path. Content will be displayed below this level.', dashboardTitle: 'Dashboard', dashboardSecondaryText: 'Repository overview', expand: 'Expand', collapse: 'Collapse', + queryTermDescription: 'The Query term', }, editPropertiesDialog: { dialogTitle: 'Edit {0}', @@ -110,7 +125,7 @@ const values = { saveFailedNotification: `There was an error during updating content '{0}'`, }, login: { - loginTitle: 'Login', + loginTitle: "It's good to see you!", loginButtonTitle: 'Login', userNameLabel: 'UserName', userNameHelperText: "Enter the user name you've registered with", @@ -121,6 +136,15 @@ const values = { loginFailed: 'Login failed.', greetings: 'Greetings, {0}!', loggingInTo: 'Logging in to {0}...', + youCanLogInWith: 'You can also log in with', + logInWithSso: 'Log in via SSO', + resetPassword: 'Reset password', + resendConfirmation: 'Resend confirmation', + unlockAccount: 'Unlock account', + newToSensenet: 'New to sensenet?', + help: 'Help', + contactUs: 'Contact us', + signUp: 'Sign up', loginSuccessNotification: `Logged in with user '{0} to repository '{1}'`, loginFailedNotification: `Failed to log in with user '{0}' to repository '{1}'`, loginErrorNotification: `There was an error during login with user '{0}' to repository '{1}'`, @@ -134,11 +158,23 @@ const values = { logoutCancel: 'Cancel', }, personalSettings: { + defaults: 'Defaults', + showDefaults: 'Show defaults', + restoreDefaults: 'Restore defaults', + restoreDialogTitle: 'Really restore defaults?', + restoreDialogTText: + 'Are you sure you want to restore the default settings? Your log will also be cleared and you will be signed out from all repositories.', + cancel: 'Cancel', + restore: 'Restore', + restoringDefaultsProgress: 'Restoring the default settings...', title: 'Personal settings', drawer: 'Options for the left drawer', drawerEnable: 'Enable or disable the drawer', drawerType: 'Drawer type', drawerItems: 'Items enabled on the drawer', + drawerItemTitle: 'Title of the item', + drawerItemDescription: 'Description of the item', + drawerItemIcon: 'The icon of the drawer item', repositoryTitle: 'A list of visited repositories', repositoryUrl: 'The path of the repository, e.g.: https://my-sensenet-repository.org', repositoryLoginName: "The last user you've logged in with", @@ -156,6 +192,28 @@ const values = { themeTitle: 'Select a dark or a light theme', eventLogSize: 'Number of entries to store in the Event Log', sendLogWithCrashReports: 'Send log data with crash reports by default', + dashboard: { + widgetName: 'Widget', + minWidth: 'The minimum width of the widget in pixels', + widgetType: 'Type of the widget', + title: 'Widget title', + queryWidget: { + settings: 'Settings for the Query widget', + query: 'The content query expression', + emptyPlaceholderText: 'The text that will be displayed if the query has no hits', + showColumnNames: 'Show or hide column names', + top: 'Limits the number of hits', + showOpenInSearch: 'Option for a button to open the query in the Search view', + showRefresh: 'Option for a refresh button', + enableSelection: 'Enable content selection', + countOnly: 'Displays only the hits count instead of a content list', + columns: 'Array of columns to display', + }, + markdownWidget: { + settings: 'Settings for the Markdown Widget', + content: 'The Markdown content to display', + }, + }, }, repositorySelector: { loggedInAs: 'You are currently logged in as {0}', diff --git a/apps/sensenet/src/localization/hungarian.ts b/apps/sensenet/src/localization/hungarian.ts index 96c669e0d..5c20039cc 100644 --- a/apps/sensenet/src/localization/hungarian.ts +++ b/apps/sensenet/src/localization/hungarian.ts @@ -35,24 +35,29 @@ const values: DeepPartial = { cancelButton: 'Mégsem', }, drawer: { - contentTitle: 'Tartalom', - contentSecondaryText: 'Tartalom böngészése', - searchTitle: 'Keresés', - searchSecondaryText: 'Testreszabott keresések futtatása és mentése későbbi használatra', - setupTitle: 'Beállítás', - setupSecondaryText: 'A rendszer beállításai', - usersAndGroupsTitle: 'Felhasználók és csoportok', - usersAndGroupsSecondaryText: 'Felhasználó és csoport kezelése, szerkesztése', - versionInfoTitle: 'Verzió névjegye', - versionInfoSecondaryText: 'Információk a telepített csomagokról és a verzióikról', + titles: { + Content: 'Tartalom', + Search: 'Keresés', + Setup: 'Beállítás', + 'Users and groups': 'Felhasználók és csoportok', + 'Version info': 'Verzió névjegye', + 'Content Types': 'Tartalom típusok', + Localization: 'Nyelvi fájlok', + Trash: 'Kuka', + }, + descriptions: { + Content: 'Tartalom böngészése', + Search: 'Testreszabott keresések futtatása és mentése későbbi használatra', + Setup: 'A rendszer beállításai', + 'Users and groups': 'Felhasználó és csoport kezelése, szerkesztése', + 'Version info': 'Információk a telepített csomagokról és a verzióikról', + 'Content Types': 'Tartalom típusok kezelése', + Localization: 'Nyelvi fájlok kezelése', + Trash: 'Törölt elemek kezelése', + }, + personalSettingsTitle: 'Személyes beállítások', personalSettingsSecondaryText: 'Az alkalmazás testreszabása', - contentTypesTitle: 'Tartalom típusok', - contentTypesSecondaryText: 'Tartalom típusok kezelése', - localizationTitle: 'Nyelvi fájlok', - localizationSecondaryText: 'Nyelvi fájlok kezelése', - trashTitle: 'Kuka', - trashSecondaryText: 'Törölt elemek kezelése', dashboardTitle: 'Irányítópult', dashboardSecondaryText: 'Olyan egyoldalas vizuális felület, amelynek segítségével a felhasználó első ránézésre monitorozhatja legfontosabb céljainak vagy elvárásainak megvalósulását', diff --git a/apps/sensenet/src/services/CommandProviderManager.ts b/apps/sensenet/src/services/CommandProviderManager.ts index efbcfdf26..0114c1fe4 100644 --- a/apps/sensenet/src/services/CommandProviderManager.ts +++ b/apps/sensenet/src/services/CommandProviderManager.ts @@ -1,10 +1,17 @@ import { Injectable, Injector } from '@furystack/inject' import { Repository } from '@sensenet/client-core' -import { CommandPaletteItem } from '../store/CommandPalette' +import { CommandPaletteItem } from '../hooks' +import { ResponsivePlatforms } from '../context' export interface CommandProvider { - shouldExec: (term: string) => boolean - getItems: (term: string, repo: Repository) => Promise + shouldExec: (options: SearchOptions) => boolean + getItems: (options: SearchOptions) => Promise +} + +export interface SearchOptions { + term: string + repository: Repository + device: ResponsivePlatforms } @Injectable({ lifetime: 'singleton' }) @@ -19,8 +26,8 @@ export class CommandProviderManager { } } - public async getItems(term: string, repo: Repository) { - const promises = this.Providers.filter(p => p.shouldExec(term)).map(provider => provider.getItems(term, repo)) + public async getItems(options: SearchOptions) { + const promises = this.Providers.filter(p => p.shouldExec(options)).map(provider => provider.getItems(options)) const results = await Promise.all(promises) return results.reduce((acc, val) => acc.concat(val), []) // flattern } diff --git a/apps/sensenet/src/services/CommandProviders/CheatCommandProvider.ts b/apps/sensenet/src/services/CommandProviders/CheatCommandProvider.ts index 7206cba72..ea229c9e9 100644 --- a/apps/sensenet/src/services/CommandProviders/CheatCommandProvider.ts +++ b/apps/sensenet/src/services/CommandProviders/CheatCommandProvider.ts @@ -1,6 +1,6 @@ import { Injectable } from '@furystack/inject' -import { CommandPaletteItem } from '../../store/CommandPalette' -import { CommandProvider } from '../CommandProviderManager' +import { CommandProvider, SearchOptions } from '../CommandProviderManager' +import { CommandPaletteItem } from '../../hooks' @Injectable({ lifetime: 'singleton' }) export class CheatCommandProvider implements CommandProvider { @@ -33,10 +33,10 @@ export class CheatCommandProvider implements CommandProvider { }, } - public shouldExec(term: string) { - return Object.keys(this.items).indexOf(term) !== -1 + public shouldExec(options: SearchOptions) { + return Object.keys(this.items).indexOf(options.term) !== -1 } - public async getItems(term: string): Promise { - return (this.items as any)[term] + public async getItems(options: SearchOptions): Promise { + return (this.items as any)[options.term] } } diff --git a/apps/sensenet/src/services/CommandProviders/CustomActionCommandProvider.ts b/apps/sensenet/src/services/CommandProviders/CustomActionCommandProvider.ts index c0ce4fc46..4d264c09a 100644 --- a/apps/sensenet/src/services/CommandProviders/CustomActionCommandProvider.ts +++ b/apps/sensenet/src/services/CommandProviders/CustomActionCommandProvider.ts @@ -1,8 +1,8 @@ import { Injectable } from '@furystack/inject' -import { MetadataAction, Repository } from '@sensenet/client-core' +import { MetadataAction } from '@sensenet/client-core' import { ObservableValue } from '@sensenet/client-utils' import { ActionModel, GenericContent } from '@sensenet/default-content-types' -import { CommandProvider } from '../CommandProviderManager' +import { CommandProvider, SearchOptions } from '../CommandProviderManager' import { LocalizationService } from '../LocalizationService' import { SelectionService } from '../SelectionService' @@ -17,17 +17,19 @@ export class CustomActionCommandProvider implements CommandProvider { public onActionExecuted = new ObservableValue<{ content: GenericContent; action: ActionModel; response: any }>() - public shouldExec(term: string) { - return this.selectionService.activeContent.getValue() && term.length > 2 && term.startsWith('>') ? true : false + public shouldExec(options: SearchOptions) { + return this.selectionService.activeContent.getValue() && options.term.length > 2 && options.term.startsWith('>') + ? true + : false } - public async getItems(_term: string, repo: Repository) { + public async getItems(options: SearchOptions) { const content = this.selectionService.activeContent.getValue() const localization = this.localization.currentValues.getValue().commandPalette.customAction - const filteredTerm = _term.substr(1).toLowerCase() + const filteredTerm = options.term.substr(1).toLowerCase() if (!content) { return [] } - const result = await repo.load({ + const result = await options.repository.load({ idOrPath: content.Id, oDataOptions: { metadata: 'full', diff --git a/apps/sensenet/src/services/CommandProviders/HelpCommandProvider.ts b/apps/sensenet/src/services/CommandProviders/HelpCommandProvider.ts index eb27dfc7e..ce8e3b36e 100644 --- a/apps/sensenet/src/services/CommandProviders/HelpCommandProvider.ts +++ b/apps/sensenet/src/services/CommandProviders/HelpCommandProvider.ts @@ -1,15 +1,15 @@ import { Injectable } from '@furystack/inject' -import { CommandPaletteItem } from '../../store/CommandPalette' -import { CommandProvider } from '../CommandProviderManager' +import { CommandProvider, SearchOptions } from '../CommandProviderManager' import { LocalizationService } from '../LocalizationService' +import { CommandPaletteItem } from '../../hooks' @Injectable({ lifetime: 'transient' }) export class HelpCommandProvider implements CommandProvider { - public shouldExec(term: string) { + public shouldExec({ term }: SearchOptions) { return term === '?' || term === 'help' } - public async getItems(term: string): Promise { + public async getItems({ term }: SearchOptions): Promise { return [ { primaryText: this.localizationService.currentValues.getValue().commandPalette.help.readMeTitle, diff --git a/apps/sensenet/src/services/CommandProviders/HistoryCommandProvider.ts b/apps/sensenet/src/services/CommandProviders/HistoryCommandProvider.ts index 906337ff2..eb5b16a26 100644 --- a/apps/sensenet/src/services/CommandProviders/HistoryCommandProvider.ts +++ b/apps/sensenet/src/services/CommandProviders/HistoryCommandProvider.ts @@ -1,10 +1,10 @@ import { Injectable } from '@furystack/inject' -import { CommandPaletteItem } from '../../store/CommandPalette' -import { CommandProvider } from '../CommandProviderManager' +import { CommandProvider, SearchOptions } from '../CommandProviderManager' +import { CommandPaletteItem } from '../../hooks' @Injectable({ lifetime: 'singleton' }) export class HistoryCommandProvider implements CommandProvider { - public shouldExec(term: string) { + public shouldExec({ term }: SearchOptions) { return term.length === 0 } public async getItems(): Promise { diff --git a/apps/sensenet/src/services/CommandProviders/InFolderSearchCommandProvider.ts b/apps/sensenet/src/services/CommandProviders/InFolderSearchCommandProvider.ts index 71495f9d0..1bb7ff8e8 100644 --- a/apps/sensenet/src/services/CommandProviders/InFolderSearchCommandProvider.ts +++ b/apps/sensenet/src/services/CommandProviders/InFolderSearchCommandProvider.ts @@ -1,26 +1,26 @@ import { Injectable } from '@furystack/inject' -import { ConstantContent, Repository } from '@sensenet/client-core' +import { ConstantContent } from '@sensenet/client-core' import { PathHelper } from '@sensenet/client-utils' import { GenericContent } from '@sensenet/default-content-types' import { Query } from '@sensenet/query' -import { CommandPaletteItem } from '../../store/CommandPalette' -import { CommandProvider } from '../CommandProviderManager' +import { CommandProvider, SearchOptions } from '../CommandProviderManager' import { ContentContextProvider } from '../ContentContextProvider' +import { CommandPaletteItem } from '../../hooks' @Injectable({ lifetime: 'singleton' }) export class InFolderSearchCommandProvider implements CommandProvider { - public shouldExec(searchTerm: string): boolean { - return searchTerm[0] === '/' + public shouldExec({ term }: SearchOptions): boolean { + return term[0] === '/' } - public async getItems(path: string, repo: Repository): Promise { - const currentPath = PathHelper.trimSlashes(path) + public async getItems(options: SearchOptions): Promise { + const currentPath = PathHelper.trimSlashes(options.term) const segments = currentPath.split('/') - const ctx = new ContentContextProvider(repo) + const ctx = new ContentContextProvider(options.repository) const parentPath = PathHelper.trimSlashes( PathHelper.joinPaths(...segments.slice(0, segments.length - 1)) || currentPath, ) - const result = await repo.loadCollection({ + const result = await options.repository.loadCollection({ path: ConstantContent.PORTAL_ROOT.Path, oDataOptions: { query: new Query(q => @@ -38,7 +38,7 @@ export class InFolderSearchCommandProvider implements CommandProvider { secondaryText: content.Path, content, url: ctx.getPrimaryActionUrl(content), - hits: [path], + hits: [options.term], })) } } diff --git a/apps/sensenet/src/services/CommandProviders/NavigationCommandProvider.ts b/apps/sensenet/src/services/CommandProviders/NavigationCommandProvider.ts index 0d42e2dcf..65dbbcb4f 100644 --- a/apps/sensenet/src/services/CommandProviders/NavigationCommandProvider.ts +++ b/apps/sensenet/src/services/CommandProviders/NavigationCommandProvider.ts @@ -1,60 +1,71 @@ import { Injectable } from '@furystack/inject' -import { Repository } from '@sensenet/client-core' -import { CommandPaletteItem } from '../../store/CommandPalette' -import { CommandProvider } from '../CommandProviderManager' +import { CommandProvider, SearchOptions } from '../CommandProviderManager' import { LocalizationService } from '../LocalizationService' +import { CommandPaletteItem } from '../../hooks' @Injectable({ lifetime: 'transient' }) export class NavigationCommandProvider implements CommandProvider { - public getRoutes: (term: string) => Array = term => [ - { - primaryText: this.localizationValues.personalSettingsPrimary, - url: '/personalSettings', - secondaryText: this.localizationValues.personalSettingsSecondary, - content: { Type: 'Settings' } as any, - keywords: 'settings setup personal settings language theme', - hits: [term], - }, - { - primaryText: this.localizationValues.contentPrimary, - url: '/:repo/browse/', - secondaryText: this.localizationValues.contentSecondary, - content: { Type: 'PortalRoot' } as any, - keywords: 'explore browse repository', - hits: [term], - }, - { - primaryText: this.localizationValues.searchPrimary, - url: '/:repo/search/', - secondaryText: this.localizationValues.searchSecondaryText, - content: { Type: 'Search' } as any, - keywords: 'search find content query', - hits: [term], - }, - { - primaryText: this.localizationValues.savedQueriesPrimary, - url: '/:repo/saved-queries/', - secondaryText: this.localizationValues.savedQueriesSecondaryText, - content: { Type: 'Search' } as any, - keywords: 'saved query search find', - hits: [term], - }, - { - primaryText: this.localizationValues.eventsPrimary, - url: '/events/', - secondaryText: this.localizationValues.eventsSecondary, - content: { Type: 'EventLog' } as any, - keywords: 'event events error warning log logs', - hits: [term], - }, - ] + public getRoutes: ({ term, device }: SearchOptions) => Array = ({ + term, + device, + }) => { + return [ + ...(device !== 'mobile' + ? [ + { + primaryText: this.localizationValues.personalSettingsPrimary, + url: '/personalSettings', + secondaryText: this.localizationValues.personalSettingsSecondary, + content: { Type: 'Settings' } as any, + keywords: 'settings setup personal settings language theme', + hits: [term], + }, + ] + : []), + ...[ + { + primaryText: this.localizationValues.contentPrimary, + url: '/:repo/browse/', + secondaryText: this.localizationValues.contentSecondary, + content: { Type: 'PortalRoot' } as any, + keywords: 'explore browse repository', + hits: [term], + }, + { + primaryText: this.localizationValues.searchPrimary, + url: '/:repo/search/', + secondaryText: this.localizationValues.searchSecondaryText, + content: { Type: 'Search' } as any, + keywords: 'search find content query', + hits: [term], + }, + { + primaryText: this.localizationValues.savedQueriesPrimary, + url: '/:repo/saved-queries/', + secondaryText: this.localizationValues.savedQueriesSecondaryText, + content: { Type: 'Search' } as any, + keywords: 'saved query search find', + hits: [term], + }, + { + primaryText: this.localizationValues.eventsPrimary, + url: '/events/', + secondaryText: this.localizationValues.eventsSecondary, + content: { Type: 'EventLog' } as any, + keywords: 'event events error warning log logs', + hits: [term], + }, + ], + ] + } + private localizationValues: ReturnType['navigationCommandProvider'] - public shouldExec(term: string) { - const termLowerCase = term.toLocaleLowerCase() + public shouldExec(options: SearchOptions) { + const termLowerCase = options.term.toLocaleLowerCase() return ( - term.length > 0 && - this.getRoutes(term).find( + options.term.length > 0 && + this.getRoutes(options).find( r => r.primaryText.toLocaleLowerCase().includes(termLowerCase) || r.secondaryText.includes(termLowerCase) || @@ -63,15 +74,17 @@ export class NavigationCommandProvider implements CommandProvider { ) } - public async getItems(term: string, repo: Repository): Promise { - return this.getRoutes(term) + public async getItems(options: SearchOptions): Promise { + return this.getRoutes(options) .filter( r => - r.primaryText.includes(term) || r.secondaryText.includes(term) || (r.keywords && r.keywords.includes(term)), + r.primaryText.includes(options.term) || + r.secondaryText.includes(options.term) || + (r.keywords && r.keywords.includes(options.term)), ) .map(r => ({ ...r, - url: r.url.replace('/:repo/', `/${btoa(repo.configuration.repositoryUrl)}/`), + url: r.url.replace('/:repo/', `/${btoa(options.repository.configuration.repositoryUrl)}/`), })) } diff --git a/apps/sensenet/src/services/CommandProviders/QueryCommandProvider.ts b/apps/sensenet/src/services/CommandProviders/QueryCommandProvider.ts index 0645f7e73..1e3754f3f 100644 --- a/apps/sensenet/src/services/CommandProviders/QueryCommandProvider.ts +++ b/apps/sensenet/src/services/CommandProviders/QueryCommandProvider.ts @@ -1,11 +1,12 @@ import { Injectable, Injector } from '@furystack/inject' -import { ConstantContent, Repository } from '@sensenet/client-core' +import { ConstantContent } from '@sensenet/client-core' import { GenericContent } from '@sensenet/default-content-types' -import { CommandPaletteItem } from '../../store/CommandPalette' -import { CommandProvider } from '../CommandProviderManager' +import { CommandProvider, SearchOptions } from '../CommandProviderManager' import { ContentContextProvider } from '../ContentContextProvider' import { LocalizationService } from '../LocalizationService' import { PersonalSettings } from '../PersonalSettings' +import { CommandPaletteItem } from '../../hooks' +import { encodeQueryData } from '../../components/search' @Injectable({ lifetime: 'singleton' }) export class QueryCommandProvider implements CommandProvider { @@ -15,16 +16,16 @@ export class QueryCommandProvider implements CommandProvider { private readonly localization: LocalizationService, ) {} - public shouldExec(searchTerm: string): boolean { - return searchTerm[0] === '+' + public shouldExec(options: SearchOptions): boolean { + return options.term[0] === '+' } - public async getItems(query: string, repo: Repository): Promise { - const ctx = new ContentContextProvider(repo) - const extendedQuery = this.personalSettings.currentValue + public async getItems(options: SearchOptions): Promise { + const ctx = new ContentContextProvider(options.repository) + const extendedQuery = this.personalSettings.effectiveValue .getValue() - .default.commandPalette.wrapQuery.replace('{0}', query) - const result = await repo.loadCollection({ + .default.commandPalette.wrapQuery.replace('{0}', options.term) + const result = await options.repository.loadCollection({ path: ConstantContent.PORTAL_ROOT.Path, oDataOptions: { query: extendedQuery, @@ -39,7 +40,7 @@ export class QueryCommandProvider implements CommandProvider { url: ctx.getPrimaryActionUrl(content), content, icon: content.Icon, - hits: query + hits: options.term .substr(1) .replace(/\*/g, ' ') .replace(/\?/g, ' ') @@ -48,7 +49,9 @@ export class QueryCommandProvider implements CommandProvider { { primaryText: this.localization.currentValues.getValue().search.openInSearchTitle, secondaryText: this.localization.currentValues.getValue().search.openInSearchDescription, - url: `/${btoa(repo.configuration.repositoryUrl)}/search/${encodeURIComponent(query)}`, + url: `/${btoa(options.repository.configuration.repositoryUrl)}/search/${encodeQueryData({ + term: options.term, + })}`, content: { Type: 'Search' } as any, hits: [], }, diff --git a/apps/sensenet/src/services/ContentContextProvider.ts b/apps/sensenet/src/services/ContentContextProvider.ts index eaa9cb77f..3b88498d4 100644 --- a/apps/sensenet/src/services/ContentContextProvider.ts +++ b/apps/sensenet/src/services/ContentContextProvider.ts @@ -1,13 +1,14 @@ import { Repository } from '@sensenet/client-core' import { + ActionModel, ContentType, - File as SnFile, GenericContent, Resource, Settings, - ActionModel, + File as SnFile, } from '@sensenet/default-content-types' import { isContentFromType } from '../utils/isContentFromType' +import { encodeBrowseData } from '../components/content' export type RouteType = | 'Browse' @@ -108,6 +109,9 @@ export class ContentContextProvider { return `/${repoSegment}/wopi/${content.Id}/${routeType === 'WopiEdit' ? 'edit' : 'read'}` } + if (routeType === 'Browse') { + return `/${repoSegment}/${routeType}/${encodeBrowseData({ currentContent: content.Id })}` + } return `/${repoSegment}/${routeType}/${content.Id}` } public getPrimaryActionUrl(content: T) { diff --git a/apps/sensenet/src/services/EventLogger.ts b/apps/sensenet/src/services/EventLogger.ts index ce1b10753..561e206c2 100644 --- a/apps/sensenet/src/services/EventLogger.ts +++ b/apps/sensenet/src/services/EventLogger.ts @@ -1,13 +1,13 @@ import { Injectable } from '@furystack/inject' -import { AbstractLogger, ILeveledLogEntry, LogLevel } from '@furystack/logging' +import { AbstractLogger, LeveledLogEntry, LogLevel } from '@furystack/logging' import { EventService } from './EventService' import { PersonalSettings } from './PersonalSettings' @Injectable() export class EventLogger extends AbstractLogger { - public async addEntry(entry: ILeveledLogEntry): Promise { + public async addEntry(entry: LeveledLogEntry): Promise { if ( - this.personalSettings.currentValue.getValue().logLevel.includes(LogLevel[entry.level] as keyof typeof LogLevel) + this.personalSettings.effectiveValue.getValue().logLevel.includes(LogLevel[entry.level] as keyof typeof LogLevel) ) { this.eventService.add(entry) } diff --git a/apps/sensenet/src/services/EventService.ts b/apps/sensenet/src/services/EventService.ts index 6574705c0..9c9f237a2 100644 --- a/apps/sensenet/src/services/EventService.ts +++ b/apps/sensenet/src/services/EventService.ts @@ -1,10 +1,10 @@ import { Injectable } from '@furystack/inject' -import { ILeveledLogEntry, LogLevel } from '@furystack/logging' +import { LeveledLogEntry, LogLevel } from '@furystack/logging' import { debounce, ObservableValue } from '@sensenet/client-utils' import { v1 } from 'uuid' import { PersonalSettings } from './PersonalSettings' -export type EventLogEntry = ILeveledLogEntry +export type EventLogEntry = LeveledLogEntry @Injectable({ lifetime: 'singleton' }) export class EventService { @@ -35,7 +35,7 @@ export class EventService { private storeChanges = debounce(() => { const values = [...this.values.getValue()] - const entries = values.slice(values.length - this.personalSettings.currentValue.getValue().eventLogSize) + const entries = values.slice(values.length - this.personalSettings.effectiveValue.getValue().eventLogSize) localStorage.setItem(EventService.storageKey, JSON.stringify(entries)) }, EventService.storageDebounceInterval) diff --git a/apps/sensenet/src/services/MonacoModels/PersonalSettingsModel.ts b/apps/sensenet/src/services/MonacoModels/PersonalSettingsModel.ts index 9f92ab96b..8efffb5bb 100644 --- a/apps/sensenet/src/services/MonacoModels/PersonalSettingsModel.ts +++ b/apps/sensenet/src/services/MonacoModels/PersonalSettingsModel.ts @@ -2,7 +2,9 @@ import { LogLevel } from '@furystack/logging' import { Repository } from '@sensenet/client-core' import { editor, languages, Uri } from 'monaco-editor' import defaultLanguage from '../../localization/default' -import { widgetTypes } from '../PersonalSettings' +import { DrawerItemType, widgetTypes } from '../PersonalSettings' +import { BrowseType } from '../../components/content' +import { wellKnownIconNames } from '../../components/Icon' export const setupModel = (language = defaultLanguage, repo: Repository) => { const personalSettingsPath = `sensenet://PersonalSettings/PersonalSettings` @@ -18,32 +20,50 @@ export const setupModel = (language = defaultLanguage, repo: Repository) => { fileMatch: [uriString], schema: { definitions: { + columns: { + type: 'array', + title: language.personalSettings.dashboard.queryWidget.columns, + uniqueItems: true, + examples: [['DisplayName', 'CreatedBy']], + items: { + enum: [ + 'Actions', + 'Type', + ...repo.schemas.getSchemaByName('GenericContent').FieldSettings.map(f => f.Name), + ], + }, + }, dashboardSection: { $id: '#/dashboardSection', type: 'object', - title: 'Query widget', + title: language.personalSettings.dashboard.widgetName, default: null, required: ['widgetType', 'title'], properties: { minWidth: { $id: '#/dashboardSection/properties/widgetType', - type: 'number', - title: 'The minimum width of the widget in pixels', - default: 250, - examples: [250, 500], + type: 'object', + title: language.personalSettings.dashboard.minWidth, + properties: { + default: { type: ['number', 'string'], default: 250 }, + mobile: { type: ['number', 'string'] }, + tablet: { type: ['number', 'string'] }, + desktop: { type: ['number', 'string'] }, + }, + default: { default: 250 }, }, widgetType: { $id: '#/dashboardSection/properties/widgetType', type: 'string', enum: [...widgetTypes], - title: 'Type of the widget', + title: language.personalSettings.dashboard.widgetType, default: 'markdown', - examples: ['query', 'markdown'], + examples: ['query', 'markdown', 'updates'], }, title: { $id: '#/dashboardSection/properties/title', type: 'string', - title: 'Widget title', + title: language.personalSettings.dashboard.title, default: '', pattern: '^(.*)$', }, @@ -57,58 +77,69 @@ export const setupModel = (language = defaultLanguage, repo: Repository) => { settings: { $id: '#/dashboardSection/properties/querySettings', type: 'object', - title: 'Settings for the Query widget', + title: language.personalSettings.dashboard.queryWidget.settings, required: ['query', 'columns'], properties: { query: { $id: '#/dashboardSection/properties/querySettings/properties/term', type: 'string', - title: 'The content query', + title: language.personalSettings.dashboard.queryWidget.query, default: '', examples: ['+alba'], pattern: '^(.*)$', }, + emptyPlaceholderText: { + $id: '#/dashboardSection/properties/querySettings/properties/emptyPlaceholderText', + type: 'string', + title: language.personalSettings.dashboard.queryWidget.emptyPlaceholderText, + default: '', + examples: ['No results.'], + pattern: '^(.*)$', + }, showColumnNames: { $id: '#/dashboardSection/properties/querySettings/properties/showColumnNames', type: 'boolean', - title: 'Show column names', + title: language.personalSettings.dashboard.queryWidget.showColumnNames, default: false, examples: [true], }, top: { - $id: '#/dashboardSection/properties/querySettings/properties/showColumnNames', + $id: '#/dashboardSection/properties/querySettings/properties/top', type: 'number', - title: 'Limits the number of hits', + title: language.personalSettings.dashboard.queryWidget.top, default: 10, examples: [5, 10, 20], }, showOpenInSearch: { $id: '#/dashboardSection/properties/querySettings/properties/showOpenInSearch', type: 'boolean', - title: 'Display a link to the Search view', + title: language.personalSettings.dashboard.queryWidget.showOpenInSearch, default: false, examples: [true], }, showRefresh: { $id: '#/dashboardSection/properties/querySettings/properties/showRefresh', type: 'boolean', - title: 'Display a refresh button', + title: language.personalSettings.dashboard.queryWidget.showRefresh, default: false, examples: [true, false], }, + enableSelection: { + $id: '#/dashboardSection/properties/querySettings/properties/enableSelection', + type: 'boolean', + title: language.personalSettings.dashboard.queryWidget.enableSelection, + default: false, + examples: [true, false], + }, + countOnly: { + $id: '#/dashboardSection/properties/querySettings/properties/countOnly', + type: 'boolean', + title: language.personalSettings.dashboard.queryWidget.countOnly, + default: false, + examples: [true], + }, columns: { - $id: '#/dashboardSection/properties/querySettings/properties/columns', - type: 'array', - title: 'Columns to display', - uniqueItems: true, - items: { - enum: [ - 'Actions', - 'Type', - /** ToDo: check for other displayable system fields */ - ...repo.schemas.getSchemaByName('GenericContent').FieldSettings.map(f => f.Name), - ], - }, + $ref: '#/definitions/columns', }, }, }, @@ -122,13 +153,13 @@ export const setupModel = (language = defaultLanguage, repo: Repository) => { properties: { settings: { type: 'object', - title: 'Settings for the Markdown widget', + title: language.personalSettings.dashboard.markdownWidget.settings, required: ['content'], properties: { content: { $id: '#/dashboardSection/properties/markdownSettings/properties/term', type: 'string', - title: 'The Markdown content', + title: language.personalSettings.dashboard.markdownWidget.content, default: '', examples: [ "### Hey I'm a Paragraph \r\n Hey, I'm not", @@ -154,17 +185,93 @@ export const setupModel = (language = defaultLanguage, repo: Repository) => { items: { description: language.personalSettings.drawerItems, type: 'array', - uniqueItems: true, items: { - enum: [ - 'Content', - 'Content Types', - 'Localization', - 'Search', - 'Setup', - 'Trash', - 'Users and Groups', - 'Version info', + type: 'object', + properties: { + itemType: { + type: 'string', + enum: [...DrawerItemType], + }, + }, + allOf: [ + { + if: { properties: { itemType: { const: 'Content' } } }, + then: { + properties: { + settings: { + type: 'object', + properties: { + root: { type: 'string', description: language.drawer.contentRootDescription }, + title: { type: 'string', description: language.personalSettings.drawerItemTitle }, + columns: { $ref: '#/definitions/columns' }, + description: { + type: 'string', + description: language.personalSettings.drawerItemDescription, + }, + icon: { + type: 'string', + enum: [...wellKnownIconNames], + description: language.personalSettings.drawerItemDescription, + }, + browseType: { + description: language.personalSettings.contentBrowseType, + enum: [...BrowseType], + }, + }, + required: ['root', 'title', 'icon'], + }, + }, + required: ['settings'], + }, + }, + { + if: { properties: { itemType: { const: 'Query' } } }, + then: { + properties: { + settings: { + type: 'object', + properties: { + term: { type: 'string', description: language.drawer.contentRootDescription }, + columns: { $ref: '#/definitions/columns' }, + title: { type: 'string', description: language.personalSettings.drawerItemTitle }, + description: { + type: 'string', + description: language.personalSettings.drawerItemDescription, + }, + icon: { + type: 'string', + enum: [...wellKnownIconNames], + description: language.personalSettings.drawerItemDescription, + }, + }, + required: ['title', 'icon', 'term'], + }, + }, + required: ['settings'], + }, + }, + { + if: { properties: { itemType: { const: 'Dashboard' } } }, + then: { + properties: { + settings: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + dashboardName: { $data: '#/definitions/dashboards' }, + icon: { + type: 'string', + enum: [...wellKnownIconNames], + description: language.personalSettings.drawerItemDescription, + }, + }, + required: ['dashboardName', 'title', 'icon'], + }, + }, + required: ['settings'], + }, + }, ], }, }, @@ -215,21 +322,9 @@ export const setupModel = (language = defaultLanguage, repo: Repository) => { properties: { browseType: { description: language.personalSettings.contentBrowseType, - enum: ['simple', 'commander', 'explorer'], - }, - fields: { - description: language.personalSettings.contentFields, - type: 'array', - uniqueItems: true, - items: { - enum: [ - 'Actions', - 'Type', - /** ToDo: check for other displayable system fields */ - ...repo.schemas.getSchemaByName('GenericContent').FieldSettings.map(f => f.Name), - ], - }, + enum: [...BrowseType], }, + fields: { $ref: '#/definitions/columns' }, }, }, settings: { @@ -242,15 +337,6 @@ export const setupModel = (language = defaultLanguage, repo: Repository) => { commandPalette: { $ref: '#/definitions/commandPalette' }, }, }, - }, - type: 'object', - required: ['default', 'repositories', 'lastRepository'], - properties: { - default: { $ref: '#/definitions/settings' }, - mobile: { $ref: '#/definitions/settings' }, - tablet: { $ref: '#/definitions/settings' }, - desktop: { $ref: '#/definitions/settings' }, - repositories: { $ref: '#/definitions/repositories' }, dashboards: { type: 'object', title: 'The default Dashboard definitions', @@ -266,6 +352,22 @@ export const setupModel = (language = defaultLanguage, repo: Repository) => { items: { $ref: '#definitions/dashboardSection' }, }, }, + additionalProperties: { + type: 'array', + description: 'Custom dashboard with custom name', + items: { $ref: '#definitions/dashboardSection' }, + }, + }, + }, + type: 'object', + properties: { + default: { $ref: '#/definitions/settings' }, + mobile: { $ref: '#/definitions/settings' }, + tablet: { $ref: '#/definitions/settings' }, + desktop: { $ref: '#/definitions/settings' }, + repositories: { $ref: '#/definitions/repositories' }, + dashboards: { + $ref: '#/definitions/dashboards', }, lastRepository: { type: 'string', description: language.personalSettings.lastRepository }, eventLogSize: { type: 'number', description: language.personalSettings.eventLogSize }, diff --git a/apps/sensenet/src/services/PersonalSettings.ts b/apps/sensenet/src/services/PersonalSettings.ts index 0f89126bb..af9f7198e 100644 --- a/apps/sensenet/src/services/PersonalSettings.ts +++ b/apps/sensenet/src/services/PersonalSettings.ts @@ -1,52 +1,116 @@ import { Injectable } from '@furystack/inject' import { LogLevel } from '@furystack/logging' -import { ObservableValue, deepMerge } from '@sensenet/client-utils' +import { deepMerge, ObservableValue } from '@sensenet/client-utils' import { GenericContent } from '@sensenet/default-content-types' import { PlatformDependent } from '../context' import { tuple } from '../utils/tuple' +import { BrowseType } from '../components/content' const settingsKey = `SN-APP-USER-SETTINGS` export interface UiSettings { theme: 'dark' | 'light' content: { - browseType: 'explorer' | 'commander' | 'simple' + browseType: typeof BrowseType[number] fields: Array } commandPalette: { enabled: boolean; wrapQuery: string } drawer: { enabled: boolean type: 'temporary' | 'permanent' | 'mini-variant' - items: string[] + items: Array> } } -export const widgetTypes = tuple('markdown', 'query') - +export const widgetTypes = tuple('markdown', 'query', 'updates') export interface Widget { title: string widgetType: typeof widgetTypes[number] settings: T - minWidth?: number + minWidth?: PlatformDependent } export interface MarkdownWidget extends Widget<{ content: string }> { widgetType: 'markdown' } +export interface UpdatesWidget extends Widget { + widgetType: 'updates' +} + export interface QueryWidget extends Widget<{ columns: Array showColumnNames: boolean showRefresh?: boolean showOpenInSearch?: boolean + enableSelection?: boolean top?: number query: string + emptyPlaceholderText?: string + countOnly?: boolean }> { widgetType: 'query' } -export type WidgetSection = Array> +export type WidgetSection = Array | UpdatesWidget> + +export const DrawerItemType = tuple( + 'Content', + 'Query', + 'Content Types', + 'Query', + 'Localization', + 'Search', + 'Setup', + 'Trash', + 'Version info', + 'Users and groups', + 'Dashboard', +) + +export interface DrawerItem { + /** */ + settings?: T + itemType: typeof DrawerItemType[number] +} + +export interface ContentDrawerItem + extends DrawerItem<{ + root: string + title: string + description?: string + icon: string + columns?: Array + browseType: typeof BrowseType[number] + }> { + itemType: 'Content' +} + +export interface QueryDrawerItem + extends DrawerItem<{ + title: string + description?: string + icon: string + term: string + columns: Array + }> { + itemType: 'Query' +} + +export interface DashboardDrawerItem + extends DrawerItem<{ + dashboardName: string + title: string + description?: string + icon: string + }> { + itemType: 'Dashboard' +} + +export interface BuiltinDrawerItem extends DrawerItem { + itemType: 'Content Types' | 'Localization' | 'Search' | 'Setup' | 'Trash' | 'Version info' | 'Users and groups' +} export type PersonalSettingsType = PlatformDependent & { repositories: Array<{ url: string; loginName?: string; displayName?: string; dashboard?: WidgetSection }> @@ -54,7 +118,7 @@ export type PersonalSettingsType = PlatformDependent & { dashboards: { globalDefault: WidgetSection repositoryDefault: WidgetSection - } + } & { [key: string]: WidgetSection } eventLogSize: number sendLogWithCrashReports: boolean logLevel: Array @@ -65,28 +129,129 @@ export const defaultSettings: PersonalSettingsType = { dashboards: { globalDefault: [ { - title: 'Global Dashboard', + title: 'Welcome back, {currentUserName}', widgetType: 'markdown', settings: { - content: 'This is an example global dashboard.', + content: "It's a great day to do admin stuff!", }, }, ], repositoryDefault: [ { - title: 'Repository Dashboard', + title: 'Welcome back, {currentUserName}', widgetType: 'markdown', settings: { - content: 'This is an example Repository dashboard.', + content: "It's a great day to do admin stuff!", + }, + minWidth: { + default: '100%', + }, + }, + { + title: 'Packages to update', + widgetType: 'updates', + minWidth: { + default: '100%', + }, + settings: undefined, + }, + { + title: 'Number of users', + widgetType: 'query', + minWidth: { default: '30%' }, + settings: { + query: "+(TypeIs:'User' AND InTree:'/Root/IMS/Public')", + columns: [], + countOnly: true, + showColumnNames: false, + showOpenInSearch: false, + showRefresh: false, + }, + }, + { + title: 'Number of content items', + widgetType: 'query', + minWidth: { default: '30%' }, + settings: { + query: "+TypeIs:'GenericContent'", + columns: [], + countOnly: true, + showColumnNames: false, + }, + }, + { + title: 'Updates since yesterday', + widgetType: 'query', + minWidth: { + default: '30%', + }, + settings: { + query: '+ModificationDate:>@@Yesterday@@', + columns: [], + countOnly: true, + showColumnNames: false, + showOpenInSearch: true, + }, + }, + { + title: 'Docs owned by me', + widgetType: 'query', + minWidth: { + default: '100%', + }, + settings: { + query: "+(Owner:@@CurrentUser@@ AND TypeIs:'File')", + columns: [], + countOnly: true, + showColumnNames: false, + showOpenInSearch: true, }, }, { - title: 'Users', + title: 'Docs shared with me', widgetType: 'query', + minWidth: { + default: '100%', + }, settings: { - columns: ['DisplayName'], + query: '+SharedWith:@@CurrentUser@@', + columns: [], + countOnly: true, showColumnNames: false, - query: "TypeIs:'User'", + showOpenInSearch: true, + }, + }, + { + title: 'Tutorials', + widgetType: 'markdown', + settings: { + content: + '[Overview](https://index.hu) \n\n [Getting started](https://index.hu) \n\n [Tutorials](https://index.hu) \n\n [Example apps](https://index.hu) \n\n ', + }, + minWidth: { + default: '45%', + }, + }, + { + title: 'API documentation', + widgetType: 'markdown', + settings: { + content: + ' [Content Delivery API](https://index.hu) \n\n [Images API](https://index.hu) \n\n [Content management API](https://index.hu) \n\n [Content preview API](https://index.hu) \n\n', + }, + minWidth: { + default: '45%', + }, + }, + { + title: 'Have any questions?', + widgetType: 'markdown', + settings: { + content: + "", + }, + minWidth: { + default: '100%', }, }, ], @@ -100,9 +265,17 @@ export const defaultSettings: PersonalSettingsType = { drawer: { enabled: true, type: 'mini-variant', - items: ['Search', 'Content', 'Users and Groups', 'Content Types', 'Localization', 'Setup', 'Version info'], + items: [ + { itemType: 'Search', settings: undefined }, + { itemType: 'Content', settings: { root: '/Root/Content' } }, + { itemType: 'Users and groups', settings: undefined }, + { itemType: 'Content Types', settings: undefined }, + { itemType: 'Localization', settings: undefined }, + { itemType: 'Setup', settings: undefined }, + { itemType: 'Version info', settings: undefined }, + ], }, - commandPalette: { enabled: true, wrapQuery: '${0} .AUTOFILTERS:OFF' }, + commandPalette: { enabled: true, wrapQuery: '{0} .AUTOFILTERS:OFF' }, }, mobile: { drawer: { @@ -129,22 +302,71 @@ export class PersonalSettings { private async init() { const currentUserSettings = await this.getLocalUserSettingsValue() - this.currentValue.setValue(deepMerge(defaultSettings, currentUserSettings)) + this.userValue.setValue(currentUserSettings) + this.effectiveValue.setValue(deepMerge(defaultSettings, currentUserSettings)) + } + + private async checkDrawerItems(settings: Partial): Promise> { + if ( + settings.default && + settings.default.drawer && + settings.default.drawer.items && + settings.default.drawer.items.find(i => typeof i === 'string') + ) { + ;(settings.default.drawer.items as any) = undefined + } + + if ( + settings.desktop && + settings.desktop.drawer && + settings.desktop.drawer.items && + settings.desktop.drawer.items.find(i => typeof i === 'string') + ) { + ;(settings.desktop.drawer.items as any) = undefined + } + + if ( + settings.tablet && + settings.tablet.drawer && + settings.tablet.drawer.items && + settings.tablet.drawer.items.find(i => typeof i === 'string') + ) { + ;(settings.tablet.drawer.items as any) = undefined + } + + if ( + settings.mobile && + settings.mobile.drawer && + settings.mobile.drawer.items && + settings.mobile.drawer.items.find(i => typeof i === 'string') + ) { + ;(settings.mobile.drawer.items as any) = undefined + } + + return settings + } + + private async checkValues(settings: Partial): Promise> { + return await this.checkDrawerItems(settings) } public async getLocalUserSettingsValue(): Promise> { try { - return JSON.parse(localStorage.getItem(`${settingsKey}`) as string) + const stored = JSON.parse((localStorage.getItem(`${settingsKey}`) as string) || '{}') + return await this.checkValues(stored) } catch { /** */ } return {} } - public currentValue = new ObservableValue(defaultSettings) + public effectiveValue = new ObservableValue(defaultSettings) + + public userValue = new ObservableValue>({}) - public async setValue(settings: PersonalSettingsType) { - this.currentValue.setValue({ ...settings }) + public async setPersonalSettingsValue(settings: Partial) { + this.userValue.setValue(settings) + this.effectiveValue.setValue(deepMerge(defaultSettings, settings)) localStorage.setItem(`${settingsKey}`, JSON.stringify(settings)) } } diff --git a/apps/sensenet/src/services/RepositoryManager.ts b/apps/sensenet/src/services/RepositoryManager.ts index 2b60c77fd..b9920a402 100644 --- a/apps/sensenet/src/services/RepositoryManager.ts +++ b/apps/sensenet/src/services/RepositoryManager.ts @@ -40,6 +40,7 @@ export class RepositoryManager { 'PageCount' as any, 'Binary', 'CreationDate', + 'Avatar', ], ...config, repositoryUrl, diff --git a/apps/sensenet/src/services/UploadTracker.ts b/apps/sensenet/src/services/UploadTracker.ts index 237f201fb..e11ec3b03 100644 --- a/apps/sensenet/src/services/UploadTracker.ts +++ b/apps/sensenet/src/services/UploadTracker.ts @@ -1,6 +1,6 @@ import { Injectable } from '@furystack/inject' import { LogLevel } from '@furystack/logging' -import { UploadProgressInfo, Repository } from '@sensenet/client-core' +import { Repository, UploadProgressInfo } from '@sensenet/client-core' import { ObservableValue } from '@sensenet/client-utils' import { EventService } from './EventService' import { LocalizationService } from './LocalizationService' diff --git a/apps/sensenet/src/store/CommandPalette.ts b/apps/sensenet/src/store/CommandPalette.ts deleted file mode 100644 index 4dd7eba28..000000000 --- a/apps/sensenet/src/store/CommandPalette.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Repository } from '@sensenet/client-core' -import { GenericContent } from '@sensenet/default-content-types' -import { createAction, isFromAction } from '@sensenet/redux' -import { Reducer } from 'redux' -import { IInjectableActionCallbackParams } from 'redux-di-middleware' -import { CommandProviderManager } from '../services/CommandProviderManager' -import { rootStateType } from '.' - -export interface CommandPaletteItem { - primaryText: string - secondaryText: string - url: string - hits: string[] - content?: GenericContent - openAction?: () => void -} - -export interface CommandPaletteState { - isOpened: boolean - inputValue: string - items: CommandPaletteItem[] -} - -export const defaultCommandPaletteState: CommandPaletteState = { - isOpened: false, - inputValue: '', - items: [], -} - -export const open = createAction(() => ({ type: 'OPEN_COMMAND_PALETTE' })) -export const close = createAction(() => ({ type: 'CLOSE_COMMAND_PALETTE' })) -export const clearItems = createAction(() => ({ type: 'CLEAR_COMMAND_PALETTE_ITEMS' })) -export const setItems = createAction((items: CommandPaletteItem[]) => ({ type: 'SET_COMMAND_PALETTE_ITEMS', items })) -export const setInputValue = createAction((value: string) => ({ type: 'SET_COMMAND_PALETTE_INPUT_VALUE', value })) - -export const updateItemsFromTerm = createAction((value: string, repo: Repository) => ({ - type: 'UPDATE_ITEMS_FROM_TERM', - inject: async (options: IInjectableActionCallbackParams) => { - const commandProviderManager = options.getInjectable(CommandProviderManager) - const items = await commandProviderManager.getItems(value, repo) - options.dispatch(setItems(items)) - }, -})) - -export const commandPalette: Reducer = (state = defaultCommandPaletteState, action) => { - if (isFromAction(action, open)) { - return { - ...state, - isOpened: true, - } - } else if (isFromAction(action, close)) { - return { - ...state, - isOpened: false, - } - } else if (isFromAction(action, setInputValue)) { - return { - ...state, - inputValue: action.value, - } - } else if (isFromAction(action, clearItems)) { - return { - ...state, - items: [], - } - } else if (isFromAction(action, setItems)) { - return { - ...state, - items: [...action.items], - } - } - - return state -} diff --git a/apps/sensenet/src/store/index.ts b/apps/sensenet/src/store/index.ts index a0a9b2719..9d425ff74 100644 --- a/apps/sensenet/src/store/index.ts +++ b/apps/sensenet/src/store/index.ts @@ -2,12 +2,10 @@ import { commentsStateReducer, sensenetDocumentViewerReducer } from '@sensenet/d import { applyMiddleware, combineReducers, compose, createStore } from 'redux' import { ReduxDiMiddleware } from 'redux-di-middleware' import { snInjector } from '../context' -import { commandPalette } from './CommandPalette' const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose export const rootReducer = combineReducers({ - commandPalette, sensenetDocumentViewer: sensenetDocumentViewerReducer, comments: commentsStateReducer, }) diff --git a/apps/sensenet/src/theme.ts b/apps/sensenet/src/theme.ts index 78ff2b8ab..402d8a5e5 100644 --- a/apps/sensenet/src/theme.ts +++ b/apps/sensenet/src/theme.ts @@ -1,12 +1,21 @@ -import { indigo, teal } from '@material-ui/core/colors' import { ThemeOptions } from '@material-ui/core/styles/createMuiTheme' import zIndex from '@material-ui/core/styles/zIndex' const theme: ThemeOptions = { palette: { type: 'dark', - primary: indigo, - secondary: teal, + secondary: { + light: '#90caf9', + main: '#1976d2', + dark: '#1565c0', + contrastText: '#fff', + }, + primary: { + light: '#80cbc4', + main: '#26a69a', + dark: '#00796b', + contrastText: '#fff', + }, }, overrides: { MuiAppBar: { diff --git a/apps/sensenet/tsconfig.json b/apps/sensenet/tsconfig.json index 1131585ad..58c50e813 100644 --- a/apps/sensenet/tsconfig.json +++ b/apps/sensenet/tsconfig.json @@ -12,11 +12,14 @@ }, "include": ["src", "../../typings"], "references": [ + { "path": "../../packages/sn-authentication-jwt" }, + { "path": "../../packages/sn-client-auth-google" }, { "path": "../../packages/sn-client-core" }, { "path": "../../packages/sn-client-utils" }, { "path": "../../packages/sn-controls-react" }, { "path": "../../packages/sn-default-content-types" }, { "path": "../../packages/sn-document-viewer-react" }, + { "path": "../../packages/sn-icons-react" }, { "path": "../../packages/sn-list-controls-react" }, { "path": "../../packages/sn-query" }, { "path": "../../packages/sn-redux" }, diff --git a/apps/sensenet/webpack.config.js b/apps/sensenet/webpack.config.js index 131062b85..a576a57fa 100644 --- a/apps/sensenet/webpack.config.js +++ b/apps/sensenet/webpack.config.js @@ -45,6 +45,7 @@ module.exports = { new TsConfigWebpackPlugin(), new HtmlWebpackPlugin({ template: './index.html', + favicon: './src/assets/favicon.ico', }), new MonacoWebpackPlugin({ languages: ['json', 'javascript', 'xml', 'html'], diff --git a/examples/sn-dms-demo/cypress/integration/typings.d.ts b/examples/sn-dms-demo/cypress/integration/typings.d.ts index 14275b7f7..dc69a4d44 100644 --- a/examples/sn-dms-demo/cypress/integration/typings.d.ts +++ b/examples/sn-dms-demo/cypress/integration/typings.d.ts @@ -6,6 +6,7 @@ declare global { interface Window { repository: Repository } + // eslint-disable-next-line no-redeclare namespace Cypress { interface Chainable { login: (email: string, password: string) => Cypress.Chainable diff --git a/examples/sn-dms-demo/package.json b/examples/sn-dms-demo/package.json index d888752e2..af1a0155d 100644 --- a/examples/sn-dms-demo/package.json +++ b/examples/sn-dms-demo/package.json @@ -1,6 +1,6 @@ { "name": "sn-dms-demo", - "version": "1.5.0", + "version": "1.5.2", "description": "Document Management Demo upon sensenet", "private": true, "repository": { @@ -28,26 +28,26 @@ "fix:prettier": "prettier \"{,!(dist|temp|bundle)/**/}*.{ts,tsx}\" --write" }, "dependencies": { - "@material-ui/core": "^4.0.1", + "@material-ui/core": "^4.3.1", "@material-ui/icons": "^4.0.1", - "@sensenet/authentication-google": "^2.0.9", - "@sensenet/authentication-jwt": "^1.0.13", - "@sensenet/client-core": "^2.1.0", - "@sensenet/client-utils": "^1.6.2", - "@sensenet/controls-react": "^2.8.0", - "@sensenet/default-content-types": "^1.2.2", - "@sensenet/document-viewer-react": "^1.1.2", - "@sensenet/icons-react": "^1.2.11", - "@sensenet/list-controls-react": "^1.3.8", - "@sensenet/pickers-react": "^1.2.1", - "@sensenet/redux": "^5.1.10", - "@sensenet/repository-events": "^1.4.2", - "@sensenet/search-react": "^1.2.6", - "autoprefixer": "^9.5.1", - "css-loader": "^2.1.0", + "@sensenet/authentication-google": "^2.0.11", + "@sensenet/authentication-jwt": "^1.0.15", + "@sensenet/client-core": "^2.2.0", + "@sensenet/client-utils": "^1.6.4", + "@sensenet/controls-react": "^3.1.0", + "@sensenet/default-content-types": "^2.0.0", + "@sensenet/document-viewer-react": "^1.2.0", + "@sensenet/icons-react": "^1.2.12", + "@sensenet/list-controls-react": "^1.3.10", + "@sensenet/pickers-react": "^1.2.3", + "@sensenet/redux": "^5.1.12", + "@sensenet/repository-events": "^1.4.4", + "@sensenet/search-react": "^1.2.8", + "autoprefixer": "^9.6.1", + "css-loader": "^3.1.0", "dotenv": "^8.0.0", - "file-loader": "3.0.1", - "fs-extra": "^8.0.1", + "file-loader": "4.1.0", + "fs-extra": "^8.1.0", "history": "^4.7.2", "html-webpack-plugin": "^3.2.0", "lodash.groupby": "^4.6.0", @@ -57,59 +57,59 @@ "postcss-loader": "^3.0.0", "promise": "^8.0.3", "react": "^16.8.4", - "react-async-script": "^1.0.1", - "react-cookie-consent": "^2.3.1", + "react-async-script": "^1.1.1", + "react-cookie-consent": "^2.3.2", "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.8.4", - "react-google-recaptcha": "^1.0.5", + "react-google-recaptcha": "^1.1.0", "react-loadable": "^5.5.0", "react-markdown": "^4.0.8", "react-moment": "^0.9.2", "react-redux": "^7.0.3", - "react-responsive": "^6.1.2", + "react-responsive": "^7.0.0", "react-router": "^5.0.0", "react-router-dom": "^5.0.0", - "redux": "^4.0.1", + "redux": "^4.0.4", "redux-di-middleware": "^4.0.1", "reflect-metadata": "^0.1.12", "semaphore-async-await": "^1.5.1", "ts-keycode-enum": "^1.0.6", - "typeface-roboto": "^0.0.54", + "typeface-roboto": "^0.0.75", "uuid": "^3.3.2" }, "devDependencies": { "@cypress/webpack-preprocessor": "^4.0.3", - "@types/chance": "^1.0.4", - "@types/jest": "^24.0.9", + "@types/chance": "^1.0.5", + "@types/jest": "^24.0.16", "@types/lodash.groupby": "^4.6.6", - "@types/node": "^12.0.3", - "@types/react": "^16.8.19", + "@types/node": "^12.6.8", + "@types/react": "^16.8.23", "@types/react-custom-scrollbars": "^4.0.5", - "@types/react-dom": "^16.8.4", - "@types/react-google-recaptcha": "^1.0.0", + "@types/react-dom": "^16.8.5", + "@types/react-google-recaptcha": "^1.1.0", "@types/react-loadable": "^5.4.2", - "@types/react-redux": "^7.0.9", + "@types/react-redux": "^7.1.1", "@types/react-responsive": "^3.0.2", "@types/react-router-dom": "^4.3.3", "@types/redux-mock-store": "^1.0.1", - "@types/uuid": "^3.4.4", + "@types/uuid": "^3.4.5", "awesome-typescript-loader": "^5.2.1", "chance": "^1.0.18", - "copy-webpack-plugin": "^5.0.3", + "copy-webpack-plugin": "^5.0.4", "cross-env": "^5.2.0", - "cypress": "^3.3.1", + "cypress": "^3.4.1", "path-to-regexp": "3.0.0", - "raw-loader": "^2.0.0", + "raw-loader": "^3.1.0", "redux-mock-store": "^1.5.3", "source-map-loader": "^0.2.4", "style-loader": "^0.23.1", - "svg-url-loader": "^2.3.2", + "svg-url-loader": "^3.0.0", "uglify-es": "^3.3.9", "uglifyjs-webpack-plugin": "^2.1.3", - "url-loader": "^1.1.2", - "webpack": "^4.32.2", - "webpack-bundle-analyzer": "^3.3.2", - "webpack-cli": "^3.3.2", + "url-loader": "^2.1.0", + "webpack": "^4.38.0", + "webpack-bundle-analyzer": "^3.4.1", + "webpack-cli": "^3.3.6", "webpack-dev-server": "^3.4.1" } } diff --git a/examples/sn-dms-demo/src/Actions.ts b/examples/sn-dms-demo/src/Actions.ts index bce47ab58..1015adb9c 100644 --- a/examples/sn-dms-demo/src/Actions.ts +++ b/examples/sn-dms-demo/src/Actions.ts @@ -5,7 +5,7 @@ import { UploadProgressInfo, } from '@sensenet/client-core' import { ObservableValue, usingAsync } from '@sensenet/client-utils' -import { File as SnFile, GenericContent } from '@sensenet/default-content-types' +import { GenericContent, File as SnFile } from '@sensenet/default-content-types' import { ActionModel } from '@sensenet/default-content-types/dist/ActionModel' import { Dispatch } from 'redux' import { IInjectableActionCallbackParams } from 'redux-di-middleware' diff --git a/examples/sn-dms-demo/src/__tests__/TestHelper.tsx b/examples/sn-dms-demo/src/__tests__/TestHelper.tsx index 4181a3bb7..bbd5c1b58 100644 --- a/examples/sn-dms-demo/src/__tests__/TestHelper.tsx +++ b/examples/sn-dms-demo/src/__tests__/TestHelper.tsx @@ -1,6 +1,6 @@ import { Repository } from '@sensenet/client-core' import { commentsStateReducer, rootReducer as sensenetDocumentViewerReducer } from '@sensenet/document-viewer-react' -import { Store, Reducers } from '@sensenet/redux' +import { Reducers, Store } from '@sensenet/redux' import { CreateStoreOptions } from '@sensenet/redux/dist/Store' import React from 'react' diff --git a/examples/sn-dms-demo/src/components/ActionMenu/ActionMenu.tsx b/examples/sn-dms-demo/src/components/ActionMenu/ActionMenu.tsx index c4e712bc7..2325405b6 100644 --- a/examples/sn-dms-demo/src/components/ActionMenu/ActionMenu.tsx +++ b/examples/sn-dms-demo/src/components/ActionMenu/ActionMenu.tsx @@ -506,10 +506,10 @@ class ActionMenu extends React.Component< type="file" onChange={ev => this.handleUpload(ev)} style={{ display: 'none' }} - {...{ + {...({ directory: '', webkitdirectory: '', - } as any} + } as any)} /> ) diff --git a/examples/sn-dms-demo/src/components/DashboardDrawer.tsx b/examples/sn-dms-demo/src/components/DashboardDrawer.tsx index c9eb34305..23e20beca 100644 --- a/examples/sn-dms-demo/src/components/DashboardDrawer.tsx +++ b/examples/sn-dms-demo/src/components/DashboardDrawer.tsx @@ -4,7 +4,7 @@ import List from '@material-ui/core/List' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles' +import withStyles from '@material-ui/core/styles/withStyles' import { ActionModel } from '@sensenet/default-content-types' import { Icon, iconType } from '@sensenet/icons-react' import { Actions } from '@sensenet/redux' @@ -92,9 +92,9 @@ const menu: Array<{ const drawerWidth = 185 -const styles: StyleRulesCallback = () => ({ +const styles = () => ({ drawerPaper: { - position: 'relative', + position: 'relative' as 'relative', width: drawerWidth, padding: '0 10px', }, diff --git a/examples/sn-dms-demo/src/components/Dialogs/AddNewDialog.tsx b/examples/sn-dms-demo/src/components/Dialogs/AddNewDialog.tsx index 2a455ea68..e678e7e2c 100644 --- a/examples/sn-dms-demo/src/components/Dialogs/AddNewDialog.tsx +++ b/examples/sn-dms-demo/src/components/Dialogs/AddNewDialog.tsx @@ -73,13 +73,12 @@ class AddNewDialog extends React.Component< {repository => ( this.handleCancel()} onSubmit={createContent} - title={title} + showTitle={!!title} extension={extension} submitCallback={this.submitCallback} /> diff --git a/examples/sn-dms-demo/src/components/Dialogs/DialogInfo.tsx b/examples/sn-dms-demo/src/components/Dialogs/DialogInfo.tsx index 75f97ab23..2f3a80d69 100644 --- a/examples/sn-dms-demo/src/components/Dialogs/DialogInfo.tsx +++ b/examples/sn-dms-demo/src/components/Dialogs/DialogInfo.tsx @@ -124,7 +124,11 @@ class DialogInfo extends React.Component<{ classes: any } & DialogInfoProps, {}> - {currentContent ? resources.VERSIONING[currentContent.VersioningMode || ''] : ''} + {currentContent + ? resources.VERSIONING[ + (currentContent.VersioningMode && currentContent.VersioningMode[0]) || '' + ] + : ''} {currentContent ? currentContent.Path : ''} diff --git a/examples/sn-dms-demo/src/components/Menu/ContentTemplatesMenu.tsx b/examples/sn-dms-demo/src/components/Menu/ContentTemplatesMenu.tsx index 64757b7e7..71540ab13 100644 --- a/examples/sn-dms-demo/src/components/Menu/ContentTemplatesMenu.tsx +++ b/examples/sn-dms-demo/src/components/Menu/ContentTemplatesMenu.tsx @@ -1,7 +1,7 @@ import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles' +import withStyles from '@material-ui/core/styles/withStyles' import { Icon, iconType } from '@sensenet/icons-react' import React from 'react' @@ -29,7 +29,7 @@ export const subMenu = [ }, ] -const styles: StyleRulesCallback = () => ({ +const styles = () => ({ primary: { color: '#666', fontFamily: 'Raleway Semibold', diff --git a/examples/sn-dms-demo/src/components/Menu/ContentTypesMenu.tsx b/examples/sn-dms-demo/src/components/Menu/ContentTypesMenu.tsx index da64708d8..3864c6555 100644 --- a/examples/sn-dms-demo/src/components/Menu/ContentTypesMenu.tsx +++ b/examples/sn-dms-demo/src/components/Menu/ContentTypesMenu.tsx @@ -3,7 +3,7 @@ import List from '@material-ui/core/List' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles' +import withStyles from '@material-ui/core/styles/withStyles' import { Icon, iconType } from '@sensenet/icons-react' import React from 'react' import { connect } from 'react-redux' @@ -31,7 +31,7 @@ const subMenu = [ }, ] -const styles: StyleRulesCallback = () => ({ +const styles = () => ({ primary: { color: '#666', fontFamily: 'Raleway Semibold', diff --git a/examples/sn-dms-demo/src/components/Menu/DocumentsMenu.tsx b/examples/sn-dms-demo/src/components/Menu/DocumentsMenu.tsx index 437b96906..61a770cf0 100644 --- a/examples/sn-dms-demo/src/components/Menu/DocumentsMenu.tsx +++ b/examples/sn-dms-demo/src/components/Menu/DocumentsMenu.tsx @@ -3,20 +3,20 @@ import List from '@material-ui/core/List' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles' +import withStyles from '@material-ui/core/styles/withStyles' import { Content, UploadProgressInfo } from '@sensenet/client-core' import { Icon, iconType } from '@sensenet/icons-react' import React from 'react' import { connect } from 'react-redux' import MediaQuery from 'react-responsive' import { RouteComponentProps, withRouter } from 'react-router-dom' -import { removeUploadItem, uploadFileList, handleDrawerMenu } from '../../Actions' +import { handleDrawerMenu, removeUploadItem, uploadFileList } from '../../Actions' import { updateChildrenOptions } from '../../store/documentlibrary/actions' import { rootStateType } from '../../store/rootReducer' import AddNewMenu from '../ActionMenu/AddNewMenu' import { UploadButton } from '../Upload/UploadButton' -const styles: StyleRulesCallback = () => ({ +const styles = () => ({ primary: { color: '#666', fontFamily: 'Raleway Semibold', diff --git a/examples/sn-dms-demo/src/components/Menu/GroupsMenu.tsx b/examples/sn-dms-demo/src/components/Menu/GroupsMenu.tsx index 66a58f620..30ccc860a 100644 --- a/examples/sn-dms-demo/src/components/Menu/GroupsMenu.tsx +++ b/examples/sn-dms-demo/src/components/Menu/GroupsMenu.tsx @@ -2,7 +2,7 @@ import Divider from '@material-ui/core/Divider' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles' +import withStyles from '@material-ui/core/styles/withStyles' import { Icon, iconType } from '@sensenet/icons-react' import React, { Component } from 'react' import { connect } from 'react-redux' @@ -13,7 +13,7 @@ import { rootStateType } from '../../store/rootReducer' import AddNewDialog from '../Dialogs/AddNewDialog' import { AddNewButton } from './AddNewButton' -const styles: StyleRulesCallback = () => ({ +const styles = () => ({ primary: { color: '#666', fontFamily: 'Raleway Semibold', diff --git a/examples/sn-dms-demo/src/components/Menu/SettingsMenu.tsx b/examples/sn-dms-demo/src/components/Menu/SettingsMenu.tsx index 17afa9fda..1a9a7e2dd 100644 --- a/examples/sn-dms-demo/src/components/Menu/SettingsMenu.tsx +++ b/examples/sn-dms-demo/src/components/Menu/SettingsMenu.tsx @@ -2,7 +2,7 @@ import List from '@material-ui/core/List' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles' +import withStyles from '@material-ui/core/styles/withStyles' import { Icon, iconType } from '@sensenet/icons-react' import React from 'react' import { connect } from 'react-redux' @@ -29,7 +29,7 @@ const subMenu = [ }, ] -const styles: StyleRulesCallback = () => ({ +const styles = () => ({ primary: { color: '#666', fontFamily: 'Raleway Semibold', diff --git a/examples/sn-dms-demo/src/components/Menu/UsersMenu.tsx b/examples/sn-dms-demo/src/components/Menu/UsersMenu.tsx index c22557b28..594b669cc 100644 --- a/examples/sn-dms-demo/src/components/Menu/UsersMenu.tsx +++ b/examples/sn-dms-demo/src/components/Menu/UsersMenu.tsx @@ -2,7 +2,7 @@ import Divider from '@material-ui/core/Divider' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles' +import withStyles from '@material-ui/core/styles/withStyles' import { Icon, iconType } from '@sensenet/icons-react' import React, { Component } from 'react' import { connect } from 'react-redux' @@ -13,7 +13,7 @@ import { rootStateType } from '../../store/rootReducer' import AddNewDialog from '../Dialogs/AddNewDialog' import { AddNewButton } from './AddNewButton' -const styles: StyleRulesCallback = () => ({ +const styles = () => ({ primary: { color: '#666', fontFamily: 'Raleway Semibold', diff --git a/examples/sn-dms-demo/src/components/Pickers/PathPicker.tsx b/examples/sn-dms-demo/src/components/Pickers/PathPicker.tsx index c1c330025..d60af0511 100644 --- a/examples/sn-dms-demo/src/components/Pickers/PathPicker.tsx +++ b/examples/sn-dms-demo/src/components/Pickers/PathPicker.tsx @@ -9,7 +9,7 @@ import { GenericContentWithIsParent, useListPicker } from '@sensenet/pickers-rea import React, { useEffect } from 'react' import { connect } from 'react-redux' import { dmsInjector } from '../../DmsRepository' -import { deselectPickeritem, selectPickerItem, reloadPickerItems } from '../../store/picker/actions' +import { deselectPickeritem, reloadPickerItems, selectPickerItem } from '../../store/picker/actions' import { rootStateType } from '../../store/rootReducer' interface PathPickerProps { diff --git a/examples/sn-dms-demo/src/components/SavedQueries.tsx b/examples/sn-dms-demo/src/components/SavedQueries.tsx index d4195ce3b..45af83195 100644 --- a/examples/sn-dms-demo/src/components/SavedQueries.tsx +++ b/examples/sn-dms-demo/src/components/SavedQueries.tsx @@ -55,7 +55,7 @@ class SavedQueries extends React.Component<
Saved Queries - + this.handleUpload(ev)} - {...{ + {...({ directory: '', webkitdirectory: '', - } as any} + } as any)} />
) : null} diff --git a/examples/sn-dms-demo/src/components/UsersAndGroups/Group/GroupList.tsx b/examples/sn-dms-demo/src/components/UsersAndGroups/Group/GroupList.tsx index 90c9742e2..8718a9dad 100644 --- a/examples/sn-dms-demo/src/components/UsersAndGroups/Group/GroupList.tsx +++ b/examples/sn-dms-demo/src/components/UsersAndGroups/Group/GroupList.tsx @@ -10,7 +10,7 @@ import { compile } from 'path-to-regexp' import React, { Component } from 'react' import { connect } from 'react-redux' import { RouteComponentProps, withRouter } from 'react-router' -import { closeActionMenu, openActionMenu, openDialog, closeDialog } from '../../../Actions' +import { closeActionMenu, closeDialog, openActionMenu, openDialog } from '../../../Actions' import { icons } from '../../../assets/icons' import { resources } from '../../../assets/resources' import { customSchema } from '../../../assets/schema' diff --git a/examples/sn-dms-demo/src/components/UsersAndGroups/User/MembersList.tsx b/examples/sn-dms-demo/src/components/UsersAndGroups/User/MembersList.tsx index 3a6edaebf..7773b8515 100644 --- a/examples/sn-dms-demo/src/components/UsersAndGroups/User/MembersList.tsx +++ b/examples/sn-dms-demo/src/components/UsersAndGroups/User/MembersList.tsx @@ -8,7 +8,7 @@ import { ContentList } from '@sensenet/list-controls-react' import React, { Component } from 'react' import { connect } from 'react-redux' import { RouteComponentProps, withRouter } from 'react-router' -import { closeActionMenu, openActionMenu, openDialog, closeDialog } from '../../../Actions' +import { closeActionMenu, closeDialog, openActionMenu, openDialog } from '../../../Actions' import { icons } from '../../../assets/icons' import { resources } from '../../../assets/resources' import { customSchema } from '../../../assets/schema' diff --git a/examples/sn-dms-demo/src/components/UsersAndGroups/User/UserList.tsx b/examples/sn-dms-demo/src/components/UsersAndGroups/User/UserList.tsx index 20e64f9cd..e0200c9a1 100644 --- a/examples/sn-dms-demo/src/components/UsersAndGroups/User/UserList.tsx +++ b/examples/sn-dms-demo/src/components/UsersAndGroups/User/UserList.tsx @@ -9,7 +9,7 @@ import { compile } from 'path-to-regexp' import React, { Component } from 'react' import { connect } from 'react-redux' import { RouteComponentProps, withRouter } from 'react-router' -import { closeActionMenu, openActionMenu, openDialog, closeDialog } from '../../../Actions' +import { closeActionMenu, closeDialog, openActionMenu, openDialog } from '../../../Actions' import { icons } from '../../../assets/icons' import { resources } from '../../../assets/resources' import { customSchema } from '../../../assets/schema' diff --git a/examples/sn-dms-demo/src/components/__tests__/UserPanel.test.tsx b/examples/sn-dms-demo/src/components/__tests__/UserPanel.test.tsx index fa1f5428d..e80e8455b 100644 --- a/examples/sn-dms-demo/src/components/__tests__/UserPanel.test.tsx +++ b/examples/sn-dms-demo/src/components/__tests__/UserPanel.test.tsx @@ -12,7 +12,7 @@ it('renders without crashing', () => { session: { repository: { repositoryUrl: 'https://dmsservice.demo.sensenet.com', - }, + } as any, user: { fullName: 'Alba Monday', userAvatarPath: '/Root/Sites/Default_Site/demoavatars/alba.jpg', diff --git a/examples/sn-dms-demo/webpack.config.js b/examples/sn-dms-demo/webpack.config.js index 05d3670f8..7e84af18d 100644 --- a/examples/sn-dms-demo/webpack.config.js +++ b/examples/sn-dms-demo/webpack.config.js @@ -103,12 +103,12 @@ module.exports = { plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ - browsers: [ - '>1%', - 'last 4 versions', - 'Firefox ESR', - 'not ie < 9', // React doesn't support IE8 anyway - ], + // browsers: [ + // '>1%', + // 'last 4 versions', + // 'Firefox ESR', + // 'not ie < 9', // React doesn't support IE8 anyway + // ], flexbox: 'no-2009', }), ], diff --git a/examples/sn-react-component-docs/README.md b/examples/sn-react-component-docs/README.md index 9d45e9e60..473eb149c 100644 --- a/examples/sn-react-component-docs/README.md +++ b/examples/sn-react-component-docs/README.md @@ -1,2 +1,5 @@ # sn-react-component-docs -UI components for building apps upon sensenet implemented in React + +> UI components for building apps upon sensenet implemented in React + +See [https://sn-react-component-docs-dev.netlify.com](https://sn-react-component-docs-dev.netlify.com) diff --git a/examples/sn-react-component-docs/package.json b/examples/sn-react-component-docs/package.json index 82e3e5565..1dae80aa1 100644 --- a/examples/sn-react-component-docs/package.json +++ b/examples/sn-react-component-docs/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "sn-react-component-docs", - "version": "2.3.0", + "version": "2.4.1", "description": "UI components for building apps upon sensenet implemented in React", "main": "index.js", "scripts": { @@ -28,23 +28,23 @@ }, "homepage": "https://sensenet.com", "dependencies": { - "@sensenet/client-core": "^2.1.0", - "@sensenet/client-utils": "^1.6.2", - "@sensenet/controls-react": "^2.8.0", - "@sensenet/default-content-types": "^1.2.2", - "@sensenet/document-viewer-react": "^1.1.2", - "@sensenet/icons-react": "^1.2.11", - "@sensenet/list-controls-react": "^1.3.8", - "@sensenet/pickers-react": "^1.2.1", - "@sensenet/query": "^1.1.7", - "@sensenet/search-react": "^1.2.6", + "@sensenet/client-core": "^2.2.0", + "@sensenet/client-utils": "^1.6.4", + "@sensenet/controls-react": "^3.1.0", + "@sensenet/default-content-types": "^2.0.0", + "@sensenet/document-viewer-react": "^1.2.0", + "@sensenet/icons-react": "^1.2.12", + "@sensenet/list-controls-react": "^1.3.10", + "@sensenet/pickers-react": "^1.2.3", + "@sensenet/query": "^1.1.8", + "@sensenet/search-react": "^1.2.8", "react": "^16.8.4", "react-dom": "^16.8.4", "react-redux": "^7.0.3", - "redux": "^4.0.1" + "redux": "^4.0.4" }, "devDependencies": { - "@babel/core": "^7.4.5", + "@babel/core": "^7.5.5", "@storybook/addon-a11y": "^5.0.11", "@storybook/addon-actions": "^5.0.11", "@storybook/addon-events": "^5.0.11", @@ -57,21 +57,21 @@ "@storybook/addon-viewport": "^5.0.11", "@storybook/addons": "^5.0.11", "@storybook/react": "^5.0.11", - "@types/node": "^12.0.3", - "@types/react": "^16.8.19", - "@types/react-redux": "^7.0.9", + "@types/node": "^12.6.8", + "@types/react": "^16.8.23", + "@types/react-redux": "^7.1.1", "@types/storybook__addon-a11y": "^5.0.0", "@types/storybook__addon-actions": "^3.4.2", "@types/storybook__addon-info": "^4.1.0", "@types/storybook__addon-options": "^4.0.1", "@types/storybook__addon-viewport": "^4.1.0", "@types/storybook__react": "^4.0.1", - "@types/uuid": "^3.4.4", + "@types/uuid": "^3.4.5", "babel-loader": "^8.0.6", "babel-preset-react-app": "^9.0.0", "react-docgen-typescript-loader": "^3.0.1", - "ts-config-webpack-plugin": "^1.3.1", + "ts-config-webpack-plugin": "^1.4.0", "uuid": "^3.3.2", - "webpack": "^4.32.2" + "webpack": "^4.38.0" } } diff --git a/examples/sn-react-component-docs/stories/FieldControl.stories.tsx b/examples/sn-react-component-docs/stories/FieldControl.stories.tsx index 3be7d7505..f171e8bf2 100644 --- a/examples/sn-react-component-docs/stories/FieldControl.stories.tsx +++ b/examples/sn-react-component-docs/stories/FieldControl.stories.tsx @@ -1,26 +1,22 @@ -import { withA11y } from '@storybook/addon-a11y' -import { action } from '@storybook/addon-actions' -import { withActions } from '@storybook/addon-actions/dist/preview' -import { boolean, date, number, select, text, withKnobs } from '@storybook/addon-knobs' -import { storiesOf } from '@storybook/react' +/* eslint-disable react/display-name */ import React from 'react' import { Repository } from '@sensenet/client-core' import { AllowedChildTypes, AutoComplete, Avatar, + BooleanComponent, CheckboxGroup, ColorPicker, DatePicker, - DateTimePicker, - DisplayName, DropDownList, FileName, FileUpload, Name, - Number, + NumberComponent, Password, RadioButtonGroup, + reactControlMapper, ReferenceGrid, RichTextEditor, ShortText, @@ -28,7 +24,7 @@ import { Textarea, TimePicker, } from '@sensenet/controls-react/src' -import { GenericContent, User } from '@sensenet/default-content-types/src' +import { GenericContent, Group, Image, Task, User, VersioningMode } from '@sensenet/default-content-types/src' import shorttextNotes from '../notes/fieldcontrols/ShortText.md' import displaynameNotes from '../notes/fieldcontrols/DisplayName.md' import checkboxgroupNotes from '../notes/fieldcontrols/CheckboxGroup.md' @@ -48,1530 +44,456 @@ import autocompleteNotes from '../notes/fieldcontrols/AutoComplete.md' import fileUploadNotes from '../notes/fieldcontrols/FileUpload.md' import referenceGridNotes from '../notes/fieldcontrols/ReferenceGrid.md' import avatarNotes from '../notes/fieldcontrols/Avatar.md' -import approvingModeChoiceNotes from '../notes/fieldcontrols/ApprovingModeChoice.md' -import versioningModeChoiceNotes from '../notes/fieldcontrols/VersioningModeChoice.md' -import versioningModeNotes from '../notes/fieldcontrols/VersioningMode.md' +// import approvingModeChoiceNotes from '../notes/fieldcontrols/ApprovingModeChoice.md' +// import versioningModeChoiceNotes from '../notes/fieldcontrols/VersioningModeChoice.md' +// import versioningModeNotes from '../notes/fieldcontrols/VersioningMode.md' import colorPickerNotes from '../notes/fieldcontrols/ColorPicker.md' import allowedTypeNotes from '../notes/fieldcontrols/AllowedChildTypes.md' -import { customSchema } from './ViewControl.stories' - -/** - * Date knob - */ -function dateKnob(name: string, defaultValue = new Date()) { - const stringTimestamp = date(name, defaultValue) - return new Date(stringTimestamp).toISOString() -} +import { customSchema } from './custom-schema' +import { DynamicControl } from './dynamic-control' +import { fieldControlStory } from './field-control-story' +import { PleaseLogin } from './PleaseLogin' export const testRepository = new Repository({ - repositoryUrl: 'https://dmsservice.demo.sensenet.com', + repositoryUrl: 'https://devservice.demo.sensenet.com', requiredSelect: ['Id', 'Path', 'Name', 'Type', 'ParentId', 'DisplayName'] as any, schemas: customSchema, sessionLifetime: 'expiration', }) -const currencyOptions = { - USD: '$', - EUR: '€', - BTC: '฿', - JPY: '¥', +const taskContent: Task = { + Id: 2344, + Name: 'Task', + Path: '/Root/Sites/Default_Site', + Type: 'Task', + TaskCompletion: 20, } -const tagsInputDataSource: User[] = [ - { Path: 'Root/Users/Alba', Name: 'Alba Monday', DisplayName: 'Alba Monday', Id: 1, Type: 'User' }, - { Path: 'Root/Users/Terry', Name: 'Terry Cherry', DisplayName: 'Terry Cherry', Id: 2, Type: 'User' }, -] - -const referenceGridDataSource = [ - { DisplayName: 'Aenean semper.doc', Id: 4083, IsFolder: false, Children: [], Type: 'File' }, - { DisplayName: 'Aliquam porta suscipit ante.doc', Id: 4082, IsFolder: false, Children: [], Type: 'File' }, - { DisplayName: 'Duis et lorem.doc', Id: 4085, IsFolder: false, Children: [], Type: 'File' }, -] +const fileContent: Image = { + Id: 3777, + Path: '/Root/Sites/Default_Site/infos/images/approving_enabled.png', + Name: 'approving_enabled.png', + DisplayName: 'approving_enabled.png', + Type: 'Image', + Icon: 'image', + RateAvg: 32.5, +} -const testContent: GenericContent = { +const testContent: GenericContent & { ExpectedRevenue: number; Color: string; Password: string } = { Name: 'Document_Library', + DisplayName: 'Document Library', + Description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc eu mi arcu. + Praesent vel ante vel nulla ornare bibendum et nec libero. + Proin ornare imperdiet ex luctus cursus. Cras turpis quam, faucibus et ante sed, egestas mollis nisi. + Maecenas sit amet tempus justo. Etiam id metus diam. + Curabitur semper facilisis odio, eu vehicula nibh auctor a. + Donec eleifend aliquam massa, vel dictum erat suscipit quis.`, Id: 4808, Path: '/Root/Sites/Default_Site', Type: 'GenericContent', + VersioningMode: [VersioningMode.Option1], + ModificationDate: new Date().toISOString(), + Index: 42, + ExpectedRevenue: 21.0, + Color: '#016d9e', + Password: 'password', +} + +const userContent: User = { + Name: 'Alba Monday', + Path: 'Root/IMS/Public/alba', + DisplayName: 'Alba Monday', + Id: 4804, + Type: 'User', + BirthDate: new Date(2000, 5, 15).toISOString(), + Avatar: { Url: '/Root/Sites/Default_Site/demoavatars/alba.jpg' }, + Enabled: true, + Manager: { + Name: 'Business Cat', + Path: 'Root/IMS/Public/businesscat', + DisplayName: 'Business Cat', + Id: 4810, + Type: 'User', + }, +} + +const groupContent: Group = { + Id: 4815, + Type: 'Group', + Name: 'DMSAdmins', + Path: '/Root/IMS/Public/DMSAdmins', + Members: [userContent], } -storiesOf('FieldControls.AllowedChildTypes', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( -
-
- To see this control in action, please login at - - https://dmsservice.demo.sensenet.com/ - -
-
- ( +
+ + -
- ), - { notes: { markdown: allowedTypeNotes } }, - ) - .add( - 'edit mode', - () => ( -
-
- To see this control in action, please login at - - https://dmsservice.demo.sensenet.com/ - -
-
- +
+ ), + markdown: allowedTypeNotes, + storyName: 'FieldControls.AllowedChildTypes', +}) + +fieldControlStory({ + component: actionName => ( +
+ + -
- ), - { notes: { markdown: allowedTypeNotes } }, - ) - .add( - 'browse mode', - () => ( -
-
- To see this control in action, please login at - - https://dmsservice.demo.sensenet.com/ - -
-
- +
+ ), + markdown: autocompleteNotes, + storyName: 'FieldControls.AutoComplete', +}) + +fieldControlStory({ + component: actionName => ( +
+ + -
- ), - { notes: { markdown: allowedTypeNotes } }, - ) + +
+ ), + markdown: avatarNotes, + storyName: 'FieldControls.Avatar', +}) -storiesOf('FieldControls.AutoComplete', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: autocompleteNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: autocompleteNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: autocompleteNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: '', + storyName: 'FieldControls.Boolean', +}) -storiesOf('FieldControls.Avatar', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: avatarNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: avatarNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: avatarNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: checkboxgroupNotes, + storyName: 'FieldControls.CheckboxGroup', +}) -storiesOf('FieldControls.CheckboxGroup', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: checkboxgroupNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: checkboxgroupNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: checkboxgroupNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: colorPickerNotes, + storyName: 'FieldControls.ColorPicker', +}) -storiesOf('FieldControls.ColorPicker', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: colorPickerNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: colorPickerNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: colorPickerNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: datepickerNotes, + storyName: 'FieldControls.DatePicker', +}) -storiesOf('FieldControls.DatePicker', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: datepickerNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: datepickerNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: datepickerNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: datetimepickerNotes, + storyName: 'FieldControls.DateTimePicker', +}) -storiesOf('FieldControls.DateTimePicker', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: datetimepickerNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: datetimepickerNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: datetimepickerNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: displaynameNotes, + storyName: 'FieldControls.DisplayName', +}) -storiesOf('FieldControls.DisplayName', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: displaynameNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: displaynameNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: displaynameNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: dropdownlistNotes, + storyName: 'FieldControls.DropDownList', +}) -storiesOf('FieldControls.DropDownList', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: dropdownlistNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: dropdownlistNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: dropdownlistNotes } }, - ) - .add( - 'ApprovingModeChoice', - () => ( - - ), - { notes: { markdown: approvingModeChoiceNotes } }, - ) - .add( - 'VersioningModeChoice', - () => ( - - ), - { notes: { markdown: versioningModeChoiceNotes } }, - ) +fieldControlStory({ + component: actionName => { + const schema = reactControlMapper(testRepository).getFullSchemaForContentType('File', actionName) + const fieldMapping = schema.fieldMappings.find(a => a.fieldSettings.Name === 'DisplayName') + return ( + + ) + }, + markdown: filenameNotes, + storyName: 'FieldControls.FileName', +}) - .add( - 'VersioningMode', - () => ( - - ), - { notes: { markdown: versioningModeNotes } }, - ) +fieldControlStory({ + component: actionName => ( +
+ + + +
+ ), + markdown: fileUploadNotes, + storyName: 'FieldControls.FileUpload', +}) -storiesOf('FieldControls.FileName', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: filenameNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: filenameNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: filenameNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: nameNotes, + storyName: 'FieldControls.Name', +}) -storiesOf('FieldControls.FileUpload', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: fileUploadNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: numberNotes, + storyName: 'FieldControls.Number', +}) -storiesOf('FieldControls.Name', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: nameNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: nameNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: nameNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: numberNotes, + storyName: 'FieldControls.Number.Percantage', +}) -storiesOf('FieldControls.Number', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode integer', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) - .add( - 'new mode decimal', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) - .add( - 'edit mode integer', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) - .add( - 'edit mode decimal', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) - .add( - 'new mode currency', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) - .add( - 'edit mode currency', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) - .add( - 'browse mode currency', - () => ( - - ), - { notes: { markdown: numberNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: numberNotes, + storyName: 'FieldControls.Number.Currency', +}) -storiesOf('FieldControls.Password', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: passwordNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: passwordNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: numberNotes, + storyName: 'FieldControls.Number.Double', +}) -storiesOf('FieldControls.RadioButtonGroup', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: radiobuttongroupNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: radiobuttongroupNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: radiobuttongroupNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: passwordNotes, + storyName: 'FieldControls.Password', +}) -storiesOf('FieldControls.ReferenceGrid', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: referenceGridNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: referenceGridNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: referenceGridNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: radiobuttongroupNotes, + storyName: 'FieldControls.RadioButtonGroup', +}) -storiesOf('FieldControls.RichTextEditor', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: richtextNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: richtextNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: richtextNotes } }, - ) +fieldControlStory({ + component: actionName => ( +
+ + + +
+ ), + markdown: referenceGridNotes, + storyName: 'FieldControls.ReferenceGrid', +}) -storiesOf('FieldControls.ShortText', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: shorttextNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: shorttextNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: shorttextNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: richtextNotes, + storyName: 'FieldControls.RichTextEditor', +}) -storiesOf('FieldControls.TagsInput', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( - - ), - { notes: { markdown: tagsInputNotes } }, - ) - .add( - 'edit mode', - () => ( - - ), - { notes: { markdown: tagsInputNotes } }, - ) - .add( - 'browse mode', - () => ( - - ), - { notes: { markdown: tagsInputNotes } }, - ) +fieldControlStory({ + component: actionName => ( + + ), + markdown: shorttextNotes, + storyName: 'FieldControls.ShortText', +}) -storiesOf('FieldControls.Textarea', module) - .addDecorator(withKnobs) - .addDecorator(withA11y) - .addDecorator(withActions('change')) - .add( - 'new mode', - () => ( -