Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

182
webapp/platform/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,182 @@
This folder contains a number of packages intended to be built and shipped separately on NPM as well as a few legacy packages for internal use only (`reselect` and `mattermost-redux`). The following documentation only applies to the newer packages and not to the legacy ones.
### Working with subpackages
To interact with one or more packages in a workspace, such as to add a dependency or run a script, use the `--workspace` (or `--workspaces`) flag. This can be done when using built-in NPM commands such as `npm add` or when running scripts. This doesn't need to be done from inside the package.
```sh
# Add a dependency to a single package
npm add react --workspace=packages/apple
# Build multiple packages
npm run build --workspace=packages/banana --workspace=packages/carrot
# Clean all workspaces
npm run clean --workspaces
```
To install dependencies for a workspace, simply run `npm install` from the root of the source tree as you would do normally. Every packages' dependencies will be included in `node_modules` and in the `package-lock.json` which are shared across the repo.
### Importing a subpackage
Subpackages should be imported using their full name, both inside the web app and when installing them using `npm`. They should not be imported using a relative path, and the `src` folder shouldn't be necessary to include.
```javascript
// Correct
import {Client4} from '@mattermost/client';
import {UserProfile} from '@mattermost/types/users';
// Incorrect
import Client4 from 'packages/client/src/client4.ts';
import {UserProfile} from '../../types/src/users';
```
Some tools have difficulty doing this on their own, but they often support import path aliases so that we can keep them consistent acrosss the code base. More details on how to do this will be provided in packages where this is necessary such as `types`.
#### Importing one subpackage into another
When building packages that depend on each other, be careful to:
1. Avoid import loops. While JavaScript lets us get away with these in most cases within a project, we cannot have two packages that depend directly with each other.
1. Not compile one subpackage into another. We don't want the published libraries to include code from one subpackage into another. They should be set up so that they're peer dependencies in the `package.json`, and if a project wants to use multiple packages, they can install them each separately.
As above, some tooling may need additional configuration to have one subpackage use code from another. For example, in packages compiled with the TypeScript compiler (tsc), you'll need to have the `tsconfig.json` from the dependent pacakge reference its dependency using the `references` field.
### Versioning subpackages
At this time, we'll have the version of each package match the version of the web app. Versions can be incremented for each affected package by using [`npm version`](https://docs.npmjs.com/cli/v6/commands/npm-version), and then `npm install` should be run to propagate those changes into the shared `package-lock.json`.
```sh
# Set a version of a single package
npm version 6.7.8 --workspace=packages/apple
# Increment the version of each package to the next minor version
npm version minor --workspaces
## Increment the version of a package to a pre-release version of the next minor version
npm version preminor --workspace=packages/apple
```
When a subpackage imports another, it should be set to depend on the `*` version of the other subpackage.
### Adding a new subpackage
To set up a new package:
1. Add a `package.json` and `README.md` for that package.
1. Ensure all source files are located in `src` and all compiled files are built to `lib`.
1. Add an entry to the `workspaces` section of the root `package.json` so that NPM is aware of your package.
1. Set up import aliases so that the package is visible from the web app to the following tools:
1. **TypeScript** - In the root `tsconfig.json`, add an entry to the `compilerOptions.paths` section pointing to the `src` folder and an entry to the `references` section pointing to the root of your package which should contain its own `tsconfig.json`.
Note that the `compilerOptions.paths` entry will differ based on if your package exports just a single module (ie a single `index.js` file) or if it exports multiple submodules.
```json5
{
"compilerOptions": {
"paths": {
"@mattermost/apple": ["packages/apple/lib"], // import * as Apple from '@mattermost/apple';
"@mattermost/banana/*": ["packages/banana/lib/*"], // import Yellow from '@mattermost/banana/yellow';
}
},
"references": [
{"path": "./packages/apple"},
{"path": "./packages/banana"},
]
}
```
1. **Jest** - Add an entry to the `jest.moduleNameMapper` section of the root `jest.config.js` for your package. Since that setting supports regexes, you can add these to the existing patterns used by the `client` and `types` packages.
Similar to TypeScript, this will differ based on if the package exports a single module or multiple modules.
```json
{
"jest": {
"moduleNameMapper": {
"^@mattermost/(apple|client)$": "<rootDir>/packages/$1/src",
"^@mattermost/(banana|types)/(.*)$": "<rootDir>/packages/$1/src/$2",
}
}
}
```
1. Add the compiled code to the CircleCI dependency cache. This is done by modifying the `paths` used by the `save_cache` step in `.circleci/config.yml`
```yml
aliases:
- &save_cache
save_cache:
paths:
- ~/mattermost/mattermost-webapp/packages/apple/lib
- ~/mattermost/mattermost-webapp/packages/banana/lib
```
### Publishing a subpackage
The following is the rough process for releasing these packages. They'll require someone with write access on our NPM organization to run them, and they'll likely change over time as we improve this process.
For full releases accompanying new versions of Mattermost:
1. Clean the repo.
```sh
make clean
```
1. Update the version of the desired packages to match the server/web app as described above.
1. Download an up to date copy of the dependencies and update package-lock.json.
```sh
make node_modules
```
1. Check in the changes to the package-lock.json.
1. Build the desired packages.
```sh
npm run build --workspace=packages/apple --workspace=packages/banana
```
1. Test everything in the web app. This will be needed until the packages get their own standalone tests.
```sh
make check-style check-types test
```
1. Assuming those pass, you can now publish those packages to npm. You can also do a dry run first or use `npm pack` to see exactly which files will be pushed.
```sh
# Run a dry run which will list all the files to be included in the published package.
npm publish --dry-run --workspace=packages/apple
# Generate the tar file that will be uploaded to NPM for inspection.
npm pack --workspace=packages/apple
# Actually publish these packages. You can also use --workspaces to publish everything.
npm publish --access=public --workspace=packages/apple --workspace=packages/banana
```
The packages have now been published! There's still a few remaining cleanup tasks to do though.
1. Tag the commit for each package that has been updated. The tag name should be of the form `@mattermost/package-name@x.y.z`.
1. Push that commit and the corresponding tags up to GitHub
```sh
git push release-x.y
git push origin @mattermost/apple@x.y.z @mattermost/banana@x.y.z
```
#### Publishing a pre-release version
Similarly, you can publish a pre-release version of the package. This can be done either to use changes from master while developing another product/plugin or to generate a release candidate.
This process is the same as above, except the version will have a suffix like `-1`, `-2`, etc. As explained above, this can be automatically done by using `npm version preminor` for minor releases, `npm version premajor` for major releases, and `npm version prerelease` for patch releases. These versions won't be automatically installed when people add them using `npm add` without a version, but they can be installed by specifying the version number manually.
### Caveats
1. Currently, all packages are treated by CI as if they're part of the web app. This means that, for example, their style checking and tests are ran as part of the web app. In turn, that means that regardless of what tooling we use to build each package, they'll be compiled into the web app using webpack directly from source, and that it's possible for them to behave slightly differently in development compared to after release.
Eventually, we hope to get these building in parallel (so instead of having webpack watch the whole repo for changes during development, we'll have multiple watchers for the web app and each package) which should solve this issue, but that requires much larger changes that we're not ready to do yet.
1. For packages that export multiple submodules (such as `types`), we've chosen to expose these using Node's [subpath exports](https://nodejs.org/api/packages.html#subpath-exports) feature. Some tools like Webpack support this natively, but others like TypeScript and Jest don't support it yet. We've provided steps on how to support this in the `README.md` for the `types` package, but this may vary depending on the project's setup.

14
webapp/platform/client/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,14 @@
# Mattermost Client
This package contains the JavaScript/TypeScript client for [Mattermost](https://github.com/mattermost/mattermost-server). It's used by [the Mattermost web app](https://github.com/mattermost/mattermost-webapp) and related projects.
## Compilation and Packaging
As a member of Mattermost with write access to our NPM organization, you can build and publish this package by running the following commands:
```bash
npm run build --workspace=packages/client
npm publish --workspace=packages/client
```
Make sure to increment the version number in `package.json` first! You can add `-0`, `-1`, etc for pre-release versions.

9
webapp/platform/client/babel.config.js Обычный файл
Просмотреть файл

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};

11
webapp/platform/client/jest.config.js Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/** @type {import('jest').Config} */
module.exports = {
moduleNameMapper: {
'^@mattermost/types/(.*)$': '<rootDir>/../types/src/$1',
},
setupFiles: ['isomorphic-fetch'],
};

43
webapp/platform/client/package.json Обычный файл
Просмотреть файл

@@ -0,0 +1,43 @@
{
"name": "@mattermost/client",
"version": "7.9.0",
"description": "JavaScript/TypeScript client for Mattermost",
"keywords": [
"mattermost"
],
"homepage": "https://github.com/mattermost/mattermost-webapp/tree/master/packages/client#readme",
"license": "MIT",
"files": [
"lib"
],
"main": "./lib/index.js",
"repository": {
"type": "git",
"url": "github:mattermost/mattermost-webapp",
"directory": "packages/client"
},
"dependencies": {
"form-data": "4.0.0"
},
"devDependencies": {
"@babel/preset-typescript": "7.21.0",
"isomorphic-fetch": "3.0.0",
"jest": "*",
"nock": "*"
},
"peerDependencies": {
"@mattermost/types": "*",
"typescript": "^4.3"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"scripts": {
"build": "tsc --build --verbose",
"run": "tsc --watch --preserveWatchOutput",
"test": "jest",
"clean": "rm -rf tsconfig.tsbuildinfo ./lib"
}
}

92
webapp/platform/client/src/client4.test.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,92 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import nock from 'nock';
import Client4, {ClientError, HEADER_X_VERSION_ID} from './client4';
import {TelemetryHandler} from './telemetry';
describe('Client4', () => {
beforeAll(() => {
if (!nock.isActive()) {
nock.activate();
}
});
afterAll(() => {
nock.restore();
});
describe('doFetchWithResponse', () => {
test('serverVersion should be set from response header', async () => {
const client = new Client4();
client.setUrl('http://mattermost.example.com');
expect(client.serverVersion).toEqual('');
nock(client.getBaseRoute()).
get('/users/me').
reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.0.0.5.0.0.abc123'});
await client.getMe();
expect(client.serverVersion).toEqual('5.0.0.5.0.0.abc123');
nock(client.getBaseRoute()).
get('/users/me').
reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.3.0.5.3.0.abc123'});
await client.getMe();
expect(client.serverVersion).toEqual('5.3.0.5.3.0.abc123');
});
});
describe('fetchWithGraphQL', () => {
test('Should have correct graphql url', async () => {
const client = new Client4();
client.setUrl('http://mattermost.example.com');
expect(client.getGraphQLUrl()).toEqual('http://mattermost.example.com/api/v5/graphql');
});
});
});
describe('ClientError', () => {
test('standard fields should be enumerable', () => {
const error = new ClientError('https://example.com', {
message: 'This is a message',
server_error_id: 'test.app_error',
status_code: 418,
url: 'https://example.com/api/v4/error',
});
const copy = {...error};
expect(copy.message).toEqual(error.message);
expect(copy.server_error_id).toEqual(error.server_error_id);
expect(copy.status_code).toEqual(error.status_code);
expect(copy.url).toEqual(error.url);
});
});
describe('trackEvent', () => {
class TestTelemetryHandler implements TelemetryHandler {
trackEvent = jest.fn();
pageVisited = jest.fn();
}
test('should call the attached RudderTelemetryHandler, if one is attached to Client4', () => {
const client = new Client4();
client.setUrl('http://mattermost.example.com');
expect(() => client.trackEvent('test', 'onClick')).not.toThrowError();
const handler = new TestTelemetryHandler();
client.setTelemetryHandler(handler);
client.trackEvent('test', 'onClick');
expect(handler.trackEvent).toHaveBeenCalledTimes(1);
});
});

4307
webapp/platform/client/src/client4.ts Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

37
webapp/platform/client/src/errors.test.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Client4 from './client4';
import {cleanUrlForLogging} from './errors';
describe('cleanUrlForLogging', () => {
const baseUrl = 'https://mattermost.example.com/subpath';
const client = new Client4();
client.setUrl(baseUrl);
const testCases = [{
name: 'should remove server URL',
input: client.getUserRoute('me'),
expected: `${client.urlVersion}/users/me`,
}, {
name: 'should filter user IDs',
input: client.getUserRoute('1234'),
expected: `${client.urlVersion}/users/<filtered>`,
}, {
name: 'should filter email addresses',
input: `${client.getUsersRoute()}/email/test@example.com`,
expected: `${client.urlVersion}/users/email/<filtered>`,
}, {
name: 'should filter query parameters',
input: `${client.getUserRoute('me')}?foo=bar`,
expected: `${client.urlVersion}/users/me?<filtered>`,
}];
for (const testCase of testCases) {
test(testCase.name, () => {
expect(cleanUrlForLogging(baseUrl, testCase.input)).toEqual(testCase.expected);
});
}
});

52
webapp/platform/client/src/errors.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Given a URL from an API request, return a URL that has any parts removed that are either sensitive or that would
// prevent properly grouping the messages in Sentry.
export function cleanUrlForLogging(baseUrl: string, apiUrl: string): string {
let url = apiUrl;
// Trim the host name
url = url.substring(baseUrl.length);
// Filter the query string
const index = url.indexOf('?');
if (index !== -1) {
url = url.substring(0, index);
}
// A non-exhaustive whitelist to exclude parts of the URL that are unimportant (eg IDs) or may be sentsitive
// (eg email addresses). We prefer filtering out fields that aren't recognized because there should generally
// be enough left over for debugging.
//
// Note that new API routes don't need to be added here since this shouldn't be happening for newly added routes.
const whitelist = [
'api', 'v4', 'users', 'teams', 'scheme', 'name', 'members', 'channels', 'posts', 'reactions', 'commands',
'files', 'preferences', 'hooks', 'incoming', 'outgoing', 'oauth', 'apps', 'emoji', 'brand', 'image',
'data_retention', 'jobs', 'plugins', 'roles', 'system', 'timezones', 'schemes', 'redirect_location', 'patch',
'mfa', 'password', 'reset', 'send', 'active', 'verify', 'terms_of_service', 'login', 'logout', 'ids',
'usernames', 'me', 'username', 'email', 'default', 'sessions', 'revoke', 'all', 'audits', 'device', 'status',
'search', 'switch', 'authorized', 'authorize', 'deauthorize', 'tokens', 'disable', 'enable', 'exists', 'unread',
'invite', 'batch', 'stats', 'import', 'schemeRoles', 'direct', 'group', 'convert', 'view', 'search_autocomplete',
'thread', 'info', 'flagged', 'pinned', 'pin', 'unpin', 'opengraph', 'actions', 'thumbnail', 'preview', 'link',
'delete', 'logs', 'ping', 'config', 'client', 'license', 'websocket', 'webrtc', 'token', 'regen_token',
'autocomplete', 'execute', 'regen_secret', 'policy', 'type', 'cancel', 'reload', 'environment', 's3_test', 'file',
'caches', 'invalidate', 'database', 'recycle', 'compliance', 'reports', 'cluster', 'ldap', 'test', 'sync', 'saml',
'certificate', 'public', 'private', 'idp', 'elasticsearch', 'purge_indexes', 'analytics', 'old', 'webapp', 'fake',
];
url = url.split('/').map((part) => {
if (part !== '' && whitelist.indexOf(part) === -1) {
return '<filtered>';
}
return part;
}).join('/');
if (index !== -1) {
// Add this on afterwards since it wouldn't pass the whitelist
url += '?<filtered>';
}
return url;
}

18
webapp/platform/client/src/helpers.test.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {buildQueryString} from './helpers';
describe('Helpers', () => {
test.each([
[{}, ''],
[{a: 1}, '?a=1'],
[{a: 1, b: 'str'}, '?a=1&b=str'],
[{a: 1, b: 'str', c: undefined}, '?a=1&b=str'],
[{a: 1, b: 'str', c: 0}, '?a=1&b=str&c=0'],
[{a: 1, b: 'str', c: ''}, '?a=1&b=str&c='],
[{a: 1, b: undefined, c: 'str'}, '?a=1&c=str'],
])('buildQueryString with %o should return %s', (params, expected) => {
expect(buildQueryString(params)).toEqual(expected);
});
});

17
webapp/platform/client/src/helpers.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export function buildQueryString(parameters: Record<string, any>): string {
const keys = Object.keys(parameters);
if (keys.length === 0) {
return '';
}
const queryParams = Object.entries(parameters).
// eslint-disable-next-line @typescript-eslint/no-unused-vars
filter(([_, value]) => value !== undefined).
map(([key, value]) => `${key}=${encodeURIComponent(value)}`).
join('&');
return queryParams.length > 0 ? `?${queryParams}` : '';
}

13
webapp/platform/client/src/index.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {
default as Client4,
ClientError,
DEFAULT_LIMIT_AFTER,
DEFAULT_LIMIT_BEFORE,
} from './client4';
export type {TelemetryHandler} from './telemetry';
export type {WebSocketMessage} from './websocket';
export {default as WebSocketClient} from './websocket';

7
webapp/platform/client/src/telemetry.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export interface TelemetryHandler {
trackEvent: (userId: string, userRoles: string, category: string, event: string, props?: any) => void;
pageVisited: (userId: string, userRoles: string, category: string, name: string) => void;
}

410
webapp/platform/client/src/websocket.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,410 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const MAX_WEBSOCKET_FAILS = 7;
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
const JITTER_RANGE = 2000; // 2 sec
const WEBSOCKET_HELLO = 'hello';
export type MessageListener = (msg: WebSocketMessage) => void;
export type FirstConnectListener = () => void;
export type ReconnectListener = () => void;
export type MissedMessageListener = () => void;
export type ErrorListener = (event: Event) => void;
export type CloseListener = (connectFailCount: number) => void;
export default class WebSocketClient {
private conn: WebSocket | null;
private connectionUrl: string | null;
// responseSequence is the number to track a response sent
// via the websocket. A response will always have the same sequence number
// as the request.
private responseSequence: number;
// serverSequence is the incrementing sequence number from the
// server-sent event stream.
private serverSequence: number;
private connectFailCount: number;
private responseCallbacks: {[x: number]: ((msg: any) => void)};
/**
* @deprecated Use messageListeners instead
*/
private eventCallback: MessageListener | null = null;
/**
* @deprecated Use firstConnectListeners instead
*/
private firstConnectCallback: FirstConnectListener | null = null;
/**
* @deprecated Use reconnectListeners instead
*/
private reconnectCallback: ReconnectListener | null = null;
/**
* @deprecated Use missedMessageListeners instead
*/
private missedEventCallback: MissedMessageListener | null = null;
/**
* @deprecated Use errorListeners instead
*/
private errorCallback: ErrorListener | null = null;
/**
* @deprecated Use closeListeners instead
*/
private closeCallback: CloseListener | null = null;
private messageListeners = new Set<MessageListener>();
private firstConnectListeners = new Set<FirstConnectListener>();
private reconnectListeners = new Set<ReconnectListener>();
private missedMessageListeners = new Set<MissedMessageListener>();
private errorListeners = new Set<ErrorListener>();
private closeListeners = new Set<CloseListener>();
private connectionId: string | null;
constructor() {
this.conn = null;
this.connectionUrl = null;
this.responseSequence = 1;
this.serverSequence = 0;
this.connectFailCount = 0;
this.responseCallbacks = {};
this.connectionId = '';
}
// on connect, only send auth cookie and blank state.
// on hello, get the connectionID and store it.
// on reconnect, send cookie, connectionID, sequence number.
initialize(connectionUrl = this.connectionUrl, token?: string) {
if (this.conn) {
return;
}
if (connectionUrl == null) {
console.log('websocket must have connection url'); //eslint-disable-line no-console
return;
}
if (this.connectFailCount === 0) {
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
}
// Add connection id, and last_sequence_number to the query param.
// We cannot use a cookie because it will bleed across tabs.
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
this.conn = new WebSocket(`${connectionUrl}?connection_id=${this.connectionId}&sequence_number=${this.serverSequence}`);
this.connectionUrl = connectionUrl;
this.conn.onopen = () => {
if (token) {
this.sendMessage('authentication_challenge', {token});
}
if (this.connectFailCount > 0) {
console.log('websocket re-established connection'); //eslint-disable-line no-console
this.reconnectCallback?.();
this.reconnectListeners.forEach((listener) => listener());
} else if (this.firstConnectCallback || this.firstConnectListeners.size > 0) {
this.firstConnectCallback?.();
this.firstConnectListeners.forEach((listener) => listener());
}
this.connectFailCount = 0;
};
this.conn.onclose = () => {
this.conn = null;
this.responseSequence = 1;
if (this.connectFailCount === 0) {
console.log('websocket closed'); //eslint-disable-line no-console
}
this.connectFailCount++;
this.closeCallback?.(this.connectFailCount);
this.closeListeners.forEach((listener) => listener(this.connectFailCount));
let retryTime = MIN_WEBSOCKET_RETRY_TIME;
// If we've failed a bunch of connections then start backing off
if (this.connectFailCount > MAX_WEBSOCKET_FAILS) {
retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount * this.connectFailCount;
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
retryTime = MAX_WEBSOCKET_RETRY_TIME;
}
}
// Applying jitter to avoid thundering herd problems.
retryTime += Math.random() * JITTER_RANGE;
setTimeout(
() => {
this.initialize(connectionUrl, token);
},
retryTime,
);
};
this.conn.onerror = (evt) => {
if (this.connectFailCount <= 1) {
console.log('websocket error'); //eslint-disable-line no-console
console.log(evt); //eslint-disable-line no-console
}
this.errorCallback?.(evt);
this.errorListeners.forEach((listener) => listener(evt));
};
this.conn.onmessage = (evt) => {
const msg = JSON.parse(evt.data);
if (msg.seq_reply) {
// This indicates a reply to a websocket request.
// We ignore sequence number validation of message responses
// and only focus on the purely server side event stream.
if (msg.error) {
console.log(msg); //eslint-disable-line no-console
}
if (this.responseCallbacks[msg.seq_reply]) {
this.responseCallbacks[msg.seq_reply](msg);
Reflect.deleteProperty(this.responseCallbacks, msg.seq_reply);
}
} else if (this.eventCallback || this.messageListeners.size > 0) {
// We check the hello packet, which is always the first packet in a stream.
if (msg.event === WEBSOCKET_HELLO && (this.missedEventCallback || this.missedMessageListeners.size > 0)) {
console.log('got connection id ', msg.data.connection_id); //eslint-disable-line no-console
// If we already have a connectionId present, and server sends a different one,
// that means it's either a long timeout, or server restart, or sequence number is not found.
// Then we do the sync calls, and reset sequence number to 0.
if (this.connectionId !== '' && this.connectionId !== msg.data.connection_id) {
console.log('long timeout, or server restart, or sequence number is not found.'); //eslint-disable-line no-console
this.missedEventCallback?.();
this.missedMessageListeners.forEach((listener) => listener());
this.serverSequence = 0;
}
// If it's a fresh connection, we have to set the connectionId regardless.
// And if it's an existing connection, setting it again is harmless, and keeps the code simple.
this.connectionId = msg.data.connection_id;
}
// Now we check for sequence number, and if it does not match,
// we just disconnect and reconnect.
if (msg.seq !== this.serverSequence) {
console.log('missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence); //eslint-disable-line no-console
// We are not calling this.close() because we need to auto-restart.
this.connectFailCount = 0;
this.responseSequence = 1;
this.conn?.close(); // Will auto-reconnect after MIN_WEBSOCKET_RETRY_TIME.
return;
}
this.serverSequence = msg.seq + 1;
this.eventCallback?.(msg);
this.messageListeners.forEach((listener) => listener(msg));
}
};
}
/**
* @deprecated Use addMessageListener instead
*/
setEventCallback(callback: MessageListener) {
this.eventCallback = callback;
}
addMessageListener(listener: MessageListener) {
this.messageListeners.add(listener);
if (this.messageListeners.size > 5) {
// eslint-disable-next-line no-console
console.warn(`WebSocketClient has ${this.messageListeners.size} message listeners registered`);
}
}
removeMessageListener(listener: MessageListener) {
this.messageListeners.delete(listener);
}
/**
* @deprecated Use addFirstConnectListener instead
*/
setFirstConnectCallback(callback: FirstConnectListener) {
this.firstConnectCallback = callback;
}
addFirstConnectListener(listener: FirstConnectListener) {
this.firstConnectListeners.add(listener);
if (this.firstConnectListeners.size > 5) {
// eslint-disable-next-line no-console
console.warn(`WebSocketClient has ${this.firstConnectListeners.size} first connect listeners registered`);
}
}
removeFirstConnectListener(listener: FirstConnectListener) {
this.firstConnectListeners.delete(listener);
}
/**
* @deprecated Use addReconnectListener instead
*/
setReconnectCallback(callback: ReconnectListener) {
this.reconnectCallback = callback;
}
addReconnectListener(listener: ReconnectListener) {
this.reconnectListeners.add(listener);
if (this.reconnectListeners.size > 5) {
// eslint-disable-next-line no-console
console.warn(`WebSocketClient has ${this.reconnectListeners.size} reconnect listeners registered`);
}
}
removeReconnectListener(listener: ReconnectListener) {
this.reconnectListeners.delete(listener);
}
/**
* @deprecated Use addMissedMessageListener instead
*/
setMissedEventCallback(callback: MissedMessageListener) {
this.missedEventCallback = callback;
}
addMissedMessageListener(listener: MissedMessageListener) {
this.missedMessageListeners.add(listener);
if (this.missedMessageListeners.size > 5) {
// eslint-disable-next-line no-console
console.warn(`WebSocketClient has ${this.missedMessageListeners.size} missed message listeners registered`);
}
}
removeMissedMessageListener(listener: MissedMessageListener) {
this.missedMessageListeners.delete(listener);
}
/**
* @deprecated Use addErrorListener instead
*/
setErrorCallback(callback: ErrorListener) {
this.errorCallback = callback;
}
addErrorListener(listener: ErrorListener) {
this.errorListeners.add(listener);
if (this.errorListeners.size > 5) {
// eslint-disable-next-line no-console
console.warn(`WebSocketClient has ${this.errorListeners.size} error listeners registered`);
}
}
removeErrorListener(listener: ErrorListener) {
this.errorListeners.delete(listener);
}
/**
* @deprecated Use addCloseListener instead
*/
setCloseCallback(callback: CloseListener) {
this.closeCallback = callback;
}
addCloseListener(listener: CloseListener) {
this.closeListeners.add(listener);
if (this.closeListeners.size > 5) {
// eslint-disable-next-line no-console
console.warn(`WebSocketClient has ${this.closeListeners.size} close listeners registered`);
}
}
removeCloseListener(listener: CloseListener) {
this.closeListeners.delete(listener);
}
close() {
this.connectFailCount = 0;
this.responseSequence = 1;
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
this.conn.onclose = () => {};
this.conn.close();
this.conn = null;
console.log('websocket closed'); //eslint-disable-line no-console
}
}
sendMessage(action: string, data: any, responseCallback?: () => void) {
const msg = {
action,
seq: this.responseSequence++,
data,
};
if (responseCallback) {
this.responseCallbacks[msg.seq] = responseCallback;
}
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
this.conn.send(JSON.stringify(msg));
} else if (!this.conn || this.conn.readyState === WebSocket.CLOSED) {
this.conn = null;
this.initialize();
}
}
userTyping(channelId: string, parentId: string, callback?: () => void) {
const data = {
channel_id: channelId,
parent_id: parentId,
};
this.sendMessage('user_typing', data, callback);
}
userUpdateActiveStatus(userIsActive: boolean, manual: boolean, callback?: () => void) {
const data = {
user_is_active: userIsActive,
manual,
};
this.sendMessage('user_update_active_status', data, callback);
}
getStatuses(callback?: () => void) {
this.sendMessage('get_statuses', null, callback);
}
getStatusesByIds(userIds: string[], callback?: () => void) {
const data = {
user_ids: userIds,
};
this.sendMessage('get_statuses_by_ids', data, callback);
}
}
export type WebSocketBroadcast = {
omit_users: Record<string, boolean>;
user_id: string;
channel_id: string;
team_id: string;
}
export type WebSocketMessage<T = any> = {
event: string;
data: T;
broadcast: WebSocketBroadcast;
seq: number;
}

24
webapp/platform/client/tsconfig.json Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es6",
"declaration": true,
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
"outDir": "./lib",
"rootDir": "./src",
"composite": true,
"paths": {
"@mattermost/types/*": ["../types/lib/*"]
}
},
"exclude": ["**/node_modules", "**/lib", "**/*.test.js", "**/*.test.ts"],
"references": [
{"path": "../types"}
]
}

