Feat/release note webview (#248)

* chore(deps): update dependencies

* chore: renamed custom-settings interface

* WIP: added new Webview handler main class

* WIP: added support for Settings webview

* WIP (webview): added gulp command for copying ui files

* WIP (preview): scripts for building updated

* chore: gitignore

* chore: switched to babel-preset-env and added browserify for bundling

* chore: small changes to webviews (added external interfaces file)

* chore: added new task on task explorer and small fix copy ui task

* WIP: webview HTML, JS and CSS added and ready to be developed

* chore: Test native elements

* chore(release): 2.3.0

* chore: init added release notes webview

* chore: Removed unused import

* chore: fixed build release-notes

* chore: Add release notes template

* chore: Update release notes

* chore: Update release notes template

* chore: Update release notes style

* Create stale.yml

* chore: Update release notes

* chore: Removed show-changelog command
This commit is contained in:
Alessio Occhipinti 2018-08-29 21:30:40 +02:00 committed by Mattia Astorino
parent 3ad76f1894
commit d9ea7c2ea6
28 changed files with 1544 additions and 285 deletions

View file

@ -1,5 +1,5 @@
{
"presets": [
"es2015"
"env"
]
}

17
.github/stale.yml vendored Normal file
View file

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- Discussion
- Bug
# Label to use when marking an issue as stale
staleLabel: Wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ node_modules/
/src/icons/svgs/*.svg
.tmp-output-remote-icons
_tmp-output-remote-icons
/ui/

View file

@ -7,6 +7,7 @@ export * from './tasks/themes';
export * from './tasks/watcher';
export * from './tasks/changelog-title';
export * from './tasks/get-remote-icons';
export * from './tasks/copy-ui';
// export default script
export default ['build:themes'];

28
.gulp/tasks/copy-ui.ts Normal file
View file

@ -0,0 +1,28 @@
import * as fs from 'fs';
import * as path from 'path';
import * as gulp from 'gulp';
import {PATHS} from '../../extensions/consts/paths';
import {ensureDir} from '../../extensions/helpers/fs';
/**
* For each ThemeIconVariant create a Material-Theme-Icons-{variant}.json
* depends on default Material-Theme-Icons.json
*/
export default gulp.task('build:copy-ui', callback => {
try {
ensureDir(path.resolve(PATHS.UI));
fs.copyFileSync(
path.join(PATHS.SRC, 'webviews', 'ui', 'release-notes', 'release-notes.html'),
path.join(PATHS.UI, 'release-notes.html')
);
fs.copyFileSync(
path.join(PATHS.SRC, 'webviews', 'ui', 'release-notes', 'style.css'),
path.join(PATHS.UI, 'release-notes.css')
);
} catch (error) {
return callback(error);
}
callback();
});

9
.vscode/tasks.json vendored
View file

@ -84,6 +84,15 @@
"command": "npm",
"dependsOn": "clean project",
"label": "tsc"
},
{
"args": [
"run",
"build-ui-only"
],
"command": "npm",
"label": "UI: build UI only",
"problemMatcher": []
}
]
}

View file

@ -1,4 +1,3 @@
export {default as accentsSetter} from './accents-setter';
export {default as fixIcons} from './theme-icons';
export {default as toggleApplyIcons} from './toggle-apply-icons';
export {default as showChangelog} from './show-changelog';

View file

@ -1,28 +0,0 @@
import * as path from 'path';
import * as vscode from 'vscode';
import {PATHS} from './../../consts/paths';
const previewFile = (): void => {
const uri = vscode.Uri.file(path.join(PATHS.VSIX_DIR, './CHANGELOG.md'));
vscode.commands.executeCommand('markdown.showPreview', uri);
};
export default (): void => {
const extname: string = 'vscode.markdown';
const md = vscode.extensions.getExtension<any>(extname);
if (md === undefined) {
console.warn(`Ext not found ${ extname }`);
return;
}
if (md.isActive) {
return previewFile();
}
md.activate()
.then(() => previewFile(),
reason => console.warn(reason)
);
};

