diff --git a/.gitignore b/.gitignore index 8638ea8..eaf45ba 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ yarn-error.log* _ignore .parcel-cache +mm-logs diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..8a573fe --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3.9" +networks: + default: + name: "mattermost-apps-dev" +services: + mattermost: + image: "mattermost/mattermost-enterprise-edition:7.9.0" # https://hub.docker.com/r/mattermost/mattermost-enterprise-edition/tags + restart: "unless-stopped" + depends_on: + - "db" + ports: + - "8065:8065" + # env_file: + # - ".docker.env" + environment: + MM_SQLSETTINGS_DRIVERNAME: "postgres" + MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest@db/mattermost_test?sslmode=disable\u0026connect_timeout=10" + MM_SERVICESETTINGS_CORSALLOWCREDENTIALS: true + MM_SERVICESETTINGS_CORSEXPOSEDHEADERS: "Access-Control-Allow-Origin,Access-Control-Allow-Methods" + MM_SERVICESETTINGS_LISTENADDRESS: ":8065" + MM_SERVICESETTINGS_SITEURL: "http://mattermost:8065" + MM_SERVICESETTINGS_ENABLEBOTACCOUNTCREATION: "true" + MM_SERVICESETTINGS_ENABLEUSERACCESSTOKENS: "true" + MM_SERVICESETTINGS_ENABLEOAUTHSERVICEPROVIDER: "true" + MM_SERVICESETTINGS_ENABLEDEVELOPER: "true" + MM_SERVICESETTINGS_ENABLETESTING: "true" + MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS: "true" + MM_EXPERIMENTALSETTINGS_ENABLEAPPBAR: "true" + MM_PLUGINSETTINGS_ENABLEUPLOADS: "true" + MM_LOGSETTINGS_CONSOLELEVEL: "DEBUG" + MM_LOGSETTINGS_FILELEVEL: "DEBUG" + MM_FILESETTINGS_MAXFILESIZE: 123524266 + volumes: + - "./mm-logs:/mattermost/logs:rw" + db: + image: "postgres" + restart: "unless-stopped" + environment: + POSTGRES_PASSWORD: "mostest" + POSTGRES_USER: "mmuser" + POSTGRES_DB: "mattermost_test" diff --git a/package-lock.json b/package-lock.json index 92d659d..f26e0ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.0", + "@mattermost/client": "^9.8.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -27,6 +28,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@mattermost/types": "^9.8.0", "jest-environment-jsdom": "^29.7.0", "parcel": "^2.12.0", "process": "^0.11.10", @@ -4092,6 +4094,36 @@ "win32" ] }, + "node_modules/@mattermost/client": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@mattermost/client/-/client-9.8.0.tgz", + "integrity": "sha512-JSYdk70CeAwrSVdza2peNUtxNpbHwiq4WzF3P2EbDSJi0Qlvzm997LR2n2tIOvaw4kPCRRSOVh4P4n4bAyU6Jw==", + "dependencies": { + "form-data": "^4.0.0" + }, + "peerDependencies": { + "@mattermost/types": "9.3.0", + "typescript": "^4.3.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@mattermost/types": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@mattermost/types/-/types-9.8.0.tgz", + "integrity": "sha512-V6GbwISnR3mic4C5kSNs0mNmtV49/lL1SnGIIOUDOc3FsWvPCLuE+Dfd76wpjyCrePzeSViu7z9pMblGV1VPCg==", + "peerDependencies": { + "typescript": "^4.3.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@mischnic/json-sourcemap": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz", @@ -12805,7 +12837,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", diff --git a/package.json b/package.json index 2dc45a1..fa84c66 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.0", + "@mattermost/client": "^9.8.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -21,6 +22,13 @@ "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, + "devDependencies": { + "@mattermost/types": "^9.8.0", + "jest-environment-jsdom": "^29.7.0", + "parcel": "^2.12.0", + "process": "^0.11.10", + "ts-jest": "^29.1.5" + }, "scripts": { "start": "parcel src/index.html --dist-dir build", "build": "parcel build src/index.html --dist-dir build", @@ -29,7 +37,8 @@ "lint": "eslint src/**/*.ts src/**/*.tsx", "fix": "npm run lint -- --fix", "check-types": "tsc --noEmit", - "ci": "npm run lint && npm run check-types && npm run test:ci && npm run build" + "ci": "npm run lint && npm run check-types && npm run test:ci && npm run build", + "mattermost": "cd docker && docker-compose up" }, "browserslist": { "production": [ @@ -42,11 +51,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "devDependencies": { - "jest-environment-jsdom": "^29.7.0", - "parcel": "^2.12.0", - "process": "^0.11.10", - "ts-jest": "^29.1.5" } } diff --git a/src/App.tsx b/src/App.tsx index 3a1664a..c3255c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import SectionPage from './components/SectionPage'; import {IClient} from './client/IClient'; import {ClientProvider} from './hooks/useClient'; import {useMount} from './hooks/useMount'; +import {MattermostProvider} from './hooks/useMM'; type AppProps = { projectId: string; @@ -58,11 +59,13 @@ const App: React.FC = ({projectId, sectionId, client}) => { ); return ( - - - {pageContent} - - + + + + {pageContent} + + + ); } diff --git a/src/components/Files.tsx b/src/components/Files.tsx index 4afed60..d706e51 100644 --- a/src/components/Files.tsx +++ b/src/components/Files.tsx @@ -1,6 +1,8 @@ import {useGlobalStore} from '@/hooks/useGlobalStore'; +import {useMattermost} from '@/hooks/useMM'; import * as types from '@/types/music_sniper_types'; import {plural} from '@/utils'; +import {useState} from 'react'; type FilesProps = { files: types.FileData[] @@ -8,10 +10,59 @@ type FilesProps = { export const Files: React.FC = ({files}) => { const globalStore = useGlobalStore(); + const [fileUpload, setFileUpload] = useState(null); + + const mm = useMattermost(); + + const onFileUploadSubmit = async () => { + if (!fileUpload) { + return; + } + + if (!mm.client4) { + alert('Mattermost not configured'); + return; + } + + try { + alert(`Uploading file: ${fileUpload.name}`); + + const response = await mm.client4.uploadFile(fileUpload); + + setFileUpload(null); + } catch (e) { + alert(`Error uploading file: ${(e as Error).message}`); + } + + // const newFile: types.FileData = { + // id: 'file-' + Math.random().toString(), + // title: fileUpload.name, + // sectionId: 'section-1', + // }; + + // globalStore.addFile(newFile); + } + + + const onFileChange = (event: React.ChangeEvent) => { + const files = event.target.files; + if (files && files.length > 0) { + setFileUpload(files[0]); + } + }; return (
- + Files + + {files.map((file) => { const numComments = globalStore.getCommentsForFile(file.id).length; diff --git a/src/components/MMConfigButton.tsx b/src/components/MMConfigButton.tsx new file mode 100644 index 0000000..7aca48e --- /dev/null +++ b/src/components/MMConfigButton.tsx @@ -0,0 +1,110 @@ +import {useGlobalStore} from '@/hooks/useGlobalStore'; +import {makeClient4FromConfig, useMattermost} from '@/hooks/useMM'; +import {Client4} from '@mattermost/client'; +import {useState} from 'react'; + +export const MMConfigButton = () => { + const [showForm, setShowForm] = useState(false); + + const onSubmit = () => { + setShowForm(false); + } + + const button = ( + + ); + + if (!showForm) { + return button; + } + + return ( + <> + {button} + + + ); +} + +type MattermostConfigFormProps = { + onSubmit: () => void; +} + +const MMConfigForm = (props: MattermostConfigFormProps) => { + const mm = useMattermost(); + + const [url, setUrl] = useState(mm.savedConfig?.url || ''); + const [token, setToken] = useState(mm.savedConfig?.token || ''); + + const onSubmit = () => { + testConnection().then(() => { + mm.setSavedConfig({url, token}); + props.onSubmit(); + }); + }; + + const testConnection = async () => { + const client4 = makeClient4FromConfig({url, token}); + return testConnectWithClient(client4); + }; + + const testSavedConnection = async () => { + const client4 = mm.client4; + if (!client4) { + return; + } + + return testConnectWithClient(client4); + }; + + return ( +
+ setUrl(e.target.value)} + /> + setToken(e.target.value)} + /> + + + +
+ ); +}; + +const testConnectWithClient = (client4: Client4) => { + return client4.getMe().then((me) => { + alert(`Success! Logged in as ${me.username}`); + }, (err) => { + alert(`Error: ${err.message}`); + }); +}; diff --git a/src/components/SectionPage.tsx b/src/components/SectionPage.tsx index 71eea6d..1ba9f3c 100644 --- a/src/components/SectionPage.tsx +++ b/src/components/SectionPage.tsx @@ -5,6 +5,7 @@ import {Comments} from './Comments'; import {CreateComment} from './CreateComment'; import {SectionTitle} from './SectionTitle'; import {useGlobalStore} from '@/hooks/useGlobalStore'; +import {MMConfigButton} from './MMConfigButton'; type SectionPageProps = { projectId: string; @@ -28,6 +29,7 @@ const SectionPage: React.FC = ({projectId, sectionId}) => { +
); } diff --git a/src/hooks/useMM.tsx b/src/hooks/useMM.tsx new file mode 100644 index 0000000..a427f2b --- /dev/null +++ b/src/hooks/useMM.tsx @@ -0,0 +1,71 @@ +import {createContext, useContext, useEffect, useMemo, useState} from 'react'; +import {Client4} from '@mattermost/client'; +import {MattermostConfig} from '@/types/mattermost_types'; + +type MattermostContextValue = { + client4: Client4 | null; + savedConfig: MattermostConfig | null; + setSavedConfig: (config: MattermostConfig) => void; +}; + +const mmContext = createContext(null); + +export const useMattermost = (): MattermostContextValue => { + return useContext(mmContext)!; +}; + +export const MattermostProvider = (props: React.PropsWithChildren) => { + const [savedConfig, setSavedConfig] = useState(null); + const [client4, setClient4] = useState(null); + + useEffect(() => { + const config = getMattermostConfigFromLocalStorage(); + if (!config) { + return; + } + + const client = makeClient4FromConfig(config); + setClient4(client); + setSavedConfig(config); + }, []); + + const value = useMemo(() => ({ + client4, + savedConfig, + setSavedConfig: (config: MattermostConfig) => { + const client = makeClient4FromConfig(config); + setClient4(client); + setSavedConfig(config); + saveMattermostConfigToLocalStorage(config); + }, + }), [savedConfig, client4]); + + return ( + + {props.children} + + ); +}; + +const MATTERMOST_CONFIG_LOCAL_STORAGE_KEY = 'mattermost_config'; + +const getMattermostConfigFromLocalStorage = (): MattermostConfig | null => { + const configString = localStorage.getItem(MATTERMOST_CONFIG_LOCAL_STORAGE_KEY); + if (!configString) { + return null; + } + + return JSON.parse(configString); +}; + +const saveMattermostConfigToLocalStorage = (config: MattermostConfig) => { + const configString = JSON.stringify(config); + localStorage.setItem(MATTERMOST_CONFIG_LOCAL_STORAGE_KEY, configString); +} + +export const makeClient4FromConfig = (config: MattermostConfig): Client4 => { + const client4 = new Client4(); + client4.setUrl(config.url); + client4.setToken(config.token); + return client4; +}; diff --git a/src/types/mattermost_types.ts b/src/types/mattermost_types.ts new file mode 100644 index 0000000..454065b --- /dev/null +++ b/src/types/mattermost_types.ts @@ -0,0 +1,4 @@ +export type MattermostConfig = { + url: string; + token: string; +}