24
webapp/platform/components/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
# Mattermost Components
The goal of this package is to be a place where components common to all products can be shared.
Currently a work in progress. Next steps involve implementing webpack module federation in the webapp and locking down how the development experience will work for the webapp multi product architecture.
## Usage
Coming soon with multi product architecture.
## Compilation
Building is done using rollup. This must be done so the webapp webpack will pick up the changes. (multi product development experience coming soon)
```bash
npm run build
```
or from the root of the webapp with
```bash
npm run build --workspace=packages/mattermost
```

42
webapp/platform/components/babel.config.js Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const config = {
presets: [
['@babel/preset-env', {
targets: {
chrome: 66,
firefox: 60,
edge: 42,
safari: 12,
},
modules: false,
}],
['@babel/preset-react', {
useBuiltIns: true,
}],
['@babel/typescript', {
allExtensions: true,
isTSX: true,
}],
],
plugins: [
'@babel/plugin-transform-runtime',
[
'babel-plugin-styled-components',
{
ssr: false,
fileName: false,
},
],
[
'formatjs',
{
idInterpolationPattern: '[sha512:contenthash:base64:6]',
ast: true,
},
],
],
};
module.exports = config;

61
webapp/platform/components/package.json Обычный файл
Просмотреть файл

@@ -0,0 +1,61 @@
{
"name": "@mattermost/components",
"version": "7.4.0",
"module": "dist/index.esm.js",
"types": "dist/index.esm.d.ts",
"styles": "dist/index.esm.css",
"scripts": {
"build": "rollup -c",
"run": "rollup -c --watch",
"clean": "rm -rf node_modules dist"
},
"devDependencies": {
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-typescript": "^8.3.1",
"@types/lodash": "^4.14.178",
"@types/react": "^17.0.2",
"@types/react-bootstrap": "^0.32.22",
"@types/react-dom": "^17.0.2",
"@types/react-redux": "^7.1.21",
"@types/shallow-equals": "^1.0.0",
"@types/styled-components": "^5.1.19",
"babel-loader": "^8.2.3",
"babel-plugin-formatjs": "10.3.14",
"babel-plugin-styled-components": "^2.0.6",
"css-loader": "^6.7.1",
"rollup": "^2.75.7",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-scss": "^3.0.0",
"rollup-plugin-ts": "^2.0.5",
"sass": "^1.49.9",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"typescript": "^4.3.4",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2"
},
"peerDependencies": {
"@babel/runtime-corejs3": "^7.17.8",
"@mui/base": "5.0.0-alpha.116",
"@mui/material": "5.11.7",
"@tippyjs/react": "^4.2.6",
"classnames": "^2.3.1",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-bootstrap": "github:mattermost/react-bootstrap#d821e2b1db1059bd36112d7587fd1b0912b27626",
"react-dom": "^17.0.2",
"react-intl": "^5.20.0",
"shallow-equals": "^1.0.0",
"styled-components": "^5.3.5",
"tippy.js": "^6.3.7"
}
}

44
webapp/platform/components/rollup.config.js Обычный файл
Просмотреть файл

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// eslint-disable-next-line import/no-unresolved
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import scss from 'rollup-plugin-scss';
import typescript from '@rollup/plugin-typescript';
import packagejson from './package.json';
const externals = [
...Object.keys(packagejson.dependencies || {}),
...Object.keys(packagejson.peerDependencies || {}),
'mattermost-redux',
'reselect',
];
export default [
{
input: 'src/index.tsx',
output: [
{
sourcemap: true,
file: packagejson.module,
format: 'es',
globals: {'styled-components': 'styled'},
},
],
plugins: [
scss(),
resolve({
browser: true,
extensions: ['.ts', '.tsx'],
}),
commonjs(),
typescript(),
],
external: (pkg) => externals.some((external) => pkg.startsWith(external)),
watch: {
clearScreen: false,
},
},
];

Просмотреть файл

@@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MutableRefObject, useEffect} from 'react';
export function useClickOutsideRef(ref: MutableRefObject<HTMLElement | null>, handler: (event: MouseEvent) => void): void {
useEffect(() => {
function onMouseDown(event: MouseEvent) {
const target = event.target as any;
if (ref.current && target instanceof Node && !ref.current.contains(target)) {
handler(event);
}
}
// Bind the event listener
document.addEventListener('mousedown', onMouseDown);
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', onMouseDown);
};
}, [ref, handler]);
}

Просмотреть файл

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useEffect, useRef, useState, useMemo} from 'react';
export function useElementAvailable(
elementIds: string[],
intervalMS = 250,
): boolean {
const checkAvailableInterval = useRef<NodeJS.Timeout | null>(null);
const [available, setAvailable] = useState(false);
useEffect(() => {
if (available) {
if (checkAvailableInterval.current) {
clearInterval(checkAvailableInterval.current);
checkAvailableInterval.current = null;
}
return;
} else if (checkAvailableInterval.current) {
return;
}
checkAvailableInterval.current = setInterval(() => {
if (elementIds.every((x) => document.getElementById(x))) {
setAvailable(true);
if (checkAvailableInterval.current) {
clearInterval(checkAvailableInterval.current);
checkAvailableInterval.current = null;
}
}
}, intervalMS);
}, []);
return useMemo(() => available, [available]);
}

Просмотреть файл

@@ -0,0 +1,71 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useLayoutEffect, useMemo, useState} from 'react';
import throttle from 'lodash/throttle';
import {useElementAvailable} from './useElementAvailable';
export type Coords = {
x?: string;
y?: string;
}
export type PunchOutCoordsHeightAndWidth = Coords & {
width: string;
height: string;
}
type PunchOutOffset = {
x: number;
y: number;
width: number;
height: number;
}
export const useMeasurePunchouts = (elementIds: string[], additionalDeps: any[], offset?: PunchOutOffset): PunchOutCoordsHeightAndWidth | null => {
const elementsAvailable = useElementAvailable(elementIds);
const [size, setSize] = useState({x: window.innerWidth, y: window.innerHeight});
const updateSize = throttle(() => {
setSize({x: window.innerWidth, y: window.innerHeight});
}, 100, {trailing: true});
useLayoutEffect(() => {
window.addEventListener('resize', updateSize);
return () =>
window.removeEventListener('resize', updateSize);
}, []);
const channelPunchout = useMemo(() => {
let minX = Number.MAX_SAFE_INTEGER;
let minY = Number.MAX_SAFE_INTEGER;
let maxX = Number.MIN_SAFE_INTEGER;
let maxY = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < elementIds.length; i++) {
const rectangle = document.getElementById(elementIds[i])?.getBoundingClientRect();
if (!rectangle) {
return null;
}
if (rectangle.x < minX) {
minX = rectangle.x;
}
if (rectangle.y < minY) {
minY = rectangle.y;
}
if (rectangle.x + rectangle.width > maxX) {
maxX = rectangle.x + rectangle.width;
}
if (rectangle.y + rectangle.height > maxY) {
maxY = rectangle.y + rectangle.height;
}
}
return {
x: `${minX + (offset ? offset.x : 0)}px`,
y: `${minY + (offset ? offset.y : 0)}px`,
width: `${(maxX - minX) + (offset ? offset.width : 0)}px`,
height: `${(maxY - minY) + (offset ? offset.height : 0)}px`,
};
}, [...elementIds, ...additionalDeps, size, elementsAvailable]);
return channelPunchout;
};

Просмотреть файл

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import MuiFocusTrap, {FocusTrapProps as MuiFocusTrapProps} from '@mui/base/FocusTrap';
export interface Props {
active: MuiFocusTrapProps['open'];
children: MuiFocusTrapProps['children'];
}
export const FocusTrap = ({active, children}: Props) => {
return (
<MuiFocusTrap open={active}>
{children}
</MuiFocusTrap>
);
};

Просмотреть файл

@@ -0,0 +1,226 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {FocusTrap} from '../focus_trap';
export type Props = {
className?: string;
onExited: () => void;
modalHeaderText?: React.ReactNode;
show?: boolean;
handleCancel?: () => void;
handleConfirm?: () => void;
handleEnterKeyPress?: () => void;
handleKeydown?: (event?: React.KeyboardEvent<HTMLDivElement>) => void;
confirmButtonText?: React.ReactNode;
confirmButtonClassName?: string;
cancelButtonText?: React.ReactNode;
cancelButtonClassName?: string;
isConfirmDisabled?: boolean;
isDeleteModal?: boolean;
id: string;
autoCloseOnCancelButton?: boolean;
autoCloseOnConfirmButton?: boolean;
/**
* If false, bootrap's Modal will not enforce focus on the modal and will
* transfer the mechanism to the FocusTrap component instead.
*/
enforceFocus?: boolean;
container?: React.ReactNode | React.ReactNodeArray;
ariaLabel?: string;
errorText?: string;
compassDesign?: boolean;
backdrop?: boolean;
backdropClassName?: string;
headerButton?: React.ReactNode;
tabIndex?: number;
children: React.ReactNode;
keyboardEscape?: boolean;
};
type State = {
show: boolean;
isFocalTrapActive: boolean;
}
export class GenericModal extends React.PureComponent<Props, State> {
static defaultProps: Partial<Props> = {
show: true,
id: 'genericModal',
autoCloseOnCancelButton: true,
autoCloseOnConfirmButton: true,
enforceFocus: true,
keyboardEscape: true,
};
constructor(props: Props) {
super(props);
this.state = {
show: props.show!,
isFocalTrapActive: false,
};
}
onHide = () => {
this.setState({show: false});
}
handleCancel = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
if (this.props.autoCloseOnCancelButton) {
this.onHide();
}
if (this.props.handleCancel) {
this.props.handleCancel();
}
}
handleConfirm = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
if (this.props.autoCloseOnConfirmButton) {
this.onHide();
}
if (this.props.handleConfirm) {
this.props.handleConfirm();
}
}
private onEnterKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter') {
if (this.props.autoCloseOnConfirmButton) {
this.onHide();
}
if (this.props.handleEnterKeyPress) {
this.props.handleEnterKeyPress();
}
}
this.props.handleKeydown?.(event);
}
private handleShow = () => {
if (this.props.enforceFocus === false) {
this.setState({isFocalTrapActive: true});
}
}
render() {
let confirmButton;
if (this.props.handleConfirm) {
const isConfirmOrDeleteClassName = this.props.isDeleteModal ? 'delete' : 'confirm';
let confirmButtonText: React.ReactNode = (
<FormattedMessage
id='generic_modal.confirm'
defaultMessage='Confirm'
/>
);
if (this.props.confirmButtonText) {
confirmButtonText = this.props.confirmButtonText;
}
confirmButton = (
<button
type='submit'
className={classNames('GenericModal__button', isConfirmOrDeleteClassName, this.props.confirmButtonClassName, {
disabled: this.props.isConfirmDisabled,
})}
onClick={this.handleConfirm}
disabled={this.props.isConfirmDisabled}
>
{confirmButtonText}
</button>
);
}
let cancelButton;
if (this.props.handleCancel) {
let cancelButtonText: React.ReactNode = (
<FormattedMessage
id='generic_modal.cancel'
defaultMessage='Cancel'
/>
);
if (this.props.cancelButtonText) {
cancelButtonText = this.props.cancelButtonText;
}
cancelButton = (
<button
type='button'
className={classNames('GenericModal__button cancel', this.props.cancelButtonClassName)}
onClick={this.handleCancel}
>
{cancelButtonText}
</button>
);
}
const headerText = this.props.modalHeaderText && (
<div className='GenericModal__header'>
<h1 id='genericModalLabel'>
{this.props.modalHeaderText}
</h1>
{this.props.headerButton}
</div>
);
const isFocusTrapActive = this.props.enforceFocus === false ? this.state.isFocalTrapActive : false;
return (
<Modal
id={this.props.id}
role='dialog'
aria-label={this.props.ariaLabel}
aria-labelledby={this.props.ariaLabel ? undefined : 'genericModalLabel'}
dialogClassName={classNames('a11y__modal GenericModal', {GenericModal__compassDesign: this.props.compassDesign}, this.props.className)}
show={this.state.show}
onShow={this.handleShow}
restoreFocus={true}
enforceFocus={this.props.enforceFocus}
onHide={this.onHide}
onExited={this.props.onExited}
backdrop={this.props.backdrop}
backdropClassName={this.props.backdropClassName}
container={this.props.container}
keyboard={this.props.keyboardEscape}
>
<FocusTrap active={isFocusTrapActive}>
<div
onKeyDown={this.onEnterKeyDown}
tabIndex={this.props.tabIndex || 0}
className='GenericModal__wrapper-enter-key-press-catcher'
>
<Modal.Header closeButton={true}>
{this.props.compassDesign && headerText}
</Modal.Header>
<Modal.Body>
{this.props.compassDesign ? (
this.props.errorText && (
<div className='genericModalError'>
<i className='icon icon-alert-outline'/>
<span>{this.props.errorText}</span>
</div>
)
) : (
headerText
)}
<div className='GenericModal__body'>
{this.props.children}
</div>
</Modal.Body>
{(cancelButton || confirmButton) && <Modal.Footer>
{cancelButton}
{confirmButton}
</Modal.Footer>}
</div>
</FocusTrap>
</Modal>
);
}
}