View file

@ -7,6 +7,7 @@ export const PATHS: IPaths = {
ICONS: './icons',
SRC: './src',
THEMES: './themes',
UI: './ui',
VSIX_DIR: path.join(__dirname, '../../'),
};

View file

@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import {IDefaults} from './../interfaces/idefaults';
import {IThemeCustomProperties} from './../interfaces/itheme-custom-properties';
import {IThemeCustomSettings} from './../interfaces/itheme-custom-properties';
import {getPackageJSON} from './fs';
/**
@ -14,8 +14,8 @@ export function getAccent(): string | undefined {
/**
* Gets custom settings
*/
export function getCustomSettings(): IThemeCustomProperties {
return vscode.workspace.getConfiguration().get<IThemeCustomProperties>('materialTheme', {});
export function getCustomSettings(): IThemeCustomSettings {
return vscode.workspace.getConfiguration().get<IThemeCustomSettings>('materialTheme', {});
}
/**

View file

@ -19,4 +19,8 @@ export interface IPaths {
* Extension directory
*/
VSIX_DIR: string;
/**
* UI directory
*/
UI: string;
}

View file

@ -1,4 +1,4 @@
export interface IThemeCustomProperties {
export interface IThemeCustomSettings {
accent?: string;
autoApplyIcons?: boolean;
}

View file

@ -1,6 +1,7 @@
import {
workspace as Workspace,
commands as Commands
commands as Commands,
ExtensionContext
} from 'vscode';
import * as ThemeCommands from './commands';
@ -9,11 +10,13 @@ import {onChangeConfiguration} from './helpers/configuration-change';
import {changelogMessage, installationMessage} from './helpers/messages';
import checkInstallation from './helpers/check-installation';
import writeChangelog from './helpers/write-changelog';
import {ReleaseNotesWebview} from '../src/webviews/ReleaseNotes';
import handleAutoapply from './helpers/handle-autoapply';
export async function activate() {
export async function activate(context: ExtensionContext) {
const config = Workspace.getConfiguration();
const installationType = checkInstallation();
const releaseNotesView = new ReleaseNotesWebview(context);
writeChangelog();
@ -34,7 +37,7 @@ export async function activate() {
const shouldShowChangelog = (installationType.isFirstInstall || installationType.isUpdate) && await changelogMessage();
if (shouldShowChangelog) {
ThemeCommands.showChangelog();
releaseNotesView.show();
}
// Registering commands
@ -44,5 +47,6 @@ export async function activate() {
});
Commands.registerCommand('materialTheme.fixIcons', () => ThemeCommands.fixIcons());
Commands.registerCommand('materialTheme.toggleApplyIcons', () => ThemeCommands.toggleApplyIcons());
Commands.registerCommand('materialTheme.showChangelog', () => ThemeCommands.showChangelog());
Commands.registerCommand('materialTheme.showReleaseNotes', () => releaseNotesView.show());
}

View file

@ -28,17 +28,22 @@
}
},
"scripts": {
"build": "rimraf _tmp-output-remote-icons/ && yarn get-remote-icons && yarn build-icons && yarn build-themes && yarn build-icons-accents && yarn build-icons-variants && yarn build-icons-variants-json",
"build": "yarn cleanup && yarn get-remote-icons && yarn build-icons && yarn build-themes && yarn build-icons-accents && yarn build-icons-variants && yarn build-icons-variants-json && yarn build-ui",
"build-icons": "yarn remove-icons && yarn minimize-icons && gulp build:icons && yarn minimize-json",
"minimize-icons": "mkdir icons && svgo -f src/icons/svgs -o icons/",
"minimize-json": "json-minify themes/.material-theme-icons.tmp > themes/Material-Theme-Icons.json && yarn remove-icons-tmp",
"remove-icons": "rimraf icons && rimraf themes/Material-Theme-Icons.json",
"remove-icons-tmp": "rimraf themes/.material-theme-icons.tmp",
"get-remote-icons": "yarn gulp build:get-remote-icons",
"build-icons": "yarn remove-icons && yarn minimize-icons && yarn gulp build:icons && yarn minimize-json",
"build-icons-accents": "yarn gulp build:icons.accents",
"build-icons-variants": "yarn gulp build:icons.variants",
"build-icons-variants-json": "yarn gulp build:icons.variants-json",
"build-themes": "yarn gulp build:themes",
"get-remote-icons": "gulp build:get-remote-icons",
"cleanup": "rimraf _tmp-output-remote-icons/ && rimraf ui",
"build-icons-accents": "gulp build:icons.accents",
"build-icons-variants": "gulp build:icons.variants",
"build-icons-variants-json": "gulp build:icons.variants-json",
"build-themes": "gulp build:themes",
"build-ui": "yarn copy-ui && yarn build-ui-release-notes",
"copy-ui": "gulp build:copy-ui",
"build-ui-release-notes": "browserify src/webviews/ui/release-notes/index.js > ui/release-notes.js",
"build-ui-only": "yarn cleanup && yarn build-ts && yarn build-ui",
"build-ts": "tsc -p ./tsconfig.json",
"test": "tslint **.ts",
"release": "standard-version",
@ -74,8 +79,8 @@
"category": "🎨 Material Theme"
},
{
"command": "materialTheme.showChangelog",
"title": "Show changelog",
"command": "materialTheme.showReleaseNotes",
"title": "Release Notes",
"category": "🎨 Material Theme"
}
],
@ -214,16 +219,18 @@
"@types/gulp": "4.0.5",
"@types/gulp-if": "0.0.33",
"@types/gulp-util": "3.0.34",
"@types/mustache": "0.8.30",
"@types/mustache": "0.8.31",
"@types/ncp": "2.0.1",
"@types/rimraf": "2.0.2",
"@types/run-sequence": "0.0.30",
"@types/through2": "2.0.33",
"@types/yamljs": "0.2.30",
"@types/yargs": "10.0.1",
"babel-core": "6.26.0",
"@types/yargs": "11.0.0",
"babel-core": "6.26.3",
"babel-preset-env": "1.7.0",
"babel-preset-es2015": "6.24.1",
"babel-root-import": "4.1.8",
"browserify": "16.2.2",
"cpx": "1.5.0",
"execa": "0.10.0",
"gulp": "3.9.1",
@ -237,12 +244,12 @@
"mustache": "2.3.0",
"ncp": "2.0.0",
"run-sequence": "2.2.1",
"standard-version": "4.3.0",
"svgo": "1.0.4",
"tslint": "5.9.1",
"tslint-xo": "0.7.2",
"typescript": "2.8.3",
"vscode": "1.1.10",
"standard-version": "4.4.0",
"svgo": "1.0.5",
"tslint": "5.10.0",
"tslint-xo": "0.8.0",
"typescript": "2.9.2",
"vscode": "1.1.18",
"yamljs": "0.3.0",
"yargs": "11.0.0"
},

