mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
Improve widget style (#652)
* Add widget card style and background * Add padding and header based on embed type parameter * Add changelog * Add loading spinner component * Add loading spinners
This commit is contained in:
parent
1b80bd8f40
commit
7575f93f67
1
changelog.d/652.misc
Normal file
1
changelog.d/652.misc
Normal file
@ -0,0 +1 @@
|
||||
Minor improvements to widget UI styles.
|
17
web/App.tsx
17
web/App.tsx
@ -1,9 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Component } from 'preact';
|
||||
import WA, { MatrixCapabilities } from 'matrix-widget-api';
|
||||
import { BridgeAPI, BridgeAPIError } from './BridgeAPI';
|
||||
import { BridgeAPI, BridgeAPIError, EmbedType, embedTypeParameter } from './BridgeAPI';
|
||||
import { BridgeRoomState } from '../src/Widgets/BridgeWidgetInterface';
|
||||
import { ErrorPane } from './components/elements';
|
||||
import { LoadingSpinner } from './components/elements/LoadingSpinner';
|
||||
import { Card, ErrorPane } from './components/elements';
|
||||
import AdminSettings from './components/AdminSettings';
|
||||
import RoomConfigView from './components/RoomConfigView';
|
||||
|
||||
@ -19,6 +20,7 @@ interface ICompleteState extends IMinimalState {
|
||||
[sectionName: string]: boolean;
|
||||
},
|
||||
serviceScope?: string,
|
||||
embedType: EmbedType,
|
||||
kind: "invite"|"admin"|"roomConfig",
|
||||
}
|
||||
|
||||
@ -55,6 +57,7 @@ export default class App extends Component<void, IState> {
|
||||
const roomId = assertParam(qs, 'roomId');
|
||||
const widgetKind = qs.get('kind') as "invite"|"admin"|"roomConfig";
|
||||
const serviceScope = qs.get('serviceScope');
|
||||
const embedType = qs.get(embedTypeParameter);
|
||||
// Fetch via config.
|
||||
this.widgetApi = new WA.WidgetApi(widgetId);
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.RequiresClient);
|
||||
@ -84,6 +87,7 @@ export default class App extends Component<void, IState> {
|
||||
roomId,
|
||||
supportedServices,
|
||||
serviceScope: serviceScope || undefined,
|
||||
embedType: embedType === EmbedType.IntegrationManager ? EmbedType.IntegrationManager : EmbedType.Default,
|
||||
kind: widgetKind,
|
||||
busy: false,
|
||||
});
|
||||
@ -110,7 +114,9 @@ export default class App extends Component<void, IState> {
|
||||
if (this.state.error) {
|
||||
content = <ErrorPane>{this.state.error}</ErrorPane>;
|
||||
} else if (this.state.busy) {
|
||||
content = <div class="spinner" />;
|
||||
content = <Card>
|
||||
<LoadingSpinner />
|
||||
</Card>;
|
||||
}
|
||||
|
||||
if ("kind" in this.state) {
|
||||
@ -123,6 +129,7 @@ export default class App extends Component<void, IState> {
|
||||
roomId={this.state.roomId}
|
||||
supportedServices={this.state.supportedServices}
|
||||
serviceScope={this.state.serviceScope}
|
||||
embedType={this.state.embedType}
|
||||
bridgeApi={this.bridgeApi}
|
||||
widgetApi={this.widgetApi}
|
||||
/>;
|
||||
@ -135,7 +142,9 @@ export default class App extends Component<void, IState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div style={{
|
||||
padding: this.state.embedType === "integration-manager" ? "0" : "16px",
|
||||
}}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
@ -140,7 +140,14 @@ export class BridgeAPI {
|
||||
}
|
||||
}
|
||||
|
||||
export const embedTypeParameter = 'io_element_embed_type';
|
||||
export enum EmbedType {
|
||||
IntegrationManager = 'integration-manager',
|
||||
Default = 'default',
|
||||
}
|
||||
|
||||
export type BridgeConfig = FunctionComponent<{
|
||||
api: BridgeAPI,
|
||||
roomId: string,
|
||||
showHeader: boolean,
|
||||
}>;
|
@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useCallback } from 'preact/hooks';
|
||||
import { LoadingSpinner } from "./elements/LoadingSpinner";
|
||||
import { BridgeRoomState } from "../../src/Widgets/BridgeWidgetInterface";
|
||||
import GeneralConfig from './configs/GeneralConfig';
|
||||
import style from "./AdminSettings.module.scss";
|
||||
@ -36,7 +37,7 @@ export default function AdminSettings(props: IProps) {
|
||||
);
|
||||
if (busy) {
|
||||
return <div class={style.root}>
|
||||
<div class="spinner" />
|
||||
<LoadingSpinner />
|
||||
</div>;
|
||||
}
|
||||
return <div class={style.root}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
import { useState } from "preact/hooks"
|
||||
import { BridgeAPI, BridgeConfig } from "../BridgeAPI";
|
||||
import { BridgeAPI, BridgeConfig, EmbedType } from "../BridgeAPI";
|
||||
import style from "./RoomConfigView.module.scss";
|
||||
import { ConnectionCard } from "./ConnectionCard";
|
||||
import { FeedsConfig } from "./roomConfig/FeedsConfig";
|
||||
@ -21,6 +21,7 @@ interface IProps {
|
||||
bridgeApi: BridgeAPI,
|
||||
supportedServices: {[service: string]: boolean},
|
||||
serviceScope?: string,
|
||||
embedType: EmbedType,
|
||||
roomId: string,
|
||||
}
|
||||
|
||||
@ -80,7 +81,11 @@ export default function RoomConfigView(props: IProps) {
|
||||
|
||||
if (activeConnectionType) {
|
||||
const ConfigComponent = connections[activeConnectionType].component;
|
||||
content = <ConfigComponent roomId={props.roomId} api={props.bridgeApi} />;
|
||||
content = <ConfigComponent
|
||||
roomId={props.roomId}
|
||||
api={props.bridgeApi}
|
||||
showHeader={props.embedType !== EmbedType.IntegrationManager}
|
||||
/>;
|
||||
} else {
|
||||
content = <>
|
||||
<section>
|
||||
@ -100,13 +105,13 @@ export default function RoomConfigView(props: IProps) {
|
||||
}
|
||||
|
||||
return <div className={style.root}>
|
||||
<header>
|
||||
{!serviceScope && activeConnectionType &&
|
||||
<header>
|
||||
<span className={style.backButton} onClick={() => setActiveConnectionType(null)}>
|
||||
<span className="chevron" /> Browse integrations
|
||||
</span>
|
||||
}
|
||||
</header>
|
||||
}
|
||||
{content}
|
||||
</div>;
|
||||
}
|
||||
|
11
web/components/elements/Card.module.scss
Normal file
11
web/components/elements/Card.module.scss
Normal file
@ -0,0 +1,11 @@
|
||||
.card {
|
||||
/* Compound/Light/Background */
|
||||
background: #FFFFFF;
|
||||
|
||||
/* Compound/Light/Quinary Content */
|
||||
border: 1px solid #E3E8F0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
|
||||
padding: 32px;
|
||||
}
|
11
web/components/elements/Card.tsx
Normal file
11
web/components/elements/Card.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'preact';
|
||||
|
||||
import styles from './Card.module.scss';
|
||||
|
||||
const Card = (props: React.ComponentProps<'div'>) =>
|
||||
<div
|
||||
{...props}
|
||||
className={styles.card}
|
||||
/>;
|
||||
|
||||
export { Card };
|
18
web/components/elements/LoadingSpinner.module.scss
Normal file
18
web/components/elements/LoadingSpinner.module.scss
Normal file
@ -0,0 +1,18 @@
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
animation-name: spin;
|
||||
animation-duration: 1000ms;
|
||||
animation-timing-function: steps(8, end);
|
||||
animation-iteration-count: infinite;
|
||||
}
|
71
web/components/elements/LoadingSpinner.tsx
Normal file
71
web/components/elements/LoadingSpinner.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'preact';
|
||||
|
||||
import styles from './LoadingSpinner.module.scss';
|
||||
|
||||
export const LoadingSpinner = (props: React.ComponentProps<'svg'>) => (
|
||||
<div className={styles.spinner}>
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6.232 6.232a1 1 0 0 0 0-1.414L4.111 2.697A1 1 0 1 0 2.697 4.11l2.121 2.121a1 1 0 0 0 1.414 0z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
opacity: 0.81,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M4.5 7a1 1 0 0 1 0 2h-3a1 1 0 0 1 0-2z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
opacity: 0.69,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M6.232 9.768a1 1 0 0 0-1.414 0l-2.121 2.121a1 1 0 1 0 1.414 1.414l2.121-2.121a1 1 0 0 0 0-1.414z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
opacity: 0.57,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M7 11.5a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
opacity: 0.45,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M13.303 13.303a1 1 0 0 0 0-1.414l-2.121-2.121a1 1 0 0 0-1.414 1.414l2.121 2.121a1 1 0 0 0 1.414 0z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
opacity: 0.32,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M14.5 7a1 1 0 1 1 0 2h-3a1 1 0 1 1 0-2z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
opacity: 0.25,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M13.303 2.697a1 1 0 0 0-1.414 0L9.768 4.818a1 1 0 1 0 1.414 1.414l2.121-2.121a1 1 0 0 0 0-1.414z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
opacity: 0.12,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M8 .5a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0v-3a1 1 0 0 0-1-1Z"
|
||||
style={{
|
||||
fill: '#000',
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
@ -1,5 +1,6 @@
|
||||
export * from "./Button";
|
||||
export * from "./ButtonSet";
|
||||
export * from "./Card";
|
||||
export * from "./ErrorPane";
|
||||
export * from "./InputField";
|
||||
export * from "./ListItem";
|
||||
|
@ -75,9 +75,10 @@ const RoomConfigText = {
|
||||
|
||||
const RoomConfigListItemFunc = (c: FeedResponseItem) => c.config.label || c.config.url;
|
||||
|
||||
export const FeedsConfig: BridgeConfig = ({ api, roomId }) => {
|
||||
export const FeedsConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
|
||||
return <RoomConfig<ServiceConfig, FeedResponseItem, FeedConnectionState>
|
||||
headerImg={FeedsIcon}
|
||||
showHeader={showHeader}
|
||||
api={api}
|
||||
roomId={roomId}
|
||||
type="feeds"
|
||||
|
@ -2,7 +2,7 @@ import { FunctionComponent, createRef } from "preact";
|
||||
import { useCallback, useState } from "preact/hooks"
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { BridgeAPI } from "../../BridgeAPI";
|
||||
import { BridgeConfig } from "../../BridgeAPI";
|
||||
import { GenericHookConnectionState, GenericHookResponseItem } from "../../../src/Connections/GenericHook";
|
||||
import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig";
|
||||
import { InputField, ButtonSet, Button } from "../elements";
|
||||
@ -73,11 +73,6 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
|
||||
</form>;
|
||||
};
|
||||
|
||||
interface IGenericWebhookConfigProps {
|
||||
api: BridgeAPI,
|
||||
roomId: string,
|
||||
}
|
||||
|
||||
interface ServiceConfig {
|
||||
allowJsTransformationFunctions: boolean
|
||||
}
|
||||
@ -91,9 +86,10 @@ const RoomConfigText = {
|
||||
|
||||
const RoomConfigListItemFunc = (c: GenericHookResponseItem) => c.config.name;
|
||||
|
||||
export const GenericWebhookConfig: FunctionComponent<IGenericWebhookConfigProps> = ({ api, roomId }) => {
|
||||
export const GenericWebhookConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
|
||||
return <RoomConfig<ServiceConfig, GenericHookResponseItem, GenericHookConnectionState>
|
||||
headerImg={WebhookIcon}
|
||||
showHeader={showHeader}
|
||||
api={api}
|
||||
roomId={roomId}
|
||||
type="generic"
|
||||
|
@ -135,9 +135,10 @@ const RoomConfigText = {
|
||||
|
||||
const RoomConfigListItemFunc = (c: GitHubRepoResponseItem) => getRepoFullName(c.config);
|
||||
|
||||
export const GithubRepoConfig: BridgeConfig = ({ api, roomId }) => {
|
||||
export const GithubRepoConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
|
||||
return <RoomConfig<never, GitHubRepoResponseItem, GitHubRepoConnectionState>
|
||||
headerImg={GitHubIcon}
|
||||
showHeader={showHeader}
|
||||
api={api}
|
||||
roomId={roomId}
|
||||
type="github"
|
||||
|
@ -122,9 +122,10 @@ const RoomConfigText = {
|
||||
|
||||
const RoomConfigListItemFunc = (c: GitLabRepoResponseItem) => c.config.path;
|
||||
|
||||
export const GitlabRepoConfig: BridgeConfig = ({ api, roomId }) => {
|
||||
export const GitlabRepoConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
|
||||
return <RoomConfig<never, GitLabRepoResponseItem, GitLabRepoConnectionState>
|
||||
headerImg={GitLabIcon}
|
||||
showHeader={showHeader}
|
||||
api={api}
|
||||
roomId={roomId}
|
||||
type="gitlab"
|
||||
|
@ -108,9 +108,10 @@ const RoomConfigText = {
|
||||
|
||||
const RoomConfigListItemFunc = (c: JiraProjectResponseItem) => c.config.url;
|
||||
|
||||
export const JiraProjectConfig: BridgeConfig = ({ api, roomId }) => {
|
||||
export const JiraProjectConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
|
||||
return <RoomConfig<never, JiraProjectResponseItem, JiraProjectConnectionState>
|
||||
headerImg={JiraIcon}
|
||||
showHeader={showHeader}
|
||||
api={api}
|
||||
roomId={roomId}
|
||||
type="jira"
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FunctionComponent } from "preact";
|
||||
import { useCallback, useEffect, useReducer, useState } from "preact/hooks"
|
||||
import { BridgeAPI, BridgeAPIError } from "../../BridgeAPI";
|
||||
import { ErrorPane, ListItem, WarningPane } from "../elements";
|
||||
import { ErrorPane, ListItem, WarningPane, Card } from "../elements";
|
||||
import style from "./RoomConfig.module.scss";
|
||||
import { GetConnectionsResponseItem } from "../../../src/provisioning/api";
|
||||
import { IConnectionState } from "../../../src/Connections";
|
||||
import { LoadingSpinner } from '../elements/LoadingSpinner';
|
||||
|
||||
|
||||
export interface ConnectionConfigurationProps<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState extends IConnectionState> {
|
||||
@ -19,6 +20,7 @@ interface IRoomConfigProps<SConfig, ConnectionType extends GetConnectionsRespons
|
||||
api: BridgeAPI;
|
||||
roomId: string;
|
||||
type: string;
|
||||
showHeader: boolean;
|
||||
headerImg: string;
|
||||
text: {
|
||||
header: string;
|
||||
@ -32,7 +34,7 @@ interface IRoomConfigProps<SConfig, ConnectionType extends GetConnectionsRespons
|
||||
}
|
||||
|
||||
export const RoomConfig = function<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState extends IConnectionState>(props: IRoomConfigProps<SConfig, ConnectionType, ConnectionState>) {
|
||||
const { api, roomId, type, headerImg, text, listItemName, connectionEventType } = props;
|
||||
const { api, roomId, type, headerImg, showHeader, text, listItemName, connectionEventType } = props;
|
||||
const ConnectionConfigComponent = props.connectionConfigComponent;
|
||||
const [ error, setError ] = useState<null|{header?: string, message: string, isWarning?: boolean, forPrevious?: boolean}>(null);
|
||||
const [ connections, setConnections ] = useState<ConnectionType[]|null>(null);
|
||||
@ -91,7 +93,8 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
|
||||
});
|
||||
}, [api, roomId, connectionEventType]);
|
||||
|
||||
return <main>
|
||||
return <Card>
|
||||
<main>
|
||||
{
|
||||
error &&
|
||||
(!error.isWarning
|
||||
@ -99,10 +102,12 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
|
||||
: <WarningPane header={error.header || "Warning"}>{error.message}</WarningPane>
|
||||
)
|
||||
}
|
||||
{ showHeader &&
|
||||
<header className={style.header}>
|
||||
<img alt="" src={headerImg} />
|
||||
<h1>{text.header}</h1>
|
||||
</header>
|
||||
}
|
||||
{ canEditRoom && <section>
|
||||
<h2>{text.createNew}</h2>
|
||||
{serviceConfig && <ConnectionConfigComponent
|
||||
@ -112,6 +117,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
|
||||
onSave={handleSaveOnCreation}
|
||||
/>}
|
||||
</section>}
|
||||
{ connections === null && <LoadingSpinner /> }
|
||||
{ !!connections?.length && <section>
|
||||
<h2>{ canEditRoom ? text.listCanEdit : text.listCantEdit }</h2>
|
||||
{ serviceConfig && connections?.map(c => <ListItem key={c.id} text={listItemName(c)}>
|
||||
@ -149,5 +155,6 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
|
||||
</ListItem>)
|
||||
}
|
||||
</section>}
|
||||
</main>;
|
||||
</main>
|
||||
</Card>;
|
||||
};
|
@ -1,11 +1,15 @@
|
||||
|
||||
|
||||
:root {
|
||||
--background-color: #FFFFFF;
|
||||
--foreground-color: #17191C;
|
||||
--light-color: #737D8C;
|
||||
--primary-color: #0DBD8B;
|
||||
--primary-color-disabled: #0dbd8baf;
|
||||
|
||||
background-color: #F4F6FA;
|
||||
color: var(--foreground-color);
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
// @media (prefers-color-scheme: dark) {
|
||||
@ -15,18 +19,6 @@
|
||||
// }
|
||||
// }
|
||||
|
||||
#root {
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
.app {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user