18
webapp/platform/components/src/index.tsx Обычный файл
Просмотреть файл

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// type
export type {Props as GenericModalProps} from './generic_modal/generic_modal';
export type {CircleSkeletonLoaderProps, RectangleSkeletonLoaderProps} from './skeleton_loader';
export type {Props as FocusTrapProps} from './focus_trap';
// components
export {GenericModal} from './generic_modal/generic_modal';
export {CircleSkeletonLoader, RectangleSkeletonLoader} from './skeleton_loader';
export * from './tour_tip';
export * from './pulsating_dot';
export {FocusTrap} from './focus_trap';
// hooks
export * from './common/hooks/useMeasurePunchouts';
export {useElementAvailable} from './common/hooks/useElementAvailable';

Просмотреть файл

@@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Coords} from '../common/hooks/useMeasurePunchouts';
import './pulsating_dot.scss';
type Props = {
targetRef?: React.RefObject<HTMLImageElement>;
className?: string;
onClick?: (e: React.MouseEvent) => void;
coords?: Coords;
}
export class PulsatingDot extends React.PureComponent<Props> {
public render() {
let customStyles = {};
if (this.props?.coords) {
customStyles = {
transform: `translate(${this.props.coords?.x}px, ${this.props.coords?.y}px)`,
};
}
let effectiveClassName = 'pulsating_dot';
if (this.props.onClick) {
effectiveClassName += ' pulsating_dot-clickable';
}
if (this.props.className) {
effectiveClassName = effectiveClassName + ' ' + this.props.className;
}
return (
<span
className={effectiveClassName}
onClick={this.props.onClick}
ref={this.props.targetRef}
style={{...customStyles}}
data-testid={'pulsating_dot'}
/>
);
}
}

Просмотреть файл

@@ -0,0 +1,37 @@
.pulsating_dot {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
&-clickable {
cursor: pointer;
}
&,
&::before,
&::after {
width: 12px;
height: 12px;
background-color: var(--online-indicator);
border-radius: 50%;
}
&::before,
&::after {
position: absolute;
top: 0;
left: 0;
display: block;
content: "";
}
&::after {
animation: pulse1 2s ease 0s infinite;
}
&::before {
animation: pulse2 2s ease 0s infinite;
}
}

Просмотреть файл

@@ -0,0 +1,81 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import styled, {keyframes} from 'styled-components';
const skeletonFade = keyframes`
0% {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
50% {
background-color: rgba(var(--center-channel-color-rgb), 0.16);
}
100% {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
`;
const BaseLoader = styled.div`
animation-duration: 1500ms;
animation-iteration-count: infinite;
animation-name: ${skeletonFade};
animation-timing-function: ease-in-out;
background-color: rgba(var(--center-channel-color-rgb), 0.08);
`;
export interface CircleSkeletonLoaderProps {
size: string | number;
}
/**
* CircleSkeletonLoader is a component that renders a filled circle with a loading animation.
* It is used to indicate that the content is loading.
* @param props.size - The size of the circle. When in number, it is treated as pixels.
* @example
* <CircleSkeletonLoader size={20}/>
* <CircleSkeletonLoader size="50%"/>
*/
export const CircleSkeletonLoader = styled(BaseLoader)<CircleSkeletonLoaderProps>`
display: block;
border-radius: 50%;
height: ${(props) => getCorrectSizeDimension(props.size)};
width: ${(props) => getCorrectSizeDimension(props.size)};
`;
export interface RectangleSkeletonLoaderProps {
height: string | number;
width?: string | number;
borderRadius?: number;
margin?: string;
flex?: string;
}
/**
* RectangleSkeletonLoader is a component that renders a filled rectangle with a loading animation.
* It is used to indicate that the content is loading.
* @param props.height - The height of the rectangle eg. 20, "20em", "20%". When in number, it is treated as pixels.
* @param props.width - The width of the rectangle eg. 30, '100%'. When in number, it is treated as pixels.
* @param props.borderRadius - The border radius of the rectangle eg. 4
* @param props.margin - The margin of the rectangle eg. '0 10px', '10px 0 0 10px'
* @param props.flex - The flex short hand of flex grow, shrink, basis of the rectangle, under flex parent css eg. '1 1 auto'
* @default
* width: 100% , borderRadius: 8px
* @example
* <RectangleSkeletonLoader height='100px' />
* <RectangleSkeletonLoader height={40} width={100} borderRadius={4} margin='0 10px 0 0' flex='1' />
*/
export const RectangleSkeletonLoader = styled(BaseLoader)<RectangleSkeletonLoaderProps>`
height: ${(props) => getCorrectSizeDimension(props.height)};
width: ${(props) => getCorrectSizeDimension(props.width, '100%')};
border-radius: ${(props) => props?.borderRadius ?? 8}px;
margin: ${(props) => props?.margin ?? null};
flex: ${(props) => props?.flex ?? null};
`;
function getCorrectSizeDimension(size: number | string | undefined, fallback: string | null = null) {
if (size) {
return (typeof size === 'string') ? size : `${size}px`;
}
return fallback;
}

Просмотреть файл

@@ -0,0 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export * from './tour_tip';
export * from './tour_tip_backdrop';

Просмотреть файл