View file

@ -0,0 +1,33 @@
import {WebviewController} from './Webview';
import {
ExtensionContext
} from 'vscode';
import {ReleaseNotesBootstrap} from './interfaces';
export class ReleaseNotesWebview extends WebviewController<ReleaseNotesBootstrap> {
constructor(context: ExtensionContext) {
super(context);
}
get filename(): string {
return 'release-notes.html';
}
get id(): string {
return 'materialTheme.releaseNotes';
}
get title(): string {
return 'Material Theme Release Notes';
}
/**
* This will be called by the WebviewController when init the view
* passing as `window.bootstrap` to the view.
*/
getBootstrap() {
return {
something: 'something'
} as ReleaseNotesBootstrap;
}
}

50
src/webviews/Settings.ts Normal file
View file

@ -0,0 +1,50 @@
import {WebviewController} from './Webview';
import {
workspace as Workspace,
ExtensionContext
} from 'vscode';
import {SettingsBootstrap} from './interfaces';
import {getCustomSettings} from '../../extensions/helpers/settings';
import {getDefaultValues} from '../../extensions/helpers/fs';
export class SettingsWebview extends WebviewController<SettingsBootstrap> {
constructor(context: ExtensionContext) {
super(context);
}
get filename(): string {
return 'settings.html';
}
get id(): string {
return 'materialTheme.settings';
}
get title(): string {
return 'Material Theme Settings';
}
private getAvailableScopes(): ['user' | 'workspace', string][] {
const scopes: ['user' | 'workspace', string][] = [['user', 'User']];
return scopes
.concat(
Workspace.workspaceFolders !== undefined && Workspace.workspaceFolders.length ?
['workspace', 'Workspace'] :
[]
);
}
/**
* This will be called by the WebviewController when init the view
* passing as `window.bootstrap` to the view.
*/
getBootstrap() {
return {
config: getCustomSettings(),
defaults: getDefaultValues(),
scope: 'user',
scopes: this.getAvailableScopes()
} as SettingsBootstrap;
}
}