@@ -0,0 +1,456 @@
.tour-tip {
display: flex;
&__box {
&.tippy-box {
padding: 18px 24px 24px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
background: var(--center-channel-bg);
border-radius: 4px;
color: var(--center-channel-color-rgb);
filter: drop-shadow(0 12px 32px rgba(0, 0, 0, 0.12));
.tippy-content {
padding: 0;
}
.tippy-arrow {
width: 12px;
height: 12px;
border-color: rgba(var(--center-channel-color-rgb), 0.16);
color: var(--center-channel-bg);
}
.tippy-arrow::before {
width: 12px;
height: 12px;
border-color: rgba(var(--center-channel-color-rgb), 0.16);
background: var(--center-channel-bg);
color: var(--center-channel-bg);
transform-origin: center;
}
// fix for https://mattermost.atlassian.net/browse/MM-41711. This covers the current placements we use for the channels and other tools tour
&[data-placement^=right] > .tippy-arrow {
transform: translate3d(0, 14px, 0) !important;
}
&[data-placement^=top] > .tippy-arrow {
transform: translate3d(14px, 0, 0) !important;
}
&[data-placement^=bottom] > .tippy-arrow {
transform: translate3d(14px, 0, 0) !important;
}
&[data-placement=bottom-end] > .tippy-arrow {
transform: translate3d(317px, 0, 0) !important;
}
}
}
&__pulsating-dot-ctr {
position: absolute;
z-index: 3;
width: 14px;
height: 14px;
cursor: pointer;
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=top] {
top: 0;
left: calc(50% - 6px);
transform: translate(0, 6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=top-start] {
top: 0;
left: 0;
transform: translate(6px, 6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=top-end] {
top: 0;
right: 0;
transform: translate(-6px, 6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=bottom] {
bottom: 0;
left: calc(50% - 6px);
transform: translate(0, -6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=bottom-start] {
bottom: 0;
left: 0;
transform: translate(6px, -6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=bottom-end] {
right: 0;
bottom: 0;
transform: translate(-6px, -6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=left] {
top: calc(50% - 6px);
left: 0;
transform: translate(6px, 0);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=left-start] {
top: 0;
left: 0;
transform: translate(6px, 6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=left-end] {
bottom: 0;
left: 0;
transform: translate(6px, -6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=right] {
top: calc(50% - 6px);
right: 0;
transform: translate(-6px, 0);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=right-start] {
top: 0;
right: 0;
transform: translate(-6px, 6px);
}
&__pulsating-dot-ctr[data-pulsating-dot-placement=right-end] {
right: 0;
bottom: 0;
transform: translate(-6px, -6px);
}
&__overlay {
position: fixed;
z-index: 999;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&__header {
display: flex;
align-items: center;
justify-content: flex-start;
&__title {
flex: none;
flex-grow: 1;
order: 0;
margin: 0;
font-family: inherit;
font-size: 1.4rem;
font-style: normal;
font-weight: 600;
line-height: 2rem;
}
&__close {
display: flex;
overflow: hidden;
width: 3.2rem;
height: 3.2rem;
align-items: center;
justify-content: center;
border: unset;
margin-right: -8px;
margin-left: 1.2rem;
background: transparent;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-weight: 600;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
}
&.active {
background: rgba(var(--center-channel-color-rgb), 0.08);
}
i {
font-size: 1.8rem;
line-height: 1.8rem;
::before {
margin-right: 0;
}
}
}
}
&__body {
display: flex;
flex-direction: column;
margin-top: 0.6rem;
p,
div {
margin: 0 0 0.8rem;
font-size: 1.4rem;
line-height: 2rem;
&:last-child {
margin-bottom: 0;
}
}
}
&__body:last-child {
margin-bottom: 0;
}
&__image {
display: flex;
align-items: center;
justify-content: center;
margin-top: 2.4rem;
img {
width: 100%;
height: 136px;
border-radius: 4px;
object-fit: cover;
}
}
&__btn-ctr {
display: flex;
flex-grow: 1;
justify-content: flex-end;
}
&__btn {
display: flex;
height: 3.2rem;
align-items: center;
padding: 10px 16px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
line-height: 12px;
}
&__confirm-btn {
background: var(--button-bg);
color: var(--button-color);
&:hover:not(.disabled) {
background:
linear-gradient(
0deg,
rgba(var(--center-channel-color-rgb), 0.16),
rgba(var(--center-channel-color-rgb), 0.16)
),
var(--button-bg);
}
&:active {
background:
linear-gradient(
0deg,
rgba(var(--center-channel-color-rgb), 0.32),
rgba(var(--center-channel-color-rgb), 0.32)
),
var(--button-bg);
}
&:focus {
box-shadow: inset 0 0 0 2px var(--sidebar-text-active-border);
}
.icon-chevron-right::before {
margin-right: -7px;
}
}
&__cancel-btn {
margin-right: 4px;
background: rgba(var(--button-bg-rgb), 0.08);
border-radius: 4px;
color: var(--button-bg);
text-decoration: none;
&:hover {
background: rgba(var(--button-bg-rgb), 0.04);
}
&:active {
background: rgba(var(--button-bg-rgb), 0.08);
}
&:focus {
box-shadow: inset 0 0 0 2px var(--sidebar-text-active-border);
}
.icon-chevron-left::before {
margin-left: -7px;
}
}
&__dot-ctr {
display: flex;
align-items: center;
justify-content: flex-start;
}
&__dot-ring {
position: relative;
display: flex;
width: 12px;
height: 12px;
align-items: center;
justify-content: center;
margin-right: 4px;
background: transparent;
border-radius: 50%;
&:last-child {
margin-right: 0;
}
}
&__dot-ring-active {
background: rgba(var(--button-bg-rgb), 0.16);
}
&__dot {
position: absolute;
top: 3px;
left: 3px;
width: 6px;
height: 6px;
background: rgba(var(--button-bg-rgb), 0.32);
border-radius: 6px;
&.active {
background: rgba(var(--button-bg-rgb), 1);
}
}
&__footer {
display: flex;
flex-direction: column;
margin-top: 2.4rem;
&-buttons {
display: flex;
align-items: center;
justify-content: start;
}
}
&__opt {
align-self: flex-end;
margin-top: 1.2rem;
font-size: 12px;
span {
opacity: 0.9;
}
}
&__backdrop {
position: absolute;
z-index: 999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
&--transparent {
background: none !important;
}
}
}
// adding important as temporary fix, will be removing tippy very soon (WIP)
.tippy-box[data-placement^=right] > .tippy-arrow::before {
top: -1px !important;
border-width: 1px 0 0 1px !important;
transform: rotate(-45deg) !important;
}
.tippy-box[data-placement^=left] > .tippy-arrow::before {
top: -1px !important;
border-width: 1px 1px 0 0 !important;
transform: rotate(45deg) !important;
}
.tippy-box[data-placement^=bottom] > .tippy-arrow::before {
left: 1px !important;
border-width: 1px 0 0 1px !important;
transform: rotate(45deg) !important;
}
.tippy-box[data-placement^=top] > .tippy-arrow::before {
left: 1px !important;
border-width: 0 0 1px 1px !important;
transform: rotate(-45deg) !important;
}
// this style is defined outside of the block scope because is intended to affect the tippy element
.tippy-blue-style {
background: var(--button-bg) !important;
color: var(--sidebar-text) !important;
.tippy-arrow {
border-color: var(--button-bg) !important;
color: var(--button-bg) !important;
&::before {
border-width: 0 !important;
border-color: var(--button-bg) !important;
border-left-color: initial;
background-color: var(--button-bg) !important;
transform-origin: unset !important;
}
}
.tour-tip__header {
font-weight: 600;
}
.icon-close {
color: var(--sidebar-text) !important;
}
// style buttons while in the blue style
.tour-tip {
&__btn {
background: var(--button-color);
color: var(--button-bg);
&:hover,
&:active,
&:focus {
background: var(--button-color);
color: var(--button-bg);
}
}
&__dot-ring {
.tour-tip__dot {
background: var(--offline-indicator);
}
}
&__dot-ring-active {
.active {
background: var(--button-color);
}
}
}
}

Просмотреть файл

@@ -0,0 +1,247 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef} from 'react';
import {FormattedMessage} from 'react-intl';
import Tippy from '@tippyjs/react';
import {Placement} from 'tippy.js';
import classNames from 'classnames';
import {PunchOutCoordsHeightAndWidth} from '../common/hooks/useMeasurePunchouts';
import 'tippy.js/dist/tippy.css';
import 'tippy.js/themes/light-border.css';
import 'tippy.js/animations/scale-subtle.css';
import 'tippy.js/animations/perspective-subtle.css';
import {PulsatingDot} from '../pulsating_dot';
import {TourTipBackdrop} from './tour_tip_backdrop';
import './tour_tip.scss';
export type TourTipEventSource = 'next' | 'prev' | 'dismiss' | 'jump' | 'skipped' | 'open' | 'punchOut'
type Props = {
show: boolean;
screen: JSX.Element;
title: JSX.Element;
step: number;
tourSteps?: Record<string, number>;
nextBtn?: JSX.Element;
prevBtn?: JSX.Element;
imageURL?: string;
singleTip?: boolean;
showOptOut?: boolean;
placement?: Placement;
pulsatingDotPlacement?: Omit<Placement, 'auto'| 'auto-end'>;
pulsatingDotTranslate?: {x: number; y: number};
offset?: [number, number];
width?: string | number;
zIndex?: number;
className?: string;
hideBackdrop?: boolean;
tippyBlueStyle?: boolean;
// if you don't want punchOut just assign null, keep null as hook may return null first than actual value
overlayPunchOut: PunchOutCoordsHeightAndWidth | null;
// if we want to interact with element visible via punchOut
interactivePunchOut?: boolean;
handleOpen?: (e: React.MouseEvent) => void;
handleNext?: (e: React.MouseEvent) => void;
handlePrevious?: (e: React.MouseEvent) => void;
handleJump?: (e: React.MouseEvent, jumpToStep: number) => void;
handleSkip?: (e: React.MouseEvent) => void;
handleDismiss?: (e: React.MouseEvent) => void;
handlePunchOut?: (e: React.MouseEvent) => void;
}
export const TourTip = ({
title,
screen,
imageURL,
overlayPunchOut,
singleTip,
step,
show,
interactivePunchOut,
tourSteps,
handleOpen,
handleDismiss,
handleNext,
handlePrevious,
handleSkip,
handleJump,
handlePunchOut,
pulsatingDotTranslate,
pulsatingDotPlacement,
nextBtn,
prevBtn,
className,
offset = [-18, 4],
placement = 'right-start',
showOptOut = true,
width = 352,
zIndex = 999,
hideBackdrop = false,
tippyBlueStyle = false,
}: Props) => {
const FIRST_STEP_INDEX = 0;
const triggerRef = useRef(null);
const onJump = (event: React.MouseEvent, jumpToStep: number) => {
if (handleJump) {
handleJump(event, jumpToStep);
}
};
// This needs to be changed if root-portal node isn't available to maybe body
const rootPortal = document.getElementById('root-portal');
const dots = [];
if (!singleTip && tourSteps) {
for (let dot = FIRST_STEP_INDEX; dot < (Object.values(tourSteps).length - 1); dot++) {
let className = 'tour-tip__dot';
let circularRing = 'tour-tip__dot-ring';
if (dot === step) {
className += ' active';
circularRing += ' tour-tip__dot-ring-active';
}
dots.push(
<div className={circularRing}>
<a
href='#'
key={'dotactive' + dot}
className={className}
data-screen={dot}
onClick={(e) => onJump(e, dot)}
/>
</div>,
);
}
}
const content = (
<>
<div
className='tour-tip__header'
data-testid={'current_tutorial_tip'}
>
<h4 className='tour-tip__header__title'>
{title}
</h4>
<button
className='tour-tip__header__close'
onClick={handleDismiss}
data-testid={'close_tutorial_tip'}
>
<i className='icon icon-close'/>
</button>
</div>
<div className='tour-tip__body'>
{screen}
</div>
{imageURL && (
<div className='tour-tip__image'>
<img
src={imageURL}
alt={'tutorial tour tip product image'}
/>
</div>
)}
{(nextBtn || prevBtn || showOptOut) && (<div className='tour-tip__footer'>
<div className='tour-tip__footer-buttons'>
<div className='tour-tip__dot-ctr'>{dots}</div>
<div className={'tour-tip__btn-ctr'}>
{step !== 0 && prevBtn && (
<button
id='tipPreviousButton'
className='tour-tip__btn tour-tip__cancel-btn'
onClick={handlePrevious}
>
{prevBtn}
</button>
)}
{nextBtn && (
<button
id='tipNextButton'
className='tour-tip__btn tour-tip__confirm-btn'
onClick={handleNext}
>
{nextBtn}
</button>
)}
</div>
</div>
{showOptOut && (
<div className='tour-tip__opt'>
<FormattedMessage
id='tutorial_tip.seen'
defaultMessage='Seen this before? '
/>
<a
href='#'
onClick={handleSkip}
>
<FormattedMessage
id='tutorial_tip.out'
defaultMessage='Opt out of these tips.'
/>
</a>
</div>
)}
</div>
)}
</>
);
return (
<>
<div
id='tipButton'
ref={triggerRef}
onClick={handleOpen}
className='tour-tip__pulsating-dot-ctr'
data-pulsating-dot-placement={pulsatingDotPlacement || 'right'}
style={{
transform: `translate(${pulsatingDotTranslate?.x}px, ${pulsatingDotTranslate?.y}px)`,
}}
>
<PulsatingDot/>
</div>
<TourTipBackdrop
show={show}
onDismiss={handleDismiss}
onPunchOut={handlePunchOut}
interactivePunchOut={interactivePunchOut}
overlayPunchOut={overlayPunchOut}
appendTo={rootPortal!}
transparent={hideBackdrop}
/>
{show && (
<Tippy
showOnCreate={show}
content={content}
animation='scale-subtle'
trigger='click'
duration={[250, 150]}
maxWidth={width}
aria={{content: 'labelledby'}}
allowHTML={true}
zIndex={zIndex}
reference={triggerRef}
interactive={true}
appendTo={rootPortal!}
offset={offset}
className={classNames(
'tour-tip__box',
className,
{'tippy-blue-style': tippyBlueStyle},
)}
placement={placement}
/>
)}
</>
);
};

Просмотреть файл

@@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import {PunchOutCoordsHeightAndWidth} from '../common/hooks/useMeasurePunchouts';
type Props = {
overlayPunchOut: PunchOutCoordsHeightAndWidth | null;
show: boolean;
interactivePunchOut?: boolean;
onDismiss?: (e: React.MouseEvent) => void;
onPunchOut?: (e: React.MouseEvent) => void;
appendTo: HTMLElement;
transparent?: boolean;
}
const TourTipRootPortal = ({children, show, element}: {children: React.ReactNode ; show: boolean; element: Element}) =>
(show ? ReactDOM.createPortal(
children,
element,
) : null);
export const TourTipBackdrop = ({
show,
overlayPunchOut,
interactivePunchOut,
onDismiss,
onPunchOut,
appendTo,
transparent,
}: Props) => {
const vertices = [];
if (overlayPunchOut) {
const {x, y, width, height} = overlayPunchOut;
// draw to top left of punch out
vertices.push('0% 0%');
vertices.push('0% 100%');
vertices.push('100% 100%');
vertices.push('100% 0%');
vertices.push(`${x} 0%`);
vertices.push(`${x} ${y}`);
// draw punch out
vertices.push(`calc(${x} + ${width}) ${y}`);
vertices.push(`calc(${x} + ${width}) calc(${y} + ${height})`);
vertices.push(`${x} calc(${y} + ${height})`);
vertices.push(`${x} ${y}`);
// close off punch out
vertices.push(`${x} 0%`);
vertices.push('0% 0%');
}
const backdrop = (
<div
onClick={onDismiss}
className={`tour-tip__backdrop ${transparent ? 'tour-tip__backdrop--transparent' : ''}`}
style={{
clipPath: vertices.length ? `polygon(${vertices.join(', ')})` : undefined,
}}
/>
);
const overlay = interactivePunchOut ? backdrop : (
<>
<div
className={'tour-tip__overlay'}
onClick={onPunchOut || onDismiss}
/>
{backdrop}
</>
);
return (
<TourTipRootPortal
show={show}
element={appendTo}
>
{overlay}
</TourTipRootPortal>
);
};

21
webapp/platform/components/tsconfig.json Обычный файл
Просмотреть файл

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"strictNullChecks": true,
"isolatedModules": true,
"noEmit": true,
"declaration": true,
"outDir": "dist",
"paths": {
"mattermost-redux/*": ["./node_modules/mattermost-redux/src/*"],
"@mattermost/types/*": ["./node_modules/@mattermost/types/src/*"]
}
}
}

51
webapp/platform/types/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,51 @@
# Mattermost Types
This package contains shared type definitions used by [the Mattermost web app](https://github.com/mattermost/mattermost-webapp) and related projects.
## Usage
For technologies that support [subpath exports](https://nodejs.org/api/packages.html#subpath-exports), such as Node.js, Webpack, and Babel, you can import these types directly from individual files.
```javascript
import {UserProfile} from '@mattermost/types/users';
```
For technologies that don't support that yet, you can add an alias in its package resolution settings to support that.
### TypeScript
In the `tsconfig.json`, you can use `compilerOptions.paths` to add that alias. This also requires a `compilerOptions.baseUrl` if you haven't set that already.
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@mattermost/types/*": ["node_modules/@mattermost/types/lib/*"]
}
}
}
```
### Jest
In your Jest config, you can use the `moduleNameMapper` field to add that alias.
```json
{
"moduleNameMapper": {
"^@mattermost/types/(.*)$": "<rootDir>/node_modules/@mattermost/types/lib/$1"
}
}
```
## Compilation and Packaging
As a member of Mattermost with write access to our NPM organization, you can build and publish this package by running the following commands:
```bash
npm run build --workspace=packages/types
npm publish --workspace=packages/types
```
Make sure to increment the version number in `package.json` first! You can add `-0`, `-1`, etc for pre-release versions.

35
webapp/platform/types/package.json Обычный файл
Просмотреть файл

@@ -0,0 +1,35 @@
{
"name": "@mattermost/types",
"version": "7.9.0",
"description": "Shared type definitions used by the Mattermost web app",
"keywords": [
"mattermost"
],
"homepage": "https://github.com/mattermost/mattermost-webapp/tree/master/packages/types#readme",
"license": "MIT",
"files": [
"lib"
],
"exports": {
"./*": "./lib/*.js"
},
"types": "./lib/*.d.ts",
"repository": {
"type": "git",
"url": "github:mattermost/mattermost-webapp",
"directory": "packages/types"
},
"peerDependencies": {
"typescript": "^4.3"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"scripts": {
"build": "tsc --build --verbose",
"run": "tsc --watch --preserveWatchOutput",
"clean": "rm -rf tsconfig.tsbuildinfo ./lib"
}
}

93
webapp/platform/types/src/admin.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Audit} from './audits';
import {Compliance} from './compliance';
import {AdminConfig, ClientLicense, EnvironmentConfig} from './config';
import {DataRetentionCustomPolicies} from './data_retention';
import {MixedUnlinkedGroupRedux} from './groups';
import {PluginRedux, PluginStatusRedux} from './plugins';
import {SamlCertificateStatus, SamlMetadataResponse} from './saml';
import {Team} from './teams';
import {UserAccessToken, UserProfile} from './users';
import {RelationOneToOne} from './utilities';
export enum LogLevelEnum {
SILLY = 'silly',
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
export type LogServerNames = string[];
export type LogLevels = LogLevelEnum[];
export type LogDateFrom = string; // epoch
export type LogDateTo = string; // epoch
export type LogObject = {
caller: string;
job_id: string;
level: LogLevelEnum;
msg: string;
timestamp: string;
worker: string;
}
export type LogFilter = {
serverNames: LogServerNames;
logLevels: LogLevels;
dateFrom: LogDateFrom;
dateTo: LogDateTo;
}
export type AdminState = {
logs: LogObject[];
audits: Record<string, Audit>;
config: Partial<AdminConfig>;
environmentConfig: Partial<EnvironmentConfig>;
complianceReports: Record<string, Compliance>;
ldapGroups: Record<string, MixedUnlinkedGroupRedux>;
ldapGroupsCount: number;
userAccessTokens: Record<string, UserAccessToken>;
clusterInfo: ClusterInfo[];
samlCertStatus?: SamlCertificateStatus;
analytics?: Record<string, number | AnalyticsRow[]>;
teamAnalytics?: RelationOneToOne<Team, Record<string, number | AnalyticsRow[]>>;
userAccessTokensByUser?: RelationOneToOne<UserProfile, Record<string, UserAccessToken>>;
plugins?: Record<string, PluginRedux>;
pluginStatuses?: Record<string, PluginStatusRedux>;
samlMetadataResponse?: SamlMetadataResponse;
dataRetentionCustomPolicies: DataRetentionCustomPolicies;
dataRetentionCustomPoliciesCount: number;
prevTrialLicense: ClientLicense;
};
export type ClusterInfo = {
id: string;
version: string;
config_hash: string;
ipaddress: string;
hostname: string;
};
export type AnalyticsRow = {
name: string;
value: number;
};
export type IndexedPluginAnalyticsRow = {
[key: string]: PluginAnalyticsRow;
}
export type PluginAnalyticsRow = {
id: string;
name: React.ReactNode;
icon: string;
value: number;
};
export type SchemaMigration = {
version: number;
name: string;
};

258
webapp/platform/types/src/apps.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,258 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ProductScope} from './products';
export enum Permission {
UserJoinedChannelNotification = 'user_joined_channel_notification',
ActAsBot = 'act_as_bot',
ActAsUser = 'act_as_user',
PermissionActAsAdmin = 'act_as_admin',
RemoteOAuth2 = 'remote_oauth2',
RemoteWebhooks = 'remote_webhooks',
}
export enum Locations {
PostMenu = '/post_menu',
ChannelHeader = '/channel_header',
Command = '/command',
InPost = '/in_post',
}
export type AppManifest = {
app_id: string;
version?: string;
homepage_url?: string;
icon?: string;
display_name: string;
description?: string;
requested_permissions?: Permission[];
requested_locations?: Locations[];
}
export type AppModalState = {
form: AppForm;
call: AppCallRequest;
}
export type AppCommandFormMap = { [location: string]: AppForm }
export type BindingsInfo = {
bindings: AppBinding[];
forms: AppCommandFormMap;
}
export type AppsState = {
main: BindingsInfo;
rhs: BindingsInfo;
pluginEnabled: boolean;
};
export type AppBinding = {
app_id: string;
location?: string;
supported_product_ids?: ProductScope;
icon?: string;
// Label is the (usually short) primary text to display at the location.
// - For LocationPostMenu is the menu item text.
// - For LocationChannelHeader is the dropdown text.
// - For LocationCommand is the name of the command
label: string;
// Hint is the secondary text to display
// - LocationPostMenu: not used
// - LocationChannelHeader: tooltip
// - LocationCommand: the "Hint" line
hint?: string;
// Description is the (optional) extended help text, used in modals and autocomplete
description?: string;
role_id?: string;
depends_on_team?: boolean;
depends_on_channel?: boolean;
depends_on_user?: boolean;
depends_on_post?: boolean;
// A Binding is either an action (makes a call), a Form, or is a
// "container" for other locations - i.e. menu sub-items or subcommands.
bindings?: AppBinding[];
form?: AppForm;
submit?: AppCall;
};
export type AppCallValues = {
[name: string]: any;
};
export type AppCall = {
path: string;
expand?: AppExpand;
state?: any;
};
export type AppCallRequest = AppCall & {
context: AppContext;
values?: AppCallValues;
raw_command?: string;
selected_field?: string;
query?: string;
};
export type AppCallResponseType = string;
export type AppCallResponse<Res = unknown> = {
type: AppCallResponseType;
text?: string;
data?: Res;
navigate_to_url?: string;
use_external_browser?: boolean;
call?: AppCall;
form?: AppForm;
app_metadata?: AppMetadataForClient;
};
export type AppMetadataForClient = {
bot_user_id: string;
bot_username: string;
}
export type AppContext = {
app_id: string;
location?: string;
acting_user_id?: string;
user_id?: string;
channel_id?: string;
team_id?: string;
post_id?: string;
root_id?: string;
props?: AppContextProps;
user_agent?: string;
track_as_submit?: boolean;
};
export type AppContextProps = {
[name: string]: string;
};
export type AppExpandLevel = ''
| 'none'
| 'summary'
| '+summary'
| 'all'
| '+all';
export type AppExpand = {
app?: AppExpandLevel;
acting_user?: AppExpandLevel;
channel?: AppExpandLevel;
config?: AppExpandLevel;
mentioned?: AppExpandLevel;
parent_post?: AppExpandLevel;
post?: AppExpandLevel;
root_post?: AppExpandLevel;
team?: AppExpandLevel;
user?: AppExpandLevel;
locale?: AppExpandLevel;
};
export type AppForm = {
title?: string;
header?: string;
footer?: string;
icon?: string;
submit_buttons?: string;
cancel_button?: boolean;
submit_on_cancel?: boolean;
fields?: AppField[];
// source is used in 2 cases:
// - if submit is not set, it is used to fetch the submittable form from
// the app.
// - if a select field change triggers a refresh, the form is refreshed
// from source.
source?: AppCall;
// submit is called when one of the submit buttons is pressed, or the
// command is executed.
submit?: AppCall;
depends_on?: string[];
};
export type AppFormValue = string | AppSelectOption | boolean | null;
export type AppFormValues = { [name: string]: AppFormValue };
export type AppSelectOption = {
label: string;
value: string;
icon_data?: string;
};
export type AppFieldType = string;
// This should go in mattermost-redux
export type AppField = {
// Name is the name of the JSON field to use.
name: string;
type: AppFieldType;
is_required?: boolean;
readonly?: boolean;
// Present (default) value of the field
value?: AppFormValue;
description?: string;
label?: string;
hint?: string;
position?: number;
modal_label?: string;
// Select props
refresh?: boolean;
options?: AppSelectOption[];
multiselect?: boolean;
lookup?: AppCall;
// Text props
subtype?: string;
min_length?: number;
max_length?: number;
};
export type AutocompleteSuggestion = {
suggestion: string;
complete?: string;
description?: string;
hint?: string;
iconData?: string;
}
export type AutocompleteSuggestionWithComplete = AutocompleteSuggestion & {
complete: string;
}
export type AutocompleteElement = AppField;
export type AutocompleteStaticSelect = AutocompleteElement & {
options: AppSelectOption[];
};
export type AutocompleteDynamicSelect = AutocompleteElement;
export type AutocompleteUserSelect = AutocompleteElement;
export type AutocompleteChannelSelect = AutocompleteElement;
export type FormResponseData = {
errors?: {
[field: string]: string;
};
}
export type AppLookupResponse = {
items: AppSelectOption[];
}

12
webapp/platform/types/src/audits.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Audit = {
id: string;
create_at: number;
user_id: string;
action: string;
extra_info: string;
ip_address: string;
session_id: string;
}

20
webapp/platform/types/src/autocomplete.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserProfile} from './users';
export type UserAutocomplete = {
users: UserProfile[];
// out_of_channel contains users that aren't in the given channel. It's only populated when autocompleting users in
// a given channel ID.
out_of_channel?: UserProfile[];
};
export type AutocompleteSuggestion = {
Complete: string;
Suggestion: string;
Hint: string;
Description: string;
IconData: string;
};

49
webapp/platform/types/src/boards.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const BoardTypeOpen = 'O';
const BoardTypePrivate = 'P';
const boardTypes = [BoardTypeOpen, BoardTypePrivate];
type BoardTypes = typeof boardTypes[number];
type PropertyTypeEnum = 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'person' | 'file' | 'checkbox' | 'url' | 'email' | 'phone' | 'createdTime' | 'createdBy' | 'updatedTime' | 'updatedBy' | 'unknown';
interface IPropertyOption {
id: string;
value: string;
color: string;
}
// A template for card properties attached to a board
interface IPropertyTemplate {
id: string;
name: string;
type: PropertyTypeEnum;
options: IPropertyOption[];
}
export declare type Board = {
id: string;
teamId: string;
channelId?: string;
createdBy: string;
modifiedBy: string;
type: BoardTypes;
minimumRole: string;
title: string;
description: string;
icon?: string;
showDescription: boolean;
isTemplate: boolean;
templateVersion: number;
properties: Record<string, string | string[]>;
cardProperties: IPropertyTemplate[];
createAt: number;
updateAt: number;
deleteAt: number;
}
export declare type CreateBoardResponse = {
boards: Board[];
}

20
webapp/platform/types/src/bots.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Bot = {
user_id: string ;
username: string ;
display_name?: string ;
description?: string ;
owner_id: string ;
create_at: number ;
update_at: number ;
delete_at: number ;
}
// BotPatch is a description of what fields to update on an existing bot.
export type BotPatch = {
username: string;
display_name: string;
description: string;
}

Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Channel} from './channels';
import {Team} from './teams';
import {UserProfile} from './users';
import {IDMappedObjects, RelationOneToOne} from './utilities';
export type ChannelCategoryType = 'favorites' | 'channels' | 'direct_messages' | 'custom';
export enum CategorySorting {
Alphabetical = 'alpha',
Default = '', // behaves the same as manual
Recency = 'recent',
Manual = 'manual',
}
export type ChannelCategory = {
id: string;
user_id: UserProfile['id'];
team_id: Team['id'];
type: ChannelCategoryType;
display_name: string;
sorting: CategorySorting;
channel_ids: Array<Channel['id']>;
muted: boolean;
collapsed: boolean;
};
export type OrderedChannelCategories = {
categories: ChannelCategory[];
order: string[];
};
export type ChannelCategoriesState = {
byId: IDMappedObjects<ChannelCategory>;
orderByTeam: RelationOneToOne<Team, Array<ChannelCategory['id']>>;
};

211
webapp/platform/types/src/channels.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,211 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IDMappedObjects, RelationOneToMany, RelationOneToOne} from './utilities';
import {Team} from './teams';
// e.g.
// **O**pen channel,
// **P**rivate channel,
// **D**irect message to one other,
// **G**roup direct message to 2+ others
export type ChannelType = 'O' | 'P' | 'D' | 'G' | 'threads';
export type ChannelStats = {
channel_id: string;
member_count: number;
guest_count: number;
pinnedpost_count: number;
files_count: number;
};
export type ChannelNotifyProps = {
desktop: 'default' | 'all' | 'mention' | 'none';
email: 'default' | 'all' | 'mention' | 'none';
mark_unread: 'all' | 'mention';
push: 'default' | 'all' | 'mention' | 'none';
ignore_channel_mentions: 'default' | 'off' | 'on';
};
export type Channel = {
id: string;
create_at: number;
update_at: number;
delete_at: number;
team_id: string;
type: ChannelType;
display_name: string;
name: string;
header: string;
purpose: string;
last_post_at: number;
last_root_post_at: number;
creator_id: string;
scheme_id: string;
teammate_id?: string;
status?: string;
group_constrained: boolean;
shared?: boolean;
props?: Record<string, any>;
policy_id?: string | null;
};
export type ServerChannel = Channel & {
/**
* The total number of posts in this channel, not including join/leave messages
*
* @remarks This field will be moved to a {@link ChannelMessageCount} object when this channel is stored in Redux.
*/
total_msg_count: number;
/**
* The number of root posts in this channel, not including join/leave messages
*
* @remarks This field will be moved to a {@link ChannelMessageCount} object when this channel is stored in Redux.
*/
total_msg_count_root: number;
}
export type ChannelMessageCount = {
/** The total number of posts in this channel, not including join/leave messages */
total: number;
/** The number of root posts in this channel, not including join/leave messages */
root: number;
}
export type ChannelWithTeamData = Channel & {
team_display_name: string;
team_name: string;
team_update_at: number;
};
export type ChannelsWithTotalCount = {
channels: ChannelWithTeamData[];
total_count: number;
};
export type ChannelMembership = {
channel_id: string;
user_id: string;
roles: string;
last_viewed_at: number;
/** The number of posts in this channel which have been read by the user */
msg_count: number;
/** The number of root posts in this channel which have been read by the user */
msg_count_root: number;
/** The number of unread mentions in this channel */
mention_count: number;
/** The number of unread mentions in root posts in this channel */
mention_count_root: number;
/** The number of unread urgent mentions in this channel */
urgent_mention_count: number;
notify_props: Partial<ChannelNotifyProps>;
last_update_at: number;
scheme_user: boolean;
scheme_admin: boolean;
post_root_id?: string;
};
export type ChannelUnread = {
channel_id: string;
user_id: string;
team_id: string;
/** The number of posts which have been read by the user */
msg_count: number;
/** The number of root posts which have been read by the user */
msg_count_root: number;
/** The number of unread mentions in this channel */
mention_count: number;
/** The number of unread urgent mentions in this channel */
urgent_mention_count: number;
/** The number of unread mentions in root posts in this channel */
mention_count_root: number;
last_viewed_at: number;
deltaMsgs: number;
};
export type ChannelsState = {
currentChannelId: string;
channels: IDMappedObjects<Channel>;
channelsInTeam: RelationOneToMany<Team, Channel>;
myMembers: RelationOneToOne<Channel, ChannelMembership>;
roles: RelationOneToOne<Channel, Set<string>>;
membersInChannel: RelationOneToOne<Channel, Record<string, ChannelMembership>>;
stats: RelationOneToOne<Channel, ChannelStats>;
groupsAssociatedToChannel: any;
totalCount: number;
manuallyUnread: RelationOneToOne<Channel, boolean>;
channelModerations: RelationOneToOne<Channel, ChannelModeration[]>;
channelMemberCountsByGroup: RelationOneToOne<Channel, ChannelMemberCountsByGroup>;
messageCounts: RelationOneToOne<Channel, ChannelMessageCount>;
};
export type ChannelModeration = {
name: string;
roles: {
guests?: {
value: boolean;
enabled: boolean;
};
members: {
value: boolean;
enabled: boolean;
};
admins: {
value: boolean;
enabled: boolean;
};
};
};
export type ChannelModerationPatch = {
name: string;
roles: {
guests?: boolean;
members?: boolean;
};
};
export type ChannelMemberCountByGroup = {
group_id: string;
channel_member_count: number;
channel_member_timezones_count: number;
};
export type ChannelMemberCountsByGroup = Record<string, ChannelMemberCountByGroup>;
export type ChannelViewResponse = {
status: string;
last_viewed_at_times: RelationOneToOne<Channel, number>;
};
export type ChannelSearchOpts = {
nonAdminSearch?: boolean;
exclude_default_channels?: boolean;
not_associated_to_group?: string;
team_ids?: string[];
group_constrained?: boolean;
exclude_group_constrained?: boolean;
public?: boolean;
private?: boolean;
include_deleted?: boolean;
include_search_by_id?: boolean;
deleted?: boolean;
page?: number;
per_page?: number;
};

38
webapp/platform/types/src/client4.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export enum LogLevel {
Error = 'ERROR',
Warning = 'WARNING',
Info = 'INFO',
Debug = 'DEBUG',
}
export type ClientResponse<T> = {
response: Response;
headers: Map<string, string>;
data: T;
};
export type Options = {
headers?: { [x: string]: string };
method?: string;
url?: string;
credentials?: 'omit' | 'same-origin' | 'include';
body?: any;
};
export type StatusOK = {
status: 'OK';
};
export type FetchPaginatedThreadOptions = {
fetchThreads?: boolean;
collapsedThreads?: boolean;
collapsedThreadsExtended?: boolean;
direction?: 'up'|'down';
fetchAll?: boolean;
perPage?: number;
fromCreateAt?: number;
fromPost?: string;
}

237
webapp/platform/types/src/cloud.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,237 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ValueOf} from './utilities';
export type CloudState = {
subscription?: Subscription;
products?: Record<string, Product>;
customer?: CloudCustomer;
invoices?: Record<string, Invoice>;
subscriptionStats?: LicenseSelfServeStatusReducer;
limits: {
limitsLoaded: boolean;
limits: Limits;
};
errors: {
subscription?: true;
products?: true;
customer?: true;
invoices?: true;
limits?: true;
trueUpReview?: true;
};
selfHostedSignup: {
progress: ValueOf<typeof SelfHostedSignupProgress>;
};
}
export type Subscription = {
id: string;
customer_id: string;
product_id: string;
add_ons: string[];
start_at: number;
end_at: number;
create_at: number;
seats: number;
last_invoice?: Invoice;
upcoming_invoice?: Invoice;
trial_end_at: number;
is_free_trial: string;
delinquent_since?: number;
compliance_blocked?: string;
}
export type Product = {
id: string;
name: string;
description: string;
price_per_seat: number;
add_ons: AddOn[];
product_family: string;
sku: string;
billing_scheme: string;
recurring_interval: string;
cross_sells_to: string;
};
export type AddOn = {
id: string;
name: string;
display_name: string;
price_per_seat: number;
};
export const TypePurchases = {
firstSelfHostLicensePurchase: 'first_purchase',
renewalSelfHost: 'renewal_self',
monthlySubscription: 'monthly_subscription',
annualSubscription: 'annual_subscription',
} as const;
export const SelfHostedSignupProgress = {
START: 'START',
CREATED_CUSTOMER: 'CREATED_CUSTOMER',
CREATED_INTENT: 'CREATED_INTENT',
CONFIRMED_INTENT: 'CONFIRMED_INTENT',
CREATED_SUBSCRIPTION: 'CREATED_SUBSCRIPTION',
PAID: 'PAID',
CREATED_LICENSE: 'CREATED_LICENSE',
} as const;
export type MetadataGatherWireTransferKeys = `${ValueOf<typeof TypePurchases>}_alt_payment_method`
export type CustomerMetadataGatherWireTransfer = Partial<Record<MetadataGatherWireTransferKeys, string>>
// Customer model represents a customer on the system.
export type CloudCustomer = {
id: string;
creator_id: string;
create_at: number;
email: string;
name: string;
num_employees: number;
contact_first_name: string;
contact_last_name: string;
billing_address: Address;
company_address: Address;
payment_method: PaymentMethod;
} & CustomerMetadataGatherWireTransfer
export type LicenseSelfServeStatus = {
is_expandable?: boolean;
is_renewable?: boolean;
}
type RequestState = 'IDLE' | 'LOADING' | 'ERROR' | 'OK'
export interface LicenseSelfServeStatusReducer extends LicenseSelfServeStatus {
getRequestState: RequestState;
}
// CustomerPatch model represents a customer patch on the system.
export type CloudCustomerPatch = {
email?: string;
name?: string;
num_employees?: number;
contact_first_name?: string;
contact_last_name?: string;
} & CustomerMetadataGatherWireTransfer
// Address model represents a customer's address.
export type Address = {
city: string;
country: string;
line1: string;
line2: string;
postal_code: string;
state: string;
}
// PaymentMethod represents methods of payment for a customer.
export type PaymentMethod = {
type: string;
last_four: string;
exp_month: number;
exp_year: number;
card_brand: string;
name: string;
}
export type NotifyAdminRequest = {
trial_notification: boolean;
required_plan: string;
required_feature: string;
}
// Invoice model represents a invoice on the system.
export type Invoice = {
id: string;
number: string;
create_at: number;
total: number;
tax: number;
status: string;
description: string;
period_start: number;
period_end: number;
subscription_id: string;
line_items: InvoiceLineItem[];
current_product_name: string;
}
// actual string values come from customer-web-server and should be kept in sync with values seen there
export const InvoiceLineItemType = {
Full: 'full',
Partial: 'partial',
OnPremise: 'onpremise',
Metered: 'metered',
} as const;
// InvoiceLineItem model represents a invoice lineitem tied to an invoice.
export type InvoiceLineItem = {
price_id: string;
total: number;
quantity: number;
price_per_unit: number;
description: string;
type: typeof InvoiceLineItemType[keyof typeof InvoiceLineItemType];
metadata: Record<string, string>;
}
export type Limits = {
messages?: {
history?: number;
};
files?: {
total_storage?: number;
};
teams?: {
active?: number;
};
}
export interface CloudUsage {
files: {
totalStorage: number;
totalStorageLoaded: boolean;
};
messages: {
history: number;
historyLoaded: boolean;
};
teams: TeamsUsage;
}
export type TeamsUsage = {
active: number;
cloudArchived: number;
teamsLoaded: boolean;
}
export type ValidBusinessEmail = {
is_valid: boolean;
}
export interface CreateSubscriptionRequest {
product_id: string;
add_ons: string[];
seats: number;
internal_purchase_order?: string;
}
export const areShippingDetailsValid = (address: Address | null | undefined): boolean => {
if (!address) {
return false;
}
return Boolean(address.city && address.country && address.line1 && address.postal_code && address.state);
};
export type Feedback = {
reason: string;
comments: string;
}
export type WorkspaceDeletionRequest = {
subscription_id: string;
delete_feedback: Feedback;
}

16
webapp/platform/types/src/compliance.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Compliance = {
id: string;
create_at: number;
user_id: string;
status: string;
count: number;
desc: string;
type: string;
start_at: number;
end_at: number;
keywords: string;
emails: string;
};

932
webapp/platform/types/src/config.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,932 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
export type ClientConfig = {
AboutLink: string;
AllowBannerDismissal: string;
AllowCustomThemes: string;
AllowSyncedDrafts: string;
AllowedThemes: string;
AndroidAppDownloadLink: string;
AndroidLatestVersion: string;
AndroidMinVersion: string;
AppDownloadLink: string;
AsymmetricSigningPublicKey: string;
AvailableLocales: string;
BannerColor: string;
BannerText: string;
BannerTextColor: string;
BuildBoards: string;
BuildDate: string;
BuildEnterpriseReady: string;
BuildHash: string;
BuildHashBoards: string;
BuildHashEnterprise: string;
BuildHashPlaybooks: string;
BuildNumber: string;
CollapsedThreads: CollapsedThreads;
CustomBrandText: string;
CustomDescriptionText: string;
CustomTermsOfServiceId: string;
CustomTermsOfServiceReAcceptancePeriod: string;
CustomUrlSchemes: string;
CWSURL: string;
DataRetentionEnableFileDeletion: string;
DataRetentionEnableMessageDeletion: string;
DataRetentionFileRetentionDays: string;
DataRetentionMessageRetentionDays: string;
DefaultClientLocale: string;
DefaultTheme: string;
DiagnosticId: string;
DiagnosticsEnabled: string;
EmailLoginButtonBorderColor: string;
EmailLoginButtonColor: string;
EmailLoginButtonTextColor: string;
EmailNotificationContentsType: string;
EnableAskCommunityLink: string;
EnableBanner: string;
EnableBotAccountCreation: string;
EnableChannelViewedMessages: string;
EnableClientPerformanceDebugging: string;
EnableCluster: string;
EnableCommands: string;
EnableCompliance: string;
EnableConfirmNotificationsToChannel: string;
EnableCustomBrand: string;
EnableCustomEmoji: string;
EnableCustomGroups: string;
EnableCustomUserStatuses: string;
EnableLastActiveTime: string;
EnableTimedDND: string;
EnableCustomTermsOfService: string;
EnableDeveloper: string;
EnableDiagnostics: string;
EnableEmailBatching: string;
EnableEmailInvitations: string;
EnableEmojiPicker: string;
EnableFileAttachments: string;
EnableFile: string;
EnableGifPicker: string;
EnableGuestAccounts: string;
EnableIncomingWebhooks: string;
EnableLatex: string;
EnableInlineLatex: string;
EnableLdap: string;
EnableLinkPreviews: string;
EnableMarketplace: string;
EnableMetrics: string;
EnableMobileFileDownload: string;
EnableMobileFileUpload: string;
EnableMultifactorAuthentication: string;
EnableOAuthServiceProvider: string;
EnableOpenServer: string;
EnableOutgoingWebhooks: string;
EnablePostIconOverride: string;
EnablePostUsernameOverride: string;
EnablePreviewFeatures: string;
EnablePreviewModeBanner: string;
EnablePublicLink: string;
EnableReliableWebSockets: string;
EnableSaml: string;
EnableSignInWithEmail: string;
EnableSignInWithUsername: string;
EnableSignUpWithEmail: string;
EnableSignUpWithGitLab: string;
EnableSignUpWithGoogle: string;
EnableSignUpWithOffice365: string;
EnableSignUpWithOpenId: string;
EnableSVGs: string;
EnableTesting: string;
EnableThemeSelection: string;
EnableTutorial: string;
EnableOnboardingFlow: string;
EnableUserAccessTokens: string;
EnableUserCreation: string;
EnableUserDeactivation: string;
EnableUserTypingMessages: string;
EnforceMultifactorAuthentication: string;
ExperimentalClientSideCertCheck: string;
ExperimentalClientSideCertEnable: string;
ExperimentalEnableAuthenticationTransfer: string;
ExperimentalEnableAutomaticReplies: string;
ExperimentalEnableDefaultChannelLeaveJoinMessages: string;
ExperimentalEnablePostMetadata: string;
ExperimentalGroupUnreadChannels: string;
ExperimentalPrimaryTeam: string;
ExperimentalTimezone: string;
ExperimentalViewArchivedChannels: string;
FileLevel: string;
FeatureFlagAppsEnabled: string;
FeatureFlagBoardsProduct: string;
FeatureFlagCallsEnabled: string;
FeatureFlagGraphQL: string;
GfycatAPIKey: string;
GfycatAPISecret: string;
GoogleDeveloperKey: string;
GuestAccountsEnforceMultifactorAuthentication: string;
HasImageProxy: string;
HelpLink: string;
IosAppDownloadLink: string;
IosLatestVersion: string;
IosMinVersion: string;
InsightsEnabled: string;
InstallationDate: string;
IsDefaultMarketplace: string;
LdapFirstNameAttributeSet: string;
LdapLastNameAttributeSet: string;
LdapLoginButtonBorderColor: string;
LdapLoginButtonColor: string;
LdapLoginButtonTextColor: string;
LdapLoginFieldName: string;
LdapNicknameAttributeSet: string;
LdapPositionAttributeSet: string;
LdapPictureAttributeSet: string;
LockTeammateNameDisplay: string;
ManagedResourcePaths: string;
MaxFileSize: string;
MaxPostSize: string;
MaxNotificationsPerChannel: string;
MinimumHashtagLength: string;
NoAccounts: string;
GitLabButtonText: string;
GitLabButtonColor: string;
OpenIdButtonText: string;
OpenIdButtonColor: string;
PasswordMinimumLength: string;
PasswordRequireLowercase: string;
PasswordRequireNumber: string;
PasswordRequireSymbol: string;
PasswordRequireUppercase: string;
PluginsEnabled: string;
PostEditTimeLimit: string;
PrivacyPolicyLink: string;
ReportAProblemLink: string;
RequireEmailVerification: string;
RestrictDirectMessage: string;
RunJobs: string;
SamlFirstNameAttributeSet: string;
SamlLastNameAttributeSet: string;
SamlLoginButtonBorderColor: string;
SamlLoginButtonColor: string;
SamlLoginButtonText: string;
SamlLoginButtonTextColor: string;
SamlNicknameAttributeSet: string;
SamlPositionAttributeSet: string;
SchemaVersion: string;
SendEmailNotifications: string;
SendPushNotifications: string;
ShowEmailAddress: string;
SiteName: string;
SiteURL: string;
SQLDriverName: string;
SupportEmail: string;
TelemetryId: string;
TeammateNameDisplay: string;
TermsOfServiceLink: string;
TimeBetweenUserTypingUpdatesMilliseconds: string;
UpgradedFromTE: string;
Version: string;
WebsocketPort: string;
WebsocketSecurePort: string;
WebsocketURL: string;
ExperimentalSharedChannels: string;
EnableAppBar: string;
EnableComplianceExport: string;
PostPriority: string;
ReduceOnBoardingTaskList: string;
PostAcknowledgements: string;
};
export type License = {
id: string;
issued_at: number;
starts_at: number;
expires_at: string;
customer: LicenseCustomer;
features: LicenseFeatures;
sku_name: string;
short_sku_name: string;
};
export type LicenseCustomer = {
id: string;
name: string;
email: string;
company: string;
};
export type LicenseFeatures = {
users?: number;
ldap?: boolean;
ldap_groups?: boolean;
mfa?: boolean;
google_oauth?: boolean;
office365_oauth?: boolean;
compliance?: boolean;
cluster?: boolean;
metrics?: boolean;
mhpns?: boolean;
saml?: boolean;
elastic_search?: boolean;
announcement?: boolean;
theme_management?: boolean;
email_notification_contents?: boolean;
data_retention?: boolean;
message_export?: boolean;
custom_permissions_schemes?: boolean;
custom_terms_of_service?: boolean;
guest_accounts?: boolean;
guest_accounts_permissions?: boolean;
id_loaded?: boolean;
lock_teammate_name_display?: boolean;
cloud?: boolean;
future_features?: boolean;
};
export type ClientLicense = Record<string, string>;
export type RequestLicenseBody = {
users: number;
terms_accepted: boolean;
receive_emails_accepted: boolean;
}
export type DataRetentionPolicy = {
message_deletion_enabled: boolean;
file_deletion_enabled: boolean;
message_retention_cutoff: number;
file_retention_cutoff: number;
};
export type ServiceSettings = {
SiteURL: string;
WebsocketURL: string;
LicenseFileLocation: string;
ListenAddress: string;
ConnectionSecurity: string;
TLSCertFile: string;
TLSKeyFile: string;
TLSMinVer: string;
TLSStrictTransport: boolean;
TLSStrictTransportMaxAge: number;
TLSOverwriteCiphers: string[];
UseLetsEncrypt: boolean;
LetsEncryptCertificateCacheFile: string;
Forward80To443: boolean;
TrustedProxyIPHeader: string[];
ReadTimeout: number;
WriteTimeout: number;
IdleTimeout: number;
MaximumLoginAttempts: number;
GoroutineHealthThreshold: number;
GoogleDeveloperKey: string;
EnableOAuthServiceProvider: boolean;
EnableIncomingWebhooks: boolean;
EnableOutgoingWebhooks: boolean;
EnableCommands: boolean;
EnablePostUsernameOverride: boolean;
EnablePostIconOverride: boolean;
EnableLinkPreviews: boolean;
EnablePermalinkPreviews: boolean;
RestrictLinkPreviews: string;
EnableTesting: boolean;
EnableDeveloper: boolean;
DeveloperFlags: string;
EnableClientPerformanceDebugging: boolean;
EnableOpenTracing: boolean;
EnableSecurityFixAlert: boolean;
EnableInsecureOutgoingConnections: boolean;
AllowedUntrustedInternalConnections: string;
EnableMultifactorAuthentication: boolean;
EnforceMultifactorAuthentication: boolean;
EnableUserAccessTokens: boolean;
AllowCorsFrom: string;
CorsExposedHeaders: string;
CorsAllowCredentials: boolean;
CorsDebug: boolean;
AllowCookiesForSubdomains: boolean;
ExtendSessionLengthWithActivity: boolean;
SessionLengthWebInDays: number;
SessionLengthWebInHours: number;
SessionLengthMobileInDays: number;
SessionLengthMobileInHours: number;
SessionLengthSSOInDays: number;
SessionLengthSSOInHours: number;
SessionCacheInMinutes: number;
SessionIdleTimeoutInMinutes: number;
WebsocketSecurePort: number;
WebsocketPort: number;
WebserverMode: string;
EnableCustomEmoji: boolean;
EnableEmojiPicker: boolean;
EnableGifPicker: boolean;
GfycatAPIKey: string;
GfycatAPISecret: string;
PostEditTimeLimit: number;
TimeBetweenUserTypingUpdatesMilliseconds: number;
EnablePostSearch: boolean;
EnableFileSearch: boolean;
MinimumHashtagLength: number;
EnableUserTypingMessages: boolean;
EnableChannelViewedMessages: boolean;
EnableUserStatuses: boolean;
ExperimentalEnableAuthenticationTransfer: boolean;
ClusterLogTimeoutMilliseconds: number;
EnablePreviewFeatures: boolean;
EnableTutorial: boolean;
EnableOnboardingFlow: boolean;
ExperimentalEnableDefaultChannelLeaveJoinMessages: boolean;
ExperimentalGroupUnreadChannels: string;
EnableAPITeamDeletion: boolean;
EnableAPITriggerAdminNotifications: boolean;
EnableAPIUserDeletion: boolean;
ExperimentalEnableHardenedMode: boolean;
ExperimentalStrictCSRFEnforcement: boolean;
EnableEmailInvitations: boolean;
DisableBotsWhenOwnerIsDeactivated: boolean;
EnableBotAccountCreation: boolean;
EnableSVGs: boolean;
EnableLatex: boolean;
EnableInlineLatex: boolean;
EnableLocalMode: boolean;
LocalModeSocketLocation: string;
CollapsedThreads: CollapsedThreads;
ThreadAutoFollow: boolean;
PostPriority: boolean;
EnableAPIChannelDeletion: boolean;
EnableAWSMetering: boolean;
SplitKey: string;
FeatureFlagSyncIntervalSeconds: number;
DebugSplit: boolean;
ManagedResourcePaths: string;
EnableCustomGroups: boolean;
SelfHostedPurchase: boolean;
AllowSyncedDrafts: boolean;
SelfHostedExpansion: boolean;
};
export type TeamSettings = {
SiteName: string;
MaxUsersPerTeam: number;
EnableCustomUserStatuses: boolean;
EnableUserCreation: boolean;
EnableOpenServer: boolean;
EnableUserDeactivation: boolean;
RestrictCreationToDomains: string;
EnableCustomBrand: boolean;
CustomBrandText: string;
CustomDescriptionText: string;
RestrictDirectMessage: string;
UserStatusAwayTimeout: number;
MaxChannelsPerTeam: number;
MaxNotificationsPerChannel: number;
EnableConfirmNotificationsToChannel: boolean;
TeammateNameDisplay: string;
ExperimentalViewArchivedChannels: boolean;
ExperimentalEnableAutomaticReplies: boolean;
LockTeammateNameDisplay: boolean;
ExperimentalPrimaryTeam: string;
ExperimentalDefaultChannels: string[];
EnableLastActiveTime: boolean;
};
export type ClientRequirements = {
AndroidLatestVersion: string;
AndroidMinVersion: string;
IosLatestVersion: string;
IosMinVersion: string;
};
export type SqlSettings = {
DriverName: string;
DataSource: string;
DataSourceReplicas: string[];
DataSourceSearchReplicas: string[];
MaxIdleConns: number;
ConnMaxLifetimeMilliseconds: number;
ConnMaxIdleTimeMilliseconds: number;
MaxOpenConns: number;
Trace: boolean;
AtRestEncryptKey: string;
QueryTimeout: number;
DisableDatabaseSearch: boolean;
MigrationsStatementTimeoutSeconds: number;
ReplicaLagSettings: ReplicaLagSetting[];
};
export type LogSettings = {
EnableConsole: boolean;
ConsoleLevel: string;
ConsoleJson: boolean;
EnableColor: boolean;
EnableFile: boolean;
FileLevel: string;
FileJson: boolean;
FileLocation: string;
EnableWebhookDebugging: boolean;
EnableDiagnostics: boolean;
VerboseDiagnostics: boolean;
EnableSentry: boolean;
AdvancedLoggingConfig: string;
};
export type ExperimentalAuditSettings = {
FileEnabled: boolean;
FileName: string;
FileMaxSizeMB: number;
FileMaxAgeDays: number;
FileMaxBackups: number;
FileCompress: boolean;
FileMaxQueueSize: number;
AdvancedLoggingConfig: string;
};
export type NotificationLogSettings = {
EnableConsole: boolean;
ConsoleLevel: string;
ConsoleJson: boolean;
EnableColor: boolean;
EnableFile: boolean;
FileLevel: string;
FileJson: boolean;
FileLocation: string;
AdvancedLoggingConfig: string;
};
export type PasswordSettings = {
MinimumLength: number;
Lowercase: boolean;
Number: boolean;
Uppercase: boolean;
Symbol: boolean;
};
export type FileSettings = {
EnableFileAttachments: boolean;
EnableMobileUpload: boolean;
EnableMobileDownload: boolean;
MaxFileSize: number;
MaxImageResolution: number;
MaxImageDecoderConcurrency: number;
DriverName: string;
Directory: string;
EnablePublicLink: boolean;
ExtractContent: boolean;
ArchiveRecursion: boolean;
PublicLinkSalt: string;
InitialFont: string;
AmazonS3AccessKeyId: string;
AmazonS3SecretAccessKey: string;
AmazonS3Bucket: string;
AmazonS3PathPrefix: string;
AmazonS3Region: string;
AmazonS3Endpoint: string;
AmazonS3SSL: boolean;
AmazonS3SignV2: boolean;
AmazonS3SSE: boolean;
AmazonS3Trace: boolean;
AmazonS3RequestTimeoutMilliseconds: number;
};
export type EmailSettings = {
EnableSignUpWithEmail: boolean;
EnableSignInWithEmail: boolean;
EnableSignInWithUsername: boolean;
SendEmailNotifications: boolean;
UseChannelInEmailNotifications: boolean;
RequireEmailVerification: boolean;
FeedbackName: string;
FeedbackEmail: string;
ReplyToAddress: string;
FeedbackOrganization: string;
EnableSMTPAuth: boolean;
SMTPUsername: string;
SMTPPassword: string;
SMTPServer: string;
SMTPPort: string;
SMTPServerTimeout: number;
ConnectionSecurity: string;
SendPushNotifications: boolean;
PushNotificationServer: string;
PushNotificationContents: string;
PushNotificationBuffer: number;
EnableEmailBatching: boolean;
EmailBatchingBufferSize: number;
EmailBatchingInterval: number;
EnablePreviewModeBanner: boolean;
SkipServerCertificateVerification: boolean;
EmailNotificationContentsType: string;
LoginButtonColor: string;
LoginButtonBorderColor: string;
LoginButtonTextColor: string;
EnableInactivityEmail: boolean;
};
export type RateLimitSettings = {
Enable: boolean;
PerSec: number;
MaxBurst: number;
MemoryStoreSize: number;
VaryByRemoteAddr: boolean;
VaryByUser: boolean;
VaryByHeader: string;
};
export type PrivacySettings = {
ShowEmailAddress: boolean;
ShowFullName: boolean;
};
export type SupportSettings = {
TermsOfServiceLink: string;
PrivacyPolicyLink: string;
AboutLink: string;
HelpLink: string;
ReportAProblemLink: string;
SupportEmail: string;
CustomTermsOfServiceEnabled: boolean;
CustomTermsOfServiceReAcceptancePeriod: number;
EnableAskCommunityLink: boolean;
};
export type AnnouncementSettings = {
EnableBanner: boolean;
BannerText: string;
BannerColor: string;
BannerTextColor: string;
AllowBannerDismissal: boolean;
AdminNoticesEnabled: boolean;
UserNoticesEnabled: boolean;
NoticesURL: string;
NoticesFetchFrequency: number;
NoticesSkipCache: boolean;
};
export type ThemeSettings = {
EnableThemeSelection: boolean;
DefaultTheme: string;
AllowCustomThemes: boolean;
AllowedThemes: string[];
};
export type SSOSettings = {
Enable: boolean;
Secret: string;
Id: string;
Scope: string;
AuthEndpoint: string;
TokenEndpoint: string;
UserAPIEndpoint: string;
DiscoveryEndpoint: string;
ButtonText: string;
ButtonColor: string;
};
export type Office365Settings = {
Enable: boolean;
Secret: string;
Id: string;
Scope: string;
AuthEndpoint: string;
TokenEndpoint: string;
UserAPIEndpoint: string;
DiscoveryEndpoint: string;
DirectoryId: string;
};
export type LdapSettings = {
Enable: boolean;
EnableSync: boolean;
LdapServer: string;
LdapPort: number;
ConnectionSecurity: string;
BaseDN: string;
BindUsername: string;
BindPassword: string;
UserFilter: string;
GroupFilter: string;
GuestFilter: string;
EnableAdminFilter: boolean;
AdminFilter: string;
GroupDisplayNameAttribute: string;
GroupIdAttribute: string;
FirstNameAttribute: string;
LastNameAttribute: string;
EmailAttribute: string;
UsernameAttribute: string;
NicknameAttribute: string;
IdAttribute: string;
PositionAttribute: string;
LoginIdAttribute: string;
PictureAttribute: string;
SyncIntervalMinutes: number;
SkipCertificateVerification: boolean;
PublicCertificateFile: string;
PrivateKeyFile: string;
QueryTimeout: number;
MaxPageSize: number;
LoginFieldName: string;
LoginButtonColor: string;
LoginButtonBorderColor: string;
LoginButtonTextColor: string;
Trace: boolean;
};
export type ComplianceSettings = {
Enable: boolean;
Directory: string;
EnableDaily: boolean;
BatchSize: number;
};
export type LocalizationSettings = {
DefaultServerLocale: string;
DefaultClientLocale: string;
AvailableLocales: string;
};
export type SamlSettings = {
Enable: boolean;
EnableSyncWithLdap: boolean;
EnableSyncWithLdapIncludeAuth: boolean;
IgnoreGuestsLdapSync: boolean;
Verify: boolean;
Encrypt: boolean;
SignRequest: boolean;
IdpURL: string;
IdpDescriptorURL: string;
IdpMetadataURL: string;
ServiceProviderIdentifier: string;
AssertionConsumerServiceURL: string;
SignatureAlgorithm: string;
CanonicalAlgorithm: string;
ScopingIDPProviderId: string;
ScopingIDPName: string;
IdpCertificateFile: string;
PublicCertificateFile: string;
PrivateKeyFile: string;
IdAttribute: string;
GuestAttribute: string;
EnableAdminAttribute: boolean;
AdminAttribute: string;
FirstNameAttribute: string;
LastNameAttribute: string;
EmailAttribute: string;
UsernameAttribute: string;
NicknameAttribute: string;
LocaleAttribute: string;
PositionAttribute: string;
LoginButtonText: string;
LoginButtonColor: string;
LoginButtonBorderColor: string;
LoginButtonTextColor: string;
};
export type NativeAppSettings = {
AppCustomURLSchemes: string[];
AppDownloadLink: string;
AndroidAppDownloadLink: string;
IosAppDownloadLink: string;
};
export type ClusterSettings = {
Enable: boolean;
ClusterName: string;
OverrideHostname: string;
NetworkInterface: string;
BindAddress: string;
AdvertiseAddress: string;
UseIPAddress: boolean;
EnableGossipCompression: boolean;
EnableExperimentalGossipEncryption: boolean;
ReadOnlyConfig: boolean;
GossipPort: number;
StreamingPort: number;
MaxIdleConns: number;
MaxIdleConnsPerHost: number;
IdleConnTimeoutMilliseconds: number;
};
export type MetricsSettings = {
Enable: boolean;
BlockProfileRate: number;
ListenAddress: string;
};
export type ExperimentalSettings = {
ClientSideCertEnable: boolean;
ClientSideCertCheck: string;
LinkMetadataTimeoutMilliseconds: number;
RestrictSystemAdmin: boolean;
UseNewSAMLLibrary: boolean;
EnableSharedChannels: boolean;
EnableRemoteClusterService: boolean;
EnableAppBar: boolean;
PatchPluginsReactDOM: boolean;
};
export type AnalyticsSettings = {
MaxUsersForStatistics: number;
};
export type ElasticsearchSettings = {
ConnectionURL: string;
Username: string;
Password: string;
EnableIndexing: boolean;
EnableSearching: boolean;
EnableAutocomplete: boolean;
Sniff: boolean;
PostIndexReplicas: number;
PostIndexShards: number;
ChannelIndexReplicas: number;
ChannelIndexShards: number;
UserIndexReplicas: number;
UserIndexShards: number;
AggregatePostsAfterDays: number;
PostsAggregatorJobStartTime: string;
IndexPrefix: string;
LiveIndexingBatchSize: number;
BatchSize: number;
RequestTimeoutSeconds: number;
SkipTLSVerification: boolean;
CA: string;
ClientCert: string;
ClientKey: string;
Trace: string;
};
export type BleveSettings = {
IndexDir: string;
EnableIndexing: boolean;
EnableSearching: boolean;
EnableAutocomplete: boolean;
BatchSize: number;
};
export type DataRetentionSettings = {
EnableMessageDeletion: boolean;
EnableFileDeletion: boolean;
EnableBoardsDeletion: boolean;
MessageRetentionDays: number;
FileRetentionDays: number;
BoardsRetentionDays: number;
DeletionJobStartTime: string;
BatchSize: number;
};
export type MessageExportSettings = {
EnableExport: boolean;
DownloadExportResults: boolean;
ExportFormat: string;
DailyRunTime: string;
ExportFromTimestamp: number;
BatchSize: number;
GlobalRelaySettings: {
CustomerType: string;
SMTPUsername: string;
SMTPPassword: string;
EmailAddress: string;
SMTPServerTimeout: number;
};
};
export type JobSettings = {
RunJobs: boolean;
RunScheduler: boolean;
CleanupJobsThresholdDays: number;
CleanupConfigThresholdDays: number;
};
export type ProductSettings = {
EnablePublicSharedBoards: boolean;
};
export type PluginSettings = {
Enable: boolean;
EnableUploads: boolean;
AllowInsecureDownloadURL: boolean;
EnableHealthCheck: boolean;
Directory: string;
ClientDirectory: string;
Plugins: Record<string, any>;
PluginStates: Record<string, { Enable: boolean }>;
EnableMarketplace: boolean;
EnableRemoteMarketplace: boolean;
AutomaticPrepackagedPlugins: boolean;
RequirePluginSignature: boolean;
MarketplaceURL: string;
SignaturePublicKeyFiles: string[];
ChimeraOAuthProxyURL: string;
};
export type DisplaySettings = {
CustomURLSchemes: string[];
ExperimentalTimezone: boolean;
};
export type GuestAccountsSettings = {
Enable: boolean;
AllowEmailAccounts: boolean;
EnforceMultifactorAuthentication: boolean;
RestrictCreationToDomains: string;
};
export type ImageProxySettings = {
Enable: boolean;
ImageProxyType: string;
RemoteImageProxyURL: string;
RemoteImageProxyOptions: string;
};
export type CloudSettings = {
CWSURL: string;
CWSAPIURL: string;
};
export type FeatureFlags = Record<string, string | boolean>;
export type ImportSettings = {
Directory: string;
RetentionDays: number;
};
export type ExportSettings = {
Directory: string;
RetentionDays: number;
};
export type AdminConfig = {
ServiceSettings: ServiceSettings;
TeamSettings: TeamSettings;
ClientRequirements: ClientRequirements;
SqlSettings: SqlSettings;
LogSettings: LogSettings;
ExperimentalAuditSettings: ExperimentalAuditSettings;
NotificationLogSettings: NotificationLogSettings;
PasswordSettings: PasswordSettings;
FileSettings: FileSettings;
EmailSettings: EmailSettings;
RateLimitSettings: RateLimitSettings;
PrivacySettings: PrivacySettings;
SupportSettings: SupportSettings;
AnnouncementSettings: AnnouncementSettings;
ThemeSettings: ThemeSettings;
GitLabSettings: SSOSettings;
GoogleSettings: SSOSettings;
Office365Settings: Office365Settings;
OpenIdSettings: SSOSettings;
LdapSettings: LdapSettings;
ComplianceSettings: ComplianceSettings;
LocalizationSettings: LocalizationSettings;
SamlSettings: SamlSettings;
NativeAppSettings: NativeAppSettings;
ClusterSettings: ClusterSettings;
MetricsSettings: MetricsSettings;
ExperimentalSettings: ExperimentalSettings;
AnalyticsSettings: AnalyticsSettings;
ElasticsearchSettings: ElasticsearchSettings;
BleveSettings: BleveSettings;
DataRetentionSettings: DataRetentionSettings;
MessageExportSettings: MessageExportSettings;
JobSettings: JobSettings;
ProductSettings: ProductSettings;
PluginSettings: PluginSettings;
DisplaySettings: DisplaySettings;
GuestAccountsSettings: GuestAccountsSettings;
ImageProxySettings: ImageProxySettings;
CloudSettings: CloudSettings;
FeatureFlags: FeatureFlags;
ImportSettings: ImportSettings;
ExportSettings: ExportSettings;
};
export type ReplicaLagSetting = {
DataSource: string;
QueryAbsoluteLag: string;
QueryTimeLag: string;
}
export type EnvironmentConfigSettings<T> = {
[P in keyof T]: boolean;
}
export type EnvironmentConfig = {
[P in keyof AdminConfig]: EnvironmentConfigSettings<AdminConfig[P]>;
}
export type WarnMetricStatus = {
id: string;
limit: number;
acked: boolean;
store_status: string;
};
export enum CollapsedThreads {
DISABLED = 'disabled',
DEFAULT_ON = 'default_on',
DEFAULT_OFF = 'default_off',
ALWAYS_ON = 'always_on',
}

39
webapp/platform/types/src/data_retention.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type DataRetentionCustomPolicy = {
id: string;
display_name: string;
post_duration: number;
team_count: number;
channel_count: number;
};
export type CreateDataRetentionCustomPolicy = {
display_name: string;
post_duration: number;
channel_ids: string[];
team_ids: string[];
};
export type PatchDataRetentionCustomPolicy = {
display_name: string;
post_duration: number;
}
export type PatchDataRetentionCustomPolicyTeams = {
team_ids: string[];
}
export type PatchDataRetentionCustomPolicyChannels = {
channel_ids: string[];
}
export type DataRetentionCustomPolicies = {
[x: string]: DataRetentionCustomPolicy;
};
export type GetDataRetentionCustomPoliciesRequest = {
policies: DataRetentionCustomPolicy[];
total_count: number;
};

18
webapp/platform/types/src/drafts.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {PostMetadata, PostPriorityMetadata} from './posts';
export type Draft = {
create_at: number;
update_at: number;
delete_at: number;
user_id: string;
channel_id: string;
root_id: string;
message: string;
props: Record<string, any>;
file_ids?: string[];
metadata?: PostMetadata;
priority?: PostPriorityMetadata;
};

65
webapp/platform/types/src/emojis.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type EmojiCategory =
| 'recent'
| 'searchResults'
| 'smileys-emotion'
| 'people-body'
| 'animals-nature'
| 'food-drink'
| 'activities'
| 'travel-places'
| 'objects'
| 'symbols'
| 'flags'
| 'custom';
export type CustomEmoji = {
id: string;
name: string;
category: 'custom';
create_at: number;
update_at: number;
delete_at: number;
creator_id: string;
};
export type SystemEmoji = {
name: string;
category: EmojiCategory;
image: string;
short_name: string;
short_names: string[];
batch: number;
skins?: string[];
skin_variations?: Record<string, SystemEmojiVariation>;
unified: string;
};
export type SystemEmojiVariation = {
unified: string;
non_qualified: null;
image: string;
sheet_x: number;
sheet_y: number;
added_in: string;
has_img_apple: boolean;
has_img_google: boolean;
has_img_twitter: boolean;
has_img_facebook: boolean;
}
export type Emoji = SystemEmoji | CustomEmoji;
export type EmojisState = {
customEmoji: {
[x: string]: CustomEmoji;
};
nonExistentEmoji: Set<string>;
};
export type RecentEmojiData = {
name: string;
usageCount: number;
};

11
webapp/platform/types/src/errors.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type ServerError = {
type?: string;
server_error_id?: string;
stack?: string;
message: string;
status_code?: number;
url?: string;
};

44
webapp/platform/types/src/files.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type FileInfo = {
id: string;
user_id: string;
create_at: number;
update_at: number;
delete_at: number;
name: string;
extension: string;
size: number;
mime_type: string;
width: number;
height: number;
has_preview_image: boolean;
clientId: string;
post_id?: string;
mini_preview?: string;
archived: boolean;
link?: string;
};
export type FilesState = {
files: Record<string, FileInfo>;
filesFromSearch: Record<string, FileSearchResultItem>;
fileIdsByPostId: Record<string, string[]>;
filePublicLink?: {link: string};
};
export type FileUploadResponse = {
file_infos: FileInfo[];
client_ids: string[];
}
export type FileSearchResultItem = FileInfo & {
channel_id: string;
}
export type FileSearchResults = {
order: Array<FileSearchResultItem['id']>;
file_infos: Map<string, FileSearchResultItem>;
next_file_info_id: string;
prev_file_info_id: string;
};

19
webapp/platform/types/src/general.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ClientConfig, ClientLicense, WarnMetricStatus} from './config';
export type GeneralState = {
config: Partial<ClientConfig>;
dataRetentionPolicy: any;
firstAdminVisitMarketplaceStatus: boolean;
firstAdminCompleteSetup: boolean;
license: ClientLicense;
serverVersion: string;
warnMetricsStatus: Record<string, WarnMetricStatus>;
};
export type SystemSetting = {
name: string;
value: string;
};

164
webapp/platform/types/src/gifs.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,164 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type GifsState = {
app: GifsAppState;
cache: GifsCacheState;
categories: GifsCategoriesState;
search: GifsSearchState;
};
export type GifsAppState = {
appClassName: string;
appId: string;
appName: string;
basePath: string;
enableHistory: boolean;
header: {
tabs: number[];
displayText: boolean;
};
itemTapType: number;
shareEvent: string;
}
type GifsCacheState = {
gifs: Record<string, GfycatAPIItem>;
updating: boolean;
}
type GifsCategoriesState = {
cursor: string;
hasMore: boolean;
isFetching: boolean;
tagsDict: Record<string, boolean>;
tagsList: GfycatAPITag[];
}
type GifsSearchState = {
priorLocation: string | null;
resultsByTerm: Record<string, GifsResult>;
scrollPosition: number;
searchBarText: string;
searchText: string;
}
export type GifsResult = GfycatAPIPaginatedResponse & {
count: number;
currentPage: number;
didInvalidate: boolean;
found: number;
isFetching: boolean;
items: string[];
moreRemaining: boolean;
pages: Record<number, string[]>;
start: number;
}
export interface GfycatAPIPaginatedResponse {
cursor?: string;
gfycats: GfycatAPIItem[];
totalCount?: number;
}
export interface GfycatAPIItemResponse {
gfyItem: GfycatAPIItem;
}
export interface GfycatAPIItem {
anonymous?: boolean;
avgColor: string;
captionsUrl?: null;
content_urls: { [key: string]: GfycatAPIContent };
createDate: number;
description?: string;
dislikes?: number;
domainWhitelist?: any[];
duration?: number;
encoding?: boolean;
extraLemmas?: string;
finished?: boolean;
frameRate: number;
gatekeeper: number;
geoWhitelist?: any[];
gfyId: string;
gfyName: string;
gfyNumber?: string;
gfySlug?: string;
gif100px?: string;
gifSize?: number;
gifUrl: string;
hasAudio: boolean;
hasTransparency: boolean;
height: number;
languageCategories: string[];
languageText?: string;
likes: number;
max1mbGif?: string;
max2mbGif: string;
max5mbGif: string;
md5?: string;
miniPosterUrl: string;
miniUrl?: string;
mobileHeight?: number;
mobilePosterUrl?: string;
mobileUrl: string;
mobileWidth?: number;
mp4Size?: number;
mp4Url: string;
nsfw: boolean | number;
numFrames: number;
posterUrl: string;
published: number;
rating?: string;
ratio?: null;
sitename?: string;
source?: number;
tags: string[];
thumb100PosterUrl: string;
title?: string;
type?: number;
url?: string;
userData?: GfycatAPIUser | [];
userDisplayName?: string;
userName?: string;
username?: string;
userProfileImageUrl?: string;
views: number;
views5?: number;
webmSize?: number;
webmUrl?: string;
webpUrl?: string;
width: number;
}
export interface GfycatAPIContent {
width: number;
size: number;
url: string;
height: number;
}
export interface GfycatAPIUser {
createDate?: number;
description?: string;
followers: number;
following: number;
iframeProfileImageVisible?: boolean;
name: string;
profileImageUrl: string;
profileUrl?: string;
publishedGfycats?: number;
subscription?: number;
url?: string;
userid?: string;
username: string;
verified: boolean;
views: number;
}
export interface GfycatAPITag {
tagName: string;
gfyId: string;
}

172
webapp/platform/types/src/groups.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,172 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserProfile} from './users';
import {RelationOneToOne} from './utilities';
export enum SyncableType {
Team = 'team',
Channel = 'channel'
}
export type SyncablePatch = {
scheme_admin: boolean;
auto_add: boolean;
};
export type GroupPatch = {
allow_reference: boolean;
name?: string;
};
export type CustomGroupPatch = {
name: string;
display_name: string;
};
export type Group = {
id: string;
name: string;
display_name: string;
description: string;
source: string;
remote_id: string | null;
create_at: number;
update_at: number;
delete_at: number;
has_syncables: boolean;
member_count: number;
scheme_admin: boolean;
allow_reference: boolean;
channel_member_count?: number;
channel_member_timezones_count?: number;
};
export enum GroupSource {
Ldap = 'ldap',
Custom = 'custom',
}
export type GroupTeam = {
team_id: string;
team_display_name: string;
team_type?: string;
group_id?: string;
auto_add?: boolean;
scheme_admin?: boolean;
create_at?: number;
delete_at?: number;
update_at?: number;
};
export type GroupChannel = {
channel_id: string;
channel_display_name: string;
channel_type?: string;
team_id: string;
team_display_name: string;
team_type?: string;
group_id?: string;
auto_add?: boolean;
scheme_admin?: boolean;
create_at?: number;
delete_at?: number;
update_at?: number;
};
export type GroupSyncable = {
group_id: string;
auto_add: boolean;
scheme_admin: boolean;
create_at: number;
delete_at: number;
update_at: number;
type: 'Team' | 'Channel';
};
export type GroupSyncablesState = {
teams: GroupTeam[];
channels: GroupChannel[];
};
export type GroupsState = {
syncables: Record<string, GroupSyncablesState>;
stats: RelationOneToOne<Group, GroupStats>;
groups: Record<string, Group>;
myGroups: string[];
};
export type GroupStats = {
group_id: string;
total_member_count: number;
};
export type GroupSearchOpts = {
q: string;
is_linked?: boolean;
is_configured?: boolean;
};
export type MixedUnlinkedGroup = {
mattermost_group_id?: string;
name: string;
primary_key: string;
has_syncables?: boolean;
};
export type MixedUnlinkedGroupRedux = MixedUnlinkedGroup & {
failed?: boolean;
};
export type UserWithGroup = UserProfile & {
groups: Group[];
scheme_guest: boolean;
scheme_user: boolean;
scheme_admin: boolean;
};
export type GroupsWithCount = {
groups: Group[];
total_group_count: number;
// These fields are added by the client after the groups are returned by the server
channelID?: string;
teamID?: string;
}
export type UsersWithGroupsAndCount = {
users: UserWithGroup[];
total_count: number;
};
export type GroupCreateWithUserIds = {
name: string;
allow_reference: boolean;
display_name: string;
source: string;
user_ids: string[];
description?: string;
}
export type GroupSearachParams = {
q: string;
filter_allow_reference: boolean;
page: number;
per_page: number;
include_member_count: boolean;
user_id?: string;
include_timezones?: string;
include_channel_member_count?: string;
}
export type GroupMembership = {
user_id: string;
roles: string;
}
export type GroupPermissions = {
can_delete: boolean;
can_manage_members: boolean;
}

Просмотреть файл

@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Address, Product, Invoice} from './cloud';
import {ValueOf} from './utilities';
export const SelfHostedSignupProgress = {
START: 'START',
CREATED_CUSTOMER: 'CREATED_CUSTOMER',
CREATED_INTENT: 'CREATED_INTENT',
CONFIRMED_INTENT: 'CONFIRMED_INTENT',
CREATED_SUBSCRIPTION: 'CREATED_SUBSCRIPTION',
PAID: 'PAID',
CREATED_LICENSE: 'CREATED_LICENSE',
} as const;
export interface SelfHostedSignupForm {
first_name: string;
last_name: string;
billing_address: Address;
organization: string;
}
export interface SelfHostedSignupBootstrapResponse {
progress: ValueOf<typeof SelfHostedSignupProgress>;
email: string;
}
export interface SelfHostedSignupCustomerResponse {
customer_id: string;
setup_intent_id: string;
setup_intent_secret: string;
progress: ValueOf<typeof SelfHostedSignupProgress>;
}
export interface SelfHostedSignupSuccessResponse {
progress: ValueOf<typeof SelfHostedSignupProgress>;
license: Record<string, string>;
}
export type HostedCustomerState = {
products: {
products: Record<string, Product>;
productsLoaded: boolean;
};
invoices: {
invoices: Record<string, Invoice>;
invoicesLoaded: boolean;
};
errors: {
products?: true;
invoices?: true;
trueUpReview?: true;
};
signupProgress: ValueOf<typeof SelfHostedSignupProgress>;
trueUpReviewStatus: TrueUpReviewStatusReducer;
trueUpReviewProfile: TrueUpReviewProfileReducer;
}
export type TrueUpReviewProfile = {
content: string;
}
export type TrueUpReviewStatus = {
due_date: number;
complete: boolean;
}
type RequestState = 'IDLE' | 'LOADING' | 'OK'
export interface TrueUpReviewProfileReducer extends TrueUpReviewProfile {
getRequestState: RequestState;
}
export interface TrueUpReviewStatusReducer extends TrueUpReviewStatus {
getRequestState: RequestState;
}

183
webapp/platform/types/src/insights.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,183 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ChannelType} from './channels';
import {Post} from './posts';
import {UserProfile} from './users';
export enum InsightsWidgetTypes {
TOP_CHANNELS = 'TOP_CHANNELS',
TOP_REACTIONS = 'TOP_REACTIONS',
TOP_THREADS = 'TOP_THREADS',
TOP_BOARDS = 'TOP_BOARDS',
LEAST_ACTIVE_CHANNELS = 'LEAST_ACTIVE_CHANNELS',
TOP_PLAYBOOKS = 'TOP_PLAYBOOKS',
TOP_DMS = 'TOP_DMS',
NEW_TEAM_MEMBERS = 'NEW_TEAM_MEMBERS',
}
export enum CardSizes {
large = 'lg',
medium = 'md',
small = 'sm',
}
export type CardSize = CardSizes;
export enum TimeFrames {
INSIGHTS_1_DAY = 'today',
INSIGHTS_7_DAYS = '7_day',
INSIGHTS_28_DAYS = '28_day',
}
export type TimeFrame = TimeFrames;
export type TopReaction = {
emoji_name: string;
count: number;
}
export type TopReactionResponse = {
has_next: boolean;
items: TopReaction[];
timeFrame?: TimeFrame;
}
export type TopChannel = {
id: string;
type: ChannelType;
display_name: string;
name: string;
team_id: string;
message_count: number;
}
export type TopChannelGraphData = Record<string, Record<string, number>>;
export type TopChannelResponse = {
has_next: boolean;
items: TopChannel[];
daily_channel_post_counts: TopChannelGraphData;
};
export type InsightsState = {
topReactions: Record<string, Record<TimeFrame, Record<string, TopReaction>>>;
myTopReactions: Record<string, Record<TimeFrame, Record<string, TopReaction>>>;
}
export type TopChannelActionResult = {
data?: TopChannelResponse;
error?: any;
};
export type TopThread = {
channel_id: string;
channel_display_name: string;
channel_name: string;
participants: string[];
user_information: {
id: string;
first_name: string;
last_name: string;
last_picture_update: number;
};
post: Post;
};
export type TopThreadResponse = {
has_next: boolean;
items: TopThread[];
};
export type TopThreadActionResult = {
data?: TopThreadResponse;
error?: any;
};
export type TopBoard = {
boardID: string;
icon: string;
title: string;
activityCount: number;
// MM-49023: community bugfix to maintain backwards compatibility
activeUsers: Array<UserProfile['id']> | string;
createdBy: string;
};
export type TopBoardResponse = {
has_next: boolean;
items: TopBoard[];
};
export type LeastActiveChannel = {
id: string;
display_name: string;
name: string;
participants: string[];
last_activity_at: number;
type: ChannelType;
team_id: string;
message_count: number;
};
export type LeastActiveChannelsResponse = {
has_next: boolean;
items: LeastActiveChannel[];
};
export type LeastActiveChannelsActionResult = {
data?: LeastActiveChannelsResponse;
error?: any;
};
export type TopPlaybook = {
playbook_id: string;
num_runs: number;
title: string;
last_run_at: number;
};
export type TopPlaybookResponse = {
has_next: boolean;
items: TopPlaybook[];
};
type MinUserProfile = {
id: string;
first_name: string;
last_name: string;
last_picture_update: number;
nickname: string;
position: string;
username: string;
};
export type TopDM = {
outgoing_message_count: number;
post_count: number;
second_participant: MinUserProfile;
};
export type TopDMsResponse = {
has_next: boolean;
items: TopDM[];
};
export type TopDMsActionResult = {
data?: TopDMsResponse;
error?: any;
};
export type NewMember = MinUserProfile & {
create_at: number;
};
export type NewMembersResponse = {
has_next: boolean;
items: NewMember[];
total_count: number;
};
export type NewMembersActionResult = {
data?: NewMembersResponse;
error?: any;
};

Просмотреть файл

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type PostAction = {
id?: string;
type?: string;
name?: string;
disabled?: boolean;
style?: string;
data_source?: string;
options?: PostActionOption[];
default_option?: string;
integration?: PostActionIntegration;
cookie?: string;
};
export type PostActionOption = {
text: string;
value: string;
};
export type PostActionIntegration = {
url?: string;
context?: Record<string, any>;
}
export type PostActionResponse = {
status: string;
trigger_id: string;
};

151
webapp/platform/types/src/integrations.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,151 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MessageAttachment} from './message_attachments';
import {IDMappedObjects} from './utilities';
export type IncomingWebhook = {
id: string;
create_at: number;
update_at: number;
delete_at: number;
user_id: string;
channel_id: string;
team_id: string;
display_name: string;
description: string;
username: string;
icon_url: string;
channel_locked: boolean;
};
export type OutgoingWebhook = {
id: string;
token: string;
create_at: number;
update_at: number;
delete_at: number;
creator_id: string;
channel_id: string;
team_id: string;
trigger_words: string[];
trigger_when: number;
callback_urls: string[];
display_name: string;
description: string;
content_type: string;
username: string;
icon_url: string;
};
export type Command = {
'id': string;
'token': string;
'create_at': number;
'update_at': number;
'delete_at': number;
'creator_id': string;
'team_id': string;
'trigger': string;
'method': 'P' | 'G' | '';
'username': string;
'icon_url': string;
'auto_complete': boolean;
'auto_complete_desc': string;
'auto_complete_hint': string;
'display_name': string;
'description': string;
'url': string;
};
export type CommandArgs = {
channel_id: string;
team_id?: string;
root_id?: string;
}
export type CommandResponse = {
response_type: string;
text: string;
username: string;
channel_id: SVGAnimatedString;
icon_url: string;
type: string;
props: Record<string, any>;
goto_location: string;
trigger_id: string;
skip_slack_parsing: boolean;
attachments: MessageAttachment[];
extra_responses: CommandResponse[];
};
export type AutocompleteSuggestion = {
Complete: string;
Suggestion: string;
Hint: string;
Description: string;
IconData: string;
type?: string;
};
export type CommandAutocompleteSuggestion = AutocompleteSuggestion; // TODO remove this alias after the mattermost-redux migration
export type OAuthApp = {
'id': string;
'creator_id': string;
'create_at': number;
'update_at': number;
'client_secret': string;
'name': string;
'description': string;
'icon_url': string;
'callback_urls': string[];
'homepage': string;
'is_trusted': boolean;
};
export type IntegrationsState = {
incomingHooks: IDMappedObjects<IncomingWebhook>;
outgoingHooks: IDMappedObjects<OutgoingWebhook>;
oauthApps: IDMappedObjects<OAuthApp>;
appsOAuthAppIDs: string[];
appsBotIDs: string[];
systemCommands: IDMappedObjects<Command>;
commands: IDMappedObjects<Command>;
};
export type DialogSubmission = {
url: string;
callback_id: string;
state: string;
user_id: string;
channel_id: string;
team_id: string;
submission: {
[x: string]: string;
};
cancelled: boolean;
};
export type DialogElement = {
display_name: string;
name: string;
type: string;
subtype: string;
default: string;
placeholder: string;
help_text: string;
optional: boolean;
min_length: number;
max_length: number;
data_source: string;
options: Array<{
text: string;
value: any;
}>;
};
export type SubmitDialogResponse = {
error?: string;
errors?: Record<string, string>;
};

27
webapp/platform/types/src/jobs.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IDMappedObjects} from './utilities';
export type JobType = 'data_retention' | 'elasticsearch_post_indexing' | 'ldap_sync' | 'message_export';
export type JobStatus = 'pending' | 'in_progress' | 'success' | 'error' | 'cancel_requested' | 'canceled' | 'warning';
export type Job = JobTypeBase & {
id: string;
priority: number;
create_at: number;
start_at: number;
last_activity_at: number;
status: JobStatus;
progress: number;
data: any;
};
export type JobsByType = {
[x in JobType]?: Job[];
};
export type JobsState = {
jobs: IDMappedObjects<Job>;
jobsByTypeList: JobsByType;
};
export type JobTypeBase = {
type: JobType;
}

51
webapp/platform/types/src/marketplace.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {PluginManifest} from './plugins';
import {AppManifest} from './apps';
export type MarketplaceLabel = {
name: string;
description?: string;
url?: string;
}
export enum HostingType {
OnPrem = 'on-prem',
Cloud = 'cloud',
}
export enum AuthorType {
Mattermost = 'mattermost',
Partner = 'partner',
Community = 'community',
}
export enum ReleaseStage {
Production = 'production',
Beta = 'beta',
Experimental = 'experimental',
}
interface MarketplaceBaseItem {
labels?: MarketplaceLabel[];
hosting?: HostingType;
author_type: AuthorType;
release_stage: ReleaseStage;
enterprise: boolean;
}
export interface MarketplacePlugin extends MarketplaceBaseItem {
manifest: PluginManifest;
icon_data?: string;
homepage_url?: string;
download_url?: string;
release_notes_url?: string;
installed_version?: string;
}
export interface MarketplaceApp extends MarketplaceBaseItem {
manifest: AppManifest;
installed: boolean;
icon_url?: string;
}