157
src/webviews/Webview.ts Normal file
View file

@ -0,0 +1,157 @@
import * as path from 'path';
import {
workspace as Workspace,
Disposable,
ExtensionContext,
WebviewPanel,
ViewColumn,
window,
WebviewPanelOnDidChangeViewStateEvent,
Uri
} from 'vscode';
import {getCustomSettings} from '../../extensions/helpers/settings';
import {Invalidates, Message, SettingsChangedMessage} from './interfaces';
export abstract class WebviewController<TBootstrap> extends Disposable {
private panel: WebviewPanel | undefined;
private disposablePanel: Disposable | undefined;
private invalidateOnVisible: Invalidates;
private context: ExtensionContext;
constructor(context: ExtensionContext) {
// Applying dispose callback for our disposable function
super(() => this.dispose());
this.context = context;
}
abstract get filename(): string;
abstract get id(): string;
abstract get title(): string;
abstract getBootstrap(): TBootstrap;
dispose() {
if (this.disposablePanel) {
this.disposablePanel.dispose();
}
}
private async getHtml(): Promise<string> {
const doc = await Workspace
.openTextDocument(this.context.asAbsolutePath(path.join('ui', this.filename)));
return doc.getText();
}
private postMessage(message: Message, invalidates: Invalidates = 'all') {
if (this.panel === undefined) {
return false;
}
const result = this.panel.webview.postMessage(message);
// If post was ok, update invalidateOnVisible if different than default
if (!result && this.invalidateOnVisible !== 'all') {
this.invalidateOnVisible = invalidates;
}
return result;
}
private postUpdatedConfiguration() {
// Post full raw configuration
return this.postMessage({
type: 'settingsChanged',
config: getCustomSettings()
} as SettingsChangedMessage, 'config');
}
private onPanelDisposed() {
if (this.disposablePanel) {
this.disposablePanel.dispose();
}
this.panel = undefined;
}
private onViewStateChanged(event: WebviewPanelOnDidChangeViewStateEvent) {
console.log('WebviewEditor.onViewStateChanged', event.webviewPanel.visible);
if (!this.invalidateOnVisible || !event.webviewPanel.visible) {
return;
}
// Update the view since it can be outdated
const invalidContext = this.invalidateOnVisible;
this.invalidateOnVisible = undefined;
switch (invalidContext) {
case 'config':
// Post the new configuration to the view
return this.postUpdatedConfiguration();
default:
return this.show();
}
}
protected async onMessageReceived(event: Message) {
if (event === null) {
return;
}
console.log(`WebviewEditor.onMessageReceived: type=${event.type}, data=${JSON.stringify(event)}`);
switch (event.type) {
case 'saveSettings':
// TODO: update settings
return;
default:
return;
}
}
async show(): Promise<void> {
const html = await this.getHtml();
const rootPath = Uri
.file(this.context.asAbsolutePath('.'))
.with({scheme: 'vscode-resource'}).toString();
// Replace placeholders in html content for assets and adding configurations as `window.bootstrap`
const fullHtml = html
.replace(/{{root}}/g, rootPath)
.replace('\'{{bootstrap}}\'', JSON.stringify(this.getBootstrap()));
// If panel already opened just reveal
if (this.panel !== undefined) {
this.panel.webview.html = fullHtml;
return this.panel.reveal(ViewColumn.Active);
}
this.panel = window.createWebviewPanel(
this.id,
this.title,
ViewColumn.Active,
{
retainContextWhenHidden: true,
enableFindWidget: true,
enableCommandUris: true,
enableScripts: true
}
);
// Applying listeners
this.disposablePanel = Disposable.from(
this.panel,
this.panel.onDidDispose(this.onPanelDisposed, this),
this.panel.onDidChangeViewState(this.onViewStateChanged, this),
this.panel.webview.onDidReceiveMessage(this.onMessageReceived, this)
);
this.panel.webview.html = fullHtml;
}
}