Просмотреть файл

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {PostAction} from './integration_actions';
export type MessageAttachment = {
id: number;
fallback: string;
color: string;
pretext: string;
author_name: string;
author_link: string;
author_icon: string;
title: string;
title_link: string;
text: string;
fields: MessageAttachmentField[];
image_url: string;
thumb_url: string;
footer: string;
footer_icon: string;
timestamp: number | string;
actions?: PostAction[];
};
export type MessageAttachmentField = {
title: string;
value: any;
short: boolean;
}

7
webapp/platform/types/src/mfa.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type MfaSecret = {
secret: string;
qr_code: string;
};

137
webapp/platform/types/src/plugins.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,137 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type PluginManifest = {
id: string;
name: string;
description?: string;
homepage_url?: string;
support_url?: string;
release_notes_url?: string;
icon_path?: string;
version: string;
min_server_version?: string;
translate?: boolean;
server?: PluginManifestServer;
backend?: PluginManifestServer;
webapp?: PluginManifestWebapp;
settings_schema?: PluginSettingsSchema;
props?: Record<string, any>;
};
export type PluginRedux = PluginManifest & {active: boolean};
export type PluginManifestServer = {
executables?: {
'linux-amd64'?: string;
'darwin-amd64'?: string;
'windows-amd64'?: string;
};
executable: string;
};
export type PluginManifestWebapp = {
bundle_path: string;
};
export type PluginSettingsSchema = {
header: string;
footer: string;
settings: PluginSetting[];
};
export type PluginSetting = {
key: string;
display_name: string;
type: string;
help_text: string;
regenerate_help_text?: string;
placeholder: string;
default: any;
options?: PluginSettingOption[];
hosting?: 'on-prem' | 'cloud';
};
export type PluginSettingOption = {
display_name: string;
value: string;
};
export type PluginsResponse = {
active: PluginManifest[];
inactive: PluginManifest[];
};
export type PluginStatus = {
plugin_id: string;
cluster_id: string;
plugin_path: string;
state: number;
name: string;
description: string;
version: string;
};
type PluginInstance = {
cluster_id: string;
version: string;
state: number;
}
export type PluginStatusRedux = {
id: string;
name: string;
description: string;
version: string;
active: boolean;
state: number;
error?: string;
instances: PluginInstance[];
}
export type ClientPluginManifest = {
id: string;
min_server_version?: string;
version: string;
webapp: {
bundle_path: string;
};
}
export type MarketplaceLabel = { // TODO remove this in favour of the definition in types/marketplace after the mattermost-redux migration
name: string;
description?: string;
url?: string;
color?: string;
}
export enum HostingType { // TODO remove this in favour of the definition in types/marketplace after the mattermost-redux migration
OnPrem = 'on-prem',
Cloud = 'cloud',
}
export enum AuthorType { // TODO remove this in favour of the definition in types/marketplace after the mattermost-redux migration
Mattermost = 'mattermost',
Partner = 'partner',
Community = 'community',
}
export enum ReleaseStage { // TODO remove this in favour of the definition in types/marketplace after the mattermost-redux migration
Production = 'production',
Beta = 'beta',
Experimental = 'experimental',
}
export type MarketplacePlugin = { // TODO remove this in favour of the definition in types/marketplace after the mattermost-redux migration
homepage_url?: string;
icon_data?: string;
download_url?: string;
release_notes_url?: string;
labels?: MarketplaceLabel[];
hosting?: HostingType;
author_type: AuthorType;
release_stage: ReleaseStage;
enterprise: boolean;
manifest: PluginManifest;
installed_version?: string;
}