View file

@ -0,0 +1,38 @@
import {IThemeCustomSettings} from '../../extensions/interfaces/itheme-custom-properties';
import {IDefaults} from '../../extensions/interfaces/idefaults';
export interface SettingsChangedMessage {
type: 'settingsChanged';
config: IThemeCustomSettings;
}
export interface SaveSettingsMessage {
type: 'saveSettings';
changes: {
[key: string]: any;
};
removes: string[];
scope: 'user' | 'workspace';
uri: string;
}
export type Message = SaveSettingsMessage | SettingsChangedMessage;
export type Invalidates = 'all' | 'config' | undefined;
export interface Bootstrap {
config: IThemeCustomSettings;
}
export interface SettingsBootstrap extends Bootstrap {
scope: 'user' | 'workspace';
scopes: ['user' | 'workspace', string][];
defaults: IDefaults;
}
export interface ReleaseNotesBootstrap extends Bootstrap {
something: 'something';
}
declare global {
interface Window { bootstrap: Bootstrap | SettingsBootstrap; }
}

View file

@ -0,0 +1 @@
console.log('ReleaseNotes');

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Material Theme Release Notes</title>
<link rel="stylesheet" href="{{root}}/ui/release-notes.css">
</head>
<body>
<main class="Container">
<section class="Release">
<header class="Release__Header">
<span class="Release__Number">2.3.0</span>
<h2 class="Release__Title">August 2018</h2>
</header>
<ul class="Release-List">
<li data-type="breaking">New file icons auto applying and notifications behaviors</li>
<li data-type="fixed">Many UI fixes and small improvements</li>
<li data-type="fixed">General colors and contrast</li>
<li data-type="fixed">Fix color contrast for hovered custom menu items (Win/Lin)</li>
<li data-type="new">New file icons</li>
<li data-type="new">Support for custom menus (Windows/Linux)</li>
<li data-type="new">Support for the editor breadcrumb</li>
<li data-type="new">Support for editor new settings view</li>
<li data-type="new">Add new Release Notes command</li>
<li data-type="new">Add icons to root folders</li>
</ul>
</section>
</main>
<script type="text/javascript">
window.bootstrap = '{{bootstrap}}';
</script>
<script type="text/javascript" src="{{root}}/ui/release-notes.js"></script>
</body>
</html>

View file