206
webapp/platform/types/src/posts.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,206 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Channel, ChannelType} from './channels';
import {CustomEmoji} from './emojis';
import {FileInfo} from './files';
import {Reaction} from './reactions';
import {UserProfile} from './users';
import {
RelationOneToOne,
RelationOneToMany,
IDMappedObjects,
} from './utilities';
export type PostType = 'system_add_remove' |
'system_add_to_channel' |
'system_add_to_team' |
'system_channel_deleted' |
'system_channel_restored' |
'system_displayname_change' |
'system_convert_channel' |
'system_ephemeral' |
'system_header_change' |
'system_join_channel' |
'system_join_leave' |
'system_leave_channel' |
'system_purpose_change' |
'system_remove_from_channel' |
'system_combined_user_activity' |
'system_fake_parent_deleted' |
'system_generic' |
'reminder' |
'';
export type PostEmbedType = 'image' | 'link' | 'message_attachment' | 'opengraph' | 'permalink';
export type PostEmbed = {
type: PostEmbedType;
url: string;
data?: OpenGraphMetadata | PostPreviewMetadata;
};
export type PostImage = {
format: string;
frameCount: number;
height: number;
width: number;
};
export type PostAcknowledgement = {
post_id: Post['id'];
user_id: UserProfile['id'];
acknowledged_at: number;
}
export type PostPriorityMetadata = {
priority: PostPriority|'';
requested_ack?: boolean;
persistent_notifications?: boolean;
}
export type PostMetadata = {
embeds: PostEmbed[];
emojis: CustomEmoji[];
files: FileInfo[];
images: Record<string, PostImage>;
reactions: Reaction[];
priority?: PostPriorityMetadata;
acknowledgements?: PostAcknowledgement[];
};
export type Post = {
id: string;
create_at: number;
update_at: number;
edit_at: number;
delete_at: number;
is_pinned: boolean;
user_id: string;
channel_id: string;
root_id: string;
original_id: string;
message: string;
type: PostType;
props: Record<string, any>;
hashtags: string;
pending_post_id: string;
reply_count: number;
file_ids?: string[];
metadata: PostMetadata;
failed?: boolean;
user_activity_posts?: Post[];
state?: PostState;
filenames?: string[];
last_reply_at?: number;
participants?: any; //Array<UserProfile | UserProfile['id']>;
message_source?: string;
is_following?: boolean;
exists?: boolean;
};
export type PostState = 'DELETED';
export enum PostPriority {
URGENT = 'urgent',
IMPORTANT = 'important',
}
export type PostList = {
order: Array<Post['id']>;
posts: Record<string, Post>;
next_post_id: string;
prev_post_id: string;
first_inaccessible_post_time: number;
};
export type PaginatedPostList = PostList & {
has_next: boolean;
}
export type PostSearchResults = PostList & {
matches: RelationOneToOne<Post, string[]>;
};
export type PostOrderBlock = {
order: string[];
recent?: boolean;
oldest?: boolean;
};
export type MessageHistory = {
messages: string[];
index: {
post: number;
comment: number;
};
};
export type PostsState = {
posts: IDMappedObjects<Post>;
postsReplies: {[x in Post['id']]: number};
postsInChannel: Record<string, PostOrderBlock[]>;
postsInThread: RelationOneToMany<Post, Post>;
reactions: RelationOneToOne<Post, Record<string, Reaction>>;
openGraph: RelationOneToOne<Post, Record<string, OpenGraphMetadata>>;
pendingPostIds: string[];
selectedPostId: string;
postEditHistory: Post[];
currentFocusedPostId: string;
messagesHistory: MessageHistory;
expandedURLs: Record<string, string>;
limitedViews: {
channels: Record<Channel['id'], number>;
threads: Record<Post['root_id'], number>;
};
acknowledgements: RelationOneToOne<Post, Record<UserProfile['id'], number>>;
};
export declare type OpenGraphMetadataImage = {
secure_url?: string;
url: string;
type?: string;
height?: number;
width?: number;
}
export declare type OpenGraphMetadata = {
type?: string;
title?: string;
description?: string;
site_name?: string;
url?: string;
images: OpenGraphMetadataImage[];
};
export declare type PostPreviewMetadata = {
post_id: string;
post?: Post;
channel_display_name: string;
team_name: string;
channel_type: ChannelType;
channel_id: string;
};
export declare type PostsUsageResponse = {
count: number;
};
export declare type FilesUsageResponse = {
bytes: number;
};
export declare type TeamsUsageResponse = {
active: number;
cloud_archived: number;
};
export type PostAnalytics = {
channel_id: string;
post_id: string;
user_actual_id: string;
root_id: string;
priority?: PostPriority|'';
requested_ack?: boolean;
persistent_notifications?: boolean;
}

13
webapp/platform/types/src/preferences.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type PreferenceType = {
category: string;
name: string;
user_id: string;
value?: string;
};
export type PreferencesType = {
[x: string]: PreferenceType;
};

Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export enum Action {
URL = 'url',
}
export type ProductNotice = {
/** Unique identifier for this notice. Can be a running number. Used for storing 'viewed' state on the server. */
id: string;
/** Notice title. Use {{Mattermost}} instead of plain text to support white-labeling. Text supports Markdown. */
title: string;
/** Notice content. Use {{Mattermost}} instead of plain text to support white-labeling. Text supports Markdown. */
description: string;
image?: string;
/** Optional override for the action button text (defaults to OK) */
actionText?: string;
/** Optional action to perform on action button click. (defaults to closing the notice) */
action?: Action;
/** Optional action parameter.
* Example: {"action": "url", actionParam: "/console/some-page"}
*/
actionParam?: string;
sysAdminOnly: boolean;
teamAdminOnly: boolean;
}
/** List of product notices. Order is important and is used to resolve priorities.
* Each notice will only be show if conditions are met.
*/
export type ProductNotices = ProductNotice[];

11
webapp/platform/types/src/products.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* - `null` - explicitly Channels
* - `string` - uuid - any other product
*/
export type ProductIdentifier = null | string;
/** @see {@link ProductIdentifier} */
export type ProductScope = ProductIdentifier | ProductIdentifier[];

9
webapp/platform/types/src/reactions.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Reaction = {
user_id: string;
post_id: string;
emoji_name: string;
create_at: number;
};

70
webapp/platform/types/src/requests.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type RequestStatusOption = 'not_started' | 'started' | 'success' | 'failure' | 'cancelled';
export type RequestStatusType = {
status: RequestStatusOption;
error: null | Record<string, any>;
};
export type ChannelsRequestsStatuses = {
getChannels: RequestStatusType;
getAllChannels: RequestStatusType;
myChannels: RequestStatusType;
createChannel: RequestStatusType;
updateChannel: RequestStatusType;
};
export type GeneralRequestsStatuses = {
websocket: RequestStatusType;
};
export type PostsRequestsStatuses = {
createPost: RequestStatusType;
editPost: RequestStatusType;
getPostThread: RequestStatusType;
};
export type ThreadsRequestStatuses = {
getThreads: RequestStatusType;
};
export type TeamsRequestsStatuses = {
getMyTeams: RequestStatusType;
getTeams: RequestStatusType;
joinTeam: RequestStatusType;
};
export type UsersRequestsStatuses = {
login: RequestStatusType;
logout: RequestStatusType;
autocompleteUsers: RequestStatusType;
updateMe: RequestStatusType;
};
export type AdminRequestsStatuses = {
createCompliance: RequestStatusType;
};
export type EmojisRequestsStatuses = {
createCustomEmoji: RequestStatusType;
getCustomEmojis: RequestStatusType;
deleteCustomEmoji: RequestStatusType;
getCustomEmoji: RequestStatusType;
};
export type FilesRequestsStatuses = {
uploadFiles: RequestStatusType;
};
export type RolesRequestsStatuses = {
getRolesByNames: RequestStatusType;
getRoleByName: RequestStatusType;
getRole: RequestStatusType;
editRole: RequestStatusType;
};
export type SearchRequestsStatuses = {
flaggedPosts: RequestStatusType;
pinnedPosts: RequestStatusType;
};

15
webapp/platform/types/src/roles.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Role = {
id: string;
name: string;
display_name: string;
description: string;
create_at: number;
update_at: number;
delete_at: number;
permissions: string[];
scheme_managed: boolean;
built_in: boolean;
};

14
webapp/platform/types/src/saml.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type SamlCertificateStatus = {
idp_certificate_file: string;
private_key_file: string;
public_certificate_file: string;
};
export type SamlMetadataResponse = {
idp_descriptor_url: string;
idp_url: string;
idp_public_certificate: string;
};

32
webapp/platform/types/src/schemes.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type SchemeScope = 'team' | 'channel';
export type Scheme = {
id: string;
name: string;
description: string;
display_name: string;
create_at: number;
update_at: number;
delete_at: number;
scope: SchemeScope;
default_team_admin_role: string;
default_team_user_role: string;
default_team_guest_role: string;
default_channel_admin_role: string;
default_channel_user_role: string;
default_channel_guest_role: string;
default_playbook_admin_role: string;
default_playbook_member_role: string;
default_run_member_role: string;
};
export type SchemesState = {
schemes: {
[x: string]: Scheme;
};
};
export type SchemePatch = {
name?: string;
description?: string;
};

33
webapp/platform/types/src/search.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Search = {
terms: string;
isOrSearch: boolean;
};
export type SearchState = {
current: any;
results: string[];
fileResults: string[];
flagged: string[];
pinned: Record<string, string[]>;
isSearchingTerm: boolean;
isSearchGettingMore: boolean;
isLimitedResults: number;
recent: {
[x: string]: Search[];
};
matches: {
[x: string]: string[];
};
};
export type SearchParameter = {
terms: string;
is_or_search: boolean;
time_zone_offset?: number;
page: number;
per_page: number;
include_deleted_channels: boolean;
}

19
webapp/platform/types/src/sessions.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {TeamMembership} from './teams';
export type Session = {
id: string;
token: string;
create_at: number;
expires_at: number;
last_activity_at: number;
user_id: string;
device_id: string;
roles: string;
is_oauth: boolean;
props: Record<string, any>;
team_members: TeamMembership[];
local: boolean;
}

6
webapp/platform/types/src/setup.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type CompleteOnboardingRequest = {
install_plugins: string[];
}

93
webapp/platform/types/src/store.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {AdminState} from './admin';
import {Bot} from './bots';
import {ChannelsState} from './channels';
import {ChannelCategoriesState} from './channel_categories';
import {CloudState, CloudUsage} from './cloud';
import {HostedCustomerState} from './hosted_customer';
import {EmojisState} from './emojis';
import {FilesState} from './files';
import {GeneralState} from './general';
import {GroupsState} from './groups';
import {IntegrationsState} from './integrations';
import {JobsState} from './jobs';
import {PostsState} from './posts';
import {PreferenceType} from './preferences';
import {
AdminRequestsStatuses, ChannelsRequestsStatuses,
FilesRequestsStatuses, GeneralRequestsStatuses,
PostsRequestsStatuses, RolesRequestsStatuses,
TeamsRequestsStatuses, UsersRequestsStatuses,
} from './requests';
import {Role} from './roles';
import {SchemesState} from './schemes';
import {SearchState} from './search';
import {TeamsState} from './teams';
import {ThreadsState} from './threads';
import {Typing} from './typing';
import {UsersState} from './users';
import {AppsState} from './apps';
import {InsightsState} from './insights';
import {GifsState} from './gifs';
import {WorkTemplatesState} from './work_templates';
export type GlobalState = {
entities: {
general: GeneralState;
users: UsersState;
teams: TeamsState;
channels: ChannelsState;
posts: PostsState;
threads: ThreadsState;
bots: {
accounts: Record<string, Bot>;
};
preferences: {
myPreferences: {
[x: string]: PreferenceType;
};
};
admin: AdminState;
jobs: JobsState;
search: SearchState;
integrations: IntegrationsState;
files: FilesState;
emojis: EmojisState;
typing: Typing;
roles: {
roles: {
[x: string]: Role;
};
pending: Set<string>;
};
schemes: SchemesState;
gifs: GifsState;
groups: GroupsState;
channelCategories: ChannelCategoriesState;
apps: AppsState;
cloud: CloudState;
hostedCustomer: HostedCustomerState;
usage: CloudUsage;
insights: InsightsState;
worktemplates: WorkTemplatesState;
};
errors: any[];
requests: {
channels: ChannelsRequestsStatuses;
general: GeneralRequestsStatuses;
posts: PostsRequestsStatuses;
teams: TeamsRequestsStatuses;
users: UsersRequestsStatuses;
admin: AdminRequestsStatuses;
files: FilesRequestsStatuses;
roles: RolesRequestsStatuses;
};
websocket: {
connected: boolean;
lastConnectAt: number;
lastDisconnectAt: number;
connectionId: string;
};
};

110
webapp/platform/types/src/teams.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,110 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ServerError} from './errors';
import {UserProfile} from './users';
import {RelationOneToOne} from './utilities';
export type TeamMembership = TeamUnread & {
user_id: string;
roles: string;
delete_at: number;
scheme_admin: boolean;
scheme_guest: boolean;
scheme_user: boolean;
};
export type TeamMemberWithError = {
member: TeamMembership;
user_id: string;
error: ServerError;
}
export type TeamType = 'O' | 'I';
export type Team = {
id: string;
create_at: number;
update_at: number;
delete_at: number;
display_name: string;
name: string;
description: string;
email: string;
type: TeamType;
company_name: string;
allowed_domains: string;
invite_id: string;
allow_open_invite: boolean;
scheme_id: string;
group_constrained: boolean;
policy_id?: string | null;
};
export type TeamsState = {
currentTeamId: string;
teams: Record<string, Team>;
myMembers: Record<string, TeamMembership>;
membersInTeam: RelationOneToOne<Team, RelationOneToOne<UserProfile, TeamMembership>>;
stats: RelationOneToOne<Team, TeamStats>;
groupsAssociatedToTeam: any;
totalCount: number;
};
export type TeamUnread = {
team_id: string;
/** The number of unread mentions in channels on this team, not including DMs and GMs */
mention_count: number;
/** The number of unread mentions in root posts in channels on this team, not including DMs and GMs */
mention_count_root: number;
/**
* The number of unread posts in channels on this team, not including DMs and GMs
*
* @remarks Note that this differs from ChannelMembership.msg_count and ChannelUnread.msg_count since it tracks
* unread posts instead of read posts.
*/
msg_count: number;
/**
* The number of unread root posts in channels on this team, not including DMs and GMs
*
* @remarks Note that this differs from ChannelMember.msg_count_root and ChannelUnread.msg_count_root since it
* tracks unread posts instead of read posts.
*/
msg_count_root: number;
thread_count?: number;
thread_mention_count?: number;
thread_urgent_mention_count?: number;
};
export type GetTeamMembersOpts = {
sort?: 'Username';
exclude_deleted_users?: boolean;
};
export type TeamsWithCount = {
teams: Team[];
total_count: number;
};
export type TeamStats = {
team_id: string;
total_member_count: number;
active_member_count: number;
};
export type TeamSearchOpts = {
page?: number;
per_page?: number;
allow_open_invite?: boolean;
group_constrained?: boolean;
}
export type TeamInviteWithError = {
email: string;
error: ServerError;
};

Просмотреть файл

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type TermsOfService = {
id: string;
create_at: number;
user_id: string;
text: string;
}

72
webapp/platform/types/src/threads.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,72 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Post} from './posts';
import type {Team} from './teams';
import type {Channel} from './channels';
import type {UserProfile} from './users';
import type {IDMappedObjects, RelationOneToMany, RelationOneToOne} from './utilities';
export enum UserThreadType {
Synthetic = 'S' // derived from post
}
export type UserThread = {
id: string;
reply_count: number;
last_reply_at: number;
last_viewed_at: number;
participants: Array<{id: UserProfile['id']} | UserProfile>;
unread_replies: number;
unread_mentions: number;
is_following: boolean;
is_urgent?: boolean;
type?: UserThreadType;
// TODO consider flattening, removing post from UserThreads in-store
/**
* only depend on channel_id and user_id for UserThread<>Channel/User mapping,
* use normalized post store/selectors as those are kept up-to-date in the store
*/
post: {
channel_id: Channel['id'];
user_id: UserProfile['id'];
};
};
type SyntheticMissingKeys = 'unread_replies' | 'unread_mentions' | 'last_viewed_at';
export type UserThreadSynthetic = Omit<UserThread, SyntheticMissingKeys> & {
type: UserThreadType.Synthetic;
}
export function threadIsSynthetic(thread: UserThread | UserThreadSynthetic): thread is UserThreadSynthetic {
return thread.type === UserThreadType.Synthetic;
}
export type UserThreadWithPost = UserThread & {post: Post};
export type UserThreadList = {
total: number;
total_unread_threads: number;
total_unread_mentions: number;
total_unread_urgent_mentions?: number;
threads: UserThreadWithPost[];
}
export type ThreadsState = {
threadsInTeam: RelationOneToMany<Team, UserThread>;
unreadThreadsInTeam: RelationOneToMany<Team, UserThread>;
threads: IDMappedObjects<UserThread>;
counts: RelationOneToOne<Team, {
total: number;
total_unread_threads: number;
total_unread_mentions: number;
total_unread_urgent_mentions?: number;
}>;
countsIncludingDirect: RelationOneToOne<Team, {
total: number;
total_unread_threads: number;
total_unread_mentions: number;
total_unread_urgent_mentions?: number;
}>;
};

8
webapp/platform/types/src/typing.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Typing = {
[x: string]: {
[x: string]: number;
};
};

141
webapp/platform/types/src/users.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,141 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Audit} from './audits';
import {Channel} from './channels';
import {Group} from './groups';
import {Session} from './sessions';
import {Team} from './teams';
import {IDMappedObjects, RelationOneToMany, RelationOneToManyUnique, RelationOneToOne} from './utilities';
export type UserNotifyProps = {
desktop: 'default' | 'all' | 'mention' | 'none';
desktop_sound: 'true' | 'false';
email: 'true' | 'false';
mark_unread: 'all' | 'mention';
push: 'default' | 'all' | 'mention' | 'none';
push_status: 'ooo' | 'offline' | 'away' | 'dnd' | 'online';
comments: 'never' | 'root' | 'any';
first_name: 'true' | 'false';
channel: 'true' | 'false';
mention_keys: string;
desktop_notification_sound?: 'Bing' | 'Crackle' | 'Down' | 'Hello' | 'Ripple' | 'Upstairs';
desktop_threads?: 'default' | 'all' | 'mention' | 'none';
email_threads?: 'default' | 'all' | 'mention' | 'none';
push_threads?: 'default' | 'all' | 'mention' | 'none';
auto_responder_active?: 'true' | 'false';
auto_responder_message?: string;
};
export type UserProfile = {
id: string;
create_at: number;
update_at: number;
delete_at: number;
username: string;
password: string;
auth_service: string;
email: string;
nickname: string;
first_name: string;
last_name: string;
position: string;
roles: string;
props: Record<string, string>;
notify_props: UserNotifyProps;
last_password_update: number;
last_picture_update: number;
locale: string;
timezone?: UserTimezone;
mfa_active: boolean;
last_activity_at: number;
is_bot: boolean;
bot_description: string;
terms_of_service_id: string;
terms_of_service_create_at: number;
remote_id?: string;
status?: string;
};
export type UserProfileWithLastViewAt = UserProfile & {
last_viewed_at: number;
};
export type UsersState = {
currentUserId: string;
isManualStatus: RelationOneToOne<UserProfile, boolean>;
mySessions: Session[];
myAudits: Audit[];
profiles: IDMappedObjects<UserProfile>;
profilesInTeam: RelationOneToMany<Team, UserProfile>;
profilesNotInTeam: RelationOneToMany<Team, UserProfile>;
profilesWithoutTeam: Set<string>;
profilesInChannel: RelationOneToManyUnique<Channel, UserProfile>;
profilesNotInChannel: RelationOneToManyUnique<Channel, UserProfile>;
profilesInGroup: RelationOneToMany<Group, UserProfile>;
profilesNotInGroup: RelationOneToMany<Group, UserProfile>;
statuses: RelationOneToOne<UserProfile, string>;
stats: RelationOneToOne<UserProfile, UsersStats>;
filteredStats?: UsersStats;
myUserAccessTokens: Record<string, UserAccessToken>;
lastActivity: RelationOneToOne<UserProfile, number>;
};
export type UserTimezone = {
useAutomaticTimezone: boolean | string;
automaticTimezone: string;
manualTimezone: string;
};
export type UserStatus = {
user_id: string;
status: string;
manual?: boolean;
last_activity_at?: number;
active_channel?: string;
dnd_end_time?: number;
};
export enum CustomStatusDuration {
DONT_CLEAR = '',
THIRTY_MINUTES = 'thirty_minutes',
ONE_HOUR = 'one_hour',
FOUR_HOURS = 'four_hours',
TODAY = 'today',
THIS_WEEK = 'this_week',
DATE_AND_TIME = 'date_and_time',
CUSTOM_DATE_TIME = 'custom_date_time',
}
export type UserCustomStatus = {
emoji: string;
text: string;
duration: CustomStatusDuration;
expires_at?: string;
};
export type UserAccessToken = {
id: string;
token?: string;
user_id: string;
description: string;
is_active: boolean;
};
export type UsersStats = {
total_users_count: number;
};
export type GetFilteredUsersStatsOpts = {
in_team?: string;
in_channel?: string;
include_deleted?: boolean;
include_bots?: boolean;
roles?: string[];
channel_roles?: string[];
team_roles?: string[];
};
export type AuthChangeResponse = {
follow_link: string;
};

26
webapp/platform/types/src/utilities.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type RelationOneToOne<E extends {id: string}, T> = {
[x in E['id']]: T;
};
export type RelationOneToMany<E1 extends {id: string}, E2 extends {id: string}> = {
[x in E1['id']]: Array<E2['id']>;
};
export type RelationOneToManyUnique<E1 extends {id: string}, E2 extends {id: string}> = {
[x in E1['id']]: Set<E2['id']>;
};
export type IDMappedObjects<E extends {id: string}> = RelationOneToOne<E, E>;
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
}
export type ValueOf<T> = T[keyof T];
/**
* Based on https://stackoverflow.com/a/49725198
*/
export type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> & {[K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>}[Keys];

97
webapp/platform/types/src/work_templates.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {RequireOnlyOne} from './utilities';
export type WorkTemplatesState = {
categories: Category[];
templatesInCategory: Record<string, WorkTemplate[]>;
playbookTemplates: PlaybookTemplateType[];
linkedProducts: Record<string, number>;
}
export interface PlaybookTemplateType {
title: string;
template: any;
}
export interface ExecuteWorkTemplateRequest {
team_id: string;
name: string;
visibility: Visibility;
work_template: WorkTemplate;
playbook_templates?: PlaybookTemplateType[];
}
export interface ExecuteWorkTemplateResponse {
channel_with_playbook_ids: string[];
channel_ids: string[];
}
export interface WorkTemplate {
id: string;
category: string;
useCase: string;
description: Description;
illustration: string;
visibility: Visibility;
content: ValidContent[];
}
export const categories = ['product', 'devops', 'company_wide', 'leadership', 'design'];
export interface Category {
id: typeof categories[number];
name: string;
}
export interface Channel {
id: string;
name: string;
illustration: string;
playbook?: string;
}
export interface Board {
id: string;
name: string;
illustration: string;
channel?: string;
}
export interface Playbook {
id: string;
name: string;
illustration: string;
template: string;
}
export interface Integration {
id: string;
name?: string;
icon?: string;
installed?: boolean;
}
interface Content {
channel?: Channel;
board?: Board;
playbook?: Playbook;
integration?: Integration;
}
type ValidContent = RequireOnlyOne<Content, 'channel' | 'board' | 'playbook' | 'integration'>;
export interface MessageWithIllustration {
message: string;
illustration?: string;
}
type MessageWithMandatoryIllustration = Partial<MessageWithIllustration> & Required<Pick<MessageWithIllustration, 'illustration'>>;
interface Description {
channel: MessageWithIllustration;
board: MessageWithIllustration;
playbook: MessageWithIllustration;
integration: MessageWithMandatoryIllustration;
}
export enum Visibility {
Public = 'public',
Private = 'private',
}

18
webapp/platform/types/tsconfig.json Обычный файл
Просмотреть файл

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es6",
"declaration": true,
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
"outDir": "./lib",
"rootDir": "./src",
"composite": true
},
"exclude": ["**/node_modules", "**/lib", "**/*.test.js", "**/*.test.ts"]
}