@ -0,0 +1,137 @@
body {
font-size: 100%;
font-family: sans-serif;
margin: 0;
}
.Container {
max-width: 900px;
min-width: 500px;
width: 100%;
display: block;
margin: 0 auto;
padding: 0 32px;
box-sizing: border-box;
}
.Release {
padding: 40px 0;
position: relative;
}
.Release:first-of-type::before {
background-image: linear-gradient(
to bottom,
rgba(255,255,255,0),
rgba(255,255,255,0.1)
50px
);
}
.Release:last-of-type::before,
.Release:only-of-type::before {
background-image: linear-gradient(
to bottom,
rgba(255,255,255,0.1),
rgba(255,255,255,0)
);
}
.Release::before {
content: "";
background-image: linear-gradient(to bottom, rgba(255,255,255,0.1), rgba(255,255,255,0.1));
width: 3px;
position: absolute;
top: 0;
bottom: 0;
left: calc(65px / 2);
z-index: -1;
}
.Release__Header {
display: flex;
align-items: center;
}
.Release__Number {
background-color: rgba(0, 0, 0, 0.5);
color: #FFF;
font-weight: 700;
text-align: center;
padding: 4px;
width: 65px;
margin-right: 16px;
font-size: 0.8rem;
line-height: 1.5;
border-radius: 4px;
cursor: default;
-webkit-user-select: none;
user-select: none;
box-sizing: border-box;
}
.Release__Title {
margin-top: 0;
line-height: 1;
margin-bottom: 0;
font-weight: 400;
cursor: default;
-webkit-user-select: none;
user-select: none;
}
.Release-List {
list-style: none;
padding-left: 80px;
}
.Release-List li {
line-height: 1.5;
display: flex;
font-size: 14px;
margin: 8px 0;
-webkit-user-select: none;
user-select: none;
cursor: default;
white
}
.Release-List li::before {
content: attr(data-type);
display: inline-block;
font-size: 10px;
line-height: 1;
box-sizing: border-box;
width: 64px;
height: 16px;
border-radius: 3px;
margin-right: 8px;
padding: 4px 5px;
text-transform: uppercase;
text-align: center;
color: #FFF;
}
[data-type="fixed"]::before {
background-color: #4242f4;
}
[data-type="improved"]::before {
background-color: #6f42c1;
}
[data-type="new"]::before {
background-color: #05a87a;
}
[data-type="breaking"]::before {
background-color: #ba0935;
}

View file

@ -0,0 +1,20 @@
import {SettingsBootstrap} from '../../interfaces';
import accentsSelector from './lib/accents-selector';
const run = () => {
bind();
const {config, defaults} = window.bootstrap as SettingsBootstrap;
accentsSelector('[data-setting="accentSelector"]', defaults.accents, config.accent);
console.log(defaults);
console.log(config);
};
const bind = () => {
document.querySelector('#fixIconsCTA').addEventListener('click', () => {
console.log('Test click');
});
};
run();

View file

@ -0,0 +1,26 @@
import {IAccents} from '../../../../../extensions/interfaces/idefaults';
const templateSingleAccent = (accentName: string, accentColor: string): string => {
const dashAccentName = accentName.toLowerCase().replace(/ /gi, '-');
return `
<label for="${dashAccentName}" data-color="${accentColor}">${accentName}</label>
<input type="radio" name="accents" id="${dashAccentName}" value="${dashAccentName}" />
`;
};
export default (containerSelector: string, accentsObject: IAccents, currentAccent: string) => {
const container = document.querySelector(containerSelector);
for (const accentKey of Object.keys(accentsObject)) {
const el = document.createElement('div');
el.innerHTML = templateSingleAccent(accentKey, accentsObject[accentKey]);
if (accentKey === currentAccent) {
el.setAttribute('selected', 'true');
el.querySelector('input').setAttribute('checked', 'checked');
}
container.appendChild(el);
}
};

View file

@ -1,7 +1,8 @@
{
"compilerOptions": {
"lib": [
"es6"
"es6",
"dom"
],
"module": "commonjs",
"allowUnreachableCode": false,

View file

@ -2,6 +2,7 @@
"extends": "tslint-xo",
"rules": {
"comment-format": false,
"indent": [true, "spaces"]
"indent": [true, "spaces"],
"no-inner-html": false
}
}

1161
yarn.lock

File diff suppressed because it is too large Load diff