mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
implement versions modal
This commit is contained in:
parent
efbee533d1
commit
df32a7bbb6
|
@ -25,6 +25,7 @@
|
|||
"test:unit": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"timeago.js": "^4.0.2",
|
||||
"v-click-outside": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -546,6 +546,34 @@ export interface ITagRow {
|
|||
delete?: boolean;
|
||||
}
|
||||
|
||||
export interface IVersion {
|
||||
name: string;
|
||||
nodes: Array<{
|
||||
name: string;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
iconData: {
|
||||
type: string;
|
||||
icon?: string;
|
||||
fileBuffer?: string;
|
||||
};
|
||||
}>;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
documentationUrl: string;
|
||||
hasBreakingChange: boolean;
|
||||
hasBugFixes: boolean;
|
||||
hasCoreChanges: boolean;
|
||||
hasNewNodes: boolean;
|
||||
hasNodeEnhancements: boolean;
|
||||
hasSecurityFix: boolean;
|
||||
hasSecurityIssue: boolean;
|
||||
isAvailableOnCloud: boolean;
|
||||
isStable: boolean;
|
||||
securityIssueFixVersion: string;
|
||||
showReleaseNotification: boolean;
|
||||
}
|
||||
|
||||
export interface IRootState {
|
||||
activeExecutions: IExecutionsCurrentSummaryExtended[];
|
||||
activeWorkflows: string[];
|
||||
|
@ -604,6 +632,11 @@ export interface IUiState {
|
|||
isPageLoading: boolean;
|
||||
}
|
||||
|
||||
export interface IVersionsState {
|
||||
nextVersions: IVersion[];
|
||||
currentVersion: IVersion | undefined;
|
||||
}
|
||||
|
||||
export interface IWorkflowsState {
|
||||
}
|
||||
|
||||
|
|
7
packages/editor-ui/src/api/versions.ts
Normal file
7
packages/editor-ui/src/api/versions.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { IVersion } from '@/Interface';
|
||||
import { get } from './helpers';
|
||||
import { VERSIONS_BASE_URL } from '@/constants';
|
||||
|
||||
export async function getNextVersions(version: string): Promise<IVersion[]> {
|
||||
return await get(VERSIONS_BASE_URL, version);
|
||||
}
|
36
packages/editor-ui/src/components/Drawer.vue
Normal file
36
packages/editor-ui/src/components/Drawer.vue
Normal file
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<SlideTransition>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</SlideTransition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import SlideTransition from "./transitions/SlideTransition.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Modal",
|
||||
props: ['name'],
|
||||
components: {
|
||||
SlideTransition,
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/deep/ *, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div {
|
||||
position: fixed;
|
||||
width: 480px;
|
||||
height: 100%;
|
||||
background-color: $--drawer-background-color;
|
||||
z-index: 200;
|
||||
}
|
||||
</style>
|
|
@ -128,9 +128,9 @@
|
|||
<MenuItemsIterator :items="sidebarMenuBottomItems" :root="true"/>
|
||||
|
||||
<div class="foot-menu-items">
|
||||
<el-menu-item index="updates" class="updates">
|
||||
<el-menu-item index="updates" class="updates" v-if="hasVersionUpdates" @click="openVersionsModal">
|
||||
<i><font-awesome-icon icon="gift"/></i>
|
||||
<span slot="title" class="item-title-root">Updates available</span>
|
||||
<span slot="title" class="item-title-root">{{nextVersions.length}} update{{nextVersions.length > 1? 's': ''}} available</span>
|
||||
</el-menu-item>
|
||||
</div>
|
||||
</el-menu>
|
||||
|
@ -238,6 +238,10 @@ export default mixins(
|
|||
...mapGetters('ui', {
|
||||
isCollapsed: 'sidebarMenuCollapsed',
|
||||
}),
|
||||
...mapGetters('versions', [
|
||||
'hasVersionUpdates',
|
||||
'nextVersions',
|
||||
]),
|
||||
exeuctionId (): string | undefined {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
|
@ -317,6 +321,9 @@ export default mixins(
|
|||
openTagManager() {
|
||||
this.$store.dispatch('ui/openTagsManagerModal');
|
||||
},
|
||||
openVersionsModal() {
|
||||
this.$store.dispatch('ui/openVersionsModal');
|
||||
},
|
||||
async stopExecution () {
|
||||
const executionId = this.$store.getters.activeExecutionId;
|
||||
if (executionId === null) {
|
||||
|
@ -592,6 +599,7 @@ a.logo {
|
|||
|
||||
.el-menu-item.updates {
|
||||
color: $--sidebar-inactive-color;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
color: $--sidebar-active-color;
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible">
|
||||
<el-drawer
|
||||
v-if="drawer"
|
||||
:direction="drawerDirection"
|
||||
:visible="dialogVisible"
|
||||
:size="drawerWidth"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<template>
|
||||
<slot name="content"/>
|
||||
</template>
|
||||
</el-drawer>
|
||||
<el-dialog
|
||||
v-else
|
||||
:visible="dialogVisible"
|
||||
:before-close="closeDialog"
|
||||
:title="title"
|
||||
|
@ -23,6 +37,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import Drawer from "./Drawer.vue";
|
||||
|
||||
const sizeMap: {[size: string]: string} = {
|
||||
xl: '80%',
|
||||
|
@ -32,7 +47,10 @@ const sizeMap: {[size: string]: string} = {
|
|||
|
||||
export default Vue.extend({
|
||||
name: "Modal",
|
||||
props: ['name', 'title', 'eventBus', 'size'],
|
||||
components: {
|
||||
Drawer,
|
||||
},
|
||||
props: ['name', 'title', 'eventBus', 'size', 'drawer', 'drawerDirection', 'drawerWidth'],
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.onWindowKeydown);
|
||||
|
||||
|
@ -84,6 +102,11 @@ export default Vue.extend({
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-drawer__header {
|
||||
margin: 0;
|
||||
padding: 30px 30px 0 30px;; //todo
|
||||
}
|
||||
|
||||
.dialog-wrapper {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
|
|
@ -24,17 +24,25 @@
|
|||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
<ModalRoot :name="VERSIONS_MODAL_KEY">
|
||||
<template v-slot="{ modalName }">
|
||||
<VersionsModal
|
||||
:modalName="modalName"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY } from '@/constants';
|
||||
|
||||
import TagsManager from "@/components/TagsManager/TagsManager.vue";
|
||||
import DuplicateWorkflowDialog from "@/components/DuplicateWorkflowDialog.vue";
|
||||
import WorkflowOpen from "@/components/WorkflowOpen.vue";
|
||||
import ModalRoot from "./ModalRoot.vue";
|
||||
import VersionsModal from "./VersionsModal.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Modals",
|
||||
|
@ -43,11 +51,13 @@ export default Vue.extend({
|
|||
DuplicateWorkflowDialog,
|
||||
WorkflowOpen,
|
||||
ModalRoot,
|
||||
VersionsModal,
|
||||
},
|
||||
data: () => ({
|
||||
DUPLICATE_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
WORKLOW_OPEN_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="node-icon-wrapper" :style="iconStyleData" :class="{shrink: isSvgIcon && shrink, full: !shrink}">
|
||||
<div v-if="nodeIconData !== null" class="icon">
|
||||
<img :src="nodeIconData.path" style="max-width: 100%; max-height: 100%;" v-if="nodeIconData.type === 'file'"/>
|
||||
<font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" />
|
||||
<img :src="nodeIconData.fileBuffer || nodeIconData.path" style="max-width: 100%; max-height: 100%;" v-if="nodeIconData.type === 'file'"/>
|
||||
<font-awesome-icon :icon="nodeIconData.icon || nodeIconData.path" v-else />
|
||||
</div>
|
||||
<div v-else class="node-icon-placeholder">
|
||||
{{nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||
|
@ -54,6 +54,10 @@ export default Vue.extend({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (this.nodeType.iconData) {
|
||||
return this.nodeType.iconData;
|
||||
}
|
||||
|
||||
const restUrl = this.$store.getters.getRestUrl;
|
||||
|
||||
if (this.nodeType.icon) {
|
||||
|
|
83
packages/editor-ui/src/components/VersionCard.vue
Normal file
83
packages/editor-ui/src/components/VersionCard.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<a :href="version.documentationUrl" :class="$style.card">
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.name">
|
||||
Version {{version.name}}
|
||||
</div>
|
||||
<div :class="$style.released">
|
||||
Released {{releaseDate}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-html="version.description" :class="$style.description">
|
||||
</div>
|
||||
<div :class="$style.nodes" v-if="version.nodes && version.nodes.length > 0">
|
||||
<NodeIcon
|
||||
v-for="node in version.nodes"
|
||||
:key="node.name"
|
||||
:nodeType="node"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { format } from 'timeago.js';
|
||||
import NodeIcon from './NodeIcon.vue';
|
||||
|
||||
|
||||
export default Vue.extend({
|
||||
components: { NodeIcon },
|
||||
name: 'VersionsModal',
|
||||
props: ['version'],
|
||||
computed: {
|
||||
releaseDate() {
|
||||
return format(this.version.createdAt);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border: 1px #DBDFE7 solid;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #DBDFE7;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
color: #7D7D87;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.released {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.nodes {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
89
packages/editor-ui/src/components/VersionsModal.vue
Normal file
89
packages/editor-ui/src/components/VersionsModal.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<Modal
|
||||
:name="modalName"
|
||||
:drawer="true"
|
||||
drawerDirection="ltr"
|
||||
drawerWidth="480px"
|
||||
>
|
||||
<template slot="header">
|
||||
<p :class="$style.title"> We’ve been busy ✨</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<section :class="$style['header-content']">
|
||||
<p>You’re on {{ currentVersion.name }}, which is <strong>{{currentReleaseDate}}</strong> and {{ nextVersions.length }} version{{nextVersions.length > 1 ? 's' : ''}} behind the latest and greatest n8n</p>
|
||||
<a><font-awesome-icon icon="info-circle"></font-awesome-icon>How to update your n8n version</a>
|
||||
</section>
|
||||
<section :class="$style.versions">
|
||||
<VersionCard
|
||||
v-for="version in nextVersions"
|
||||
:key="version.name"
|
||||
:version="version"
|
||||
:class="$style['versions-card']"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { format } from 'timeago.js';
|
||||
|
||||
import Modal from './Modal.vue';
|
||||
import VersionCard from './VersionCard.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'VersionsModal',
|
||||
components: {
|
||||
Modal,
|
||||
VersionCard,
|
||||
},
|
||||
props: ['modalName'],
|
||||
computed: {
|
||||
...mapGetters('versions', [
|
||||
'nextVersions',
|
||||
'currentVersion',
|
||||
]),
|
||||
currentReleaseDate() {
|
||||
return format(this.currentVersion.createdAt).replace('ago', 'old');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.header-content {
|
||||
padding: 0px 30px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #7D7D87;
|
||||
}
|
||||
|
||||
div {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: #555555;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.versions {
|
||||
background-color: #F8F9FB;
|
||||
border-top: 1px #DBDFE7 solid;
|
||||
height: 100%;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.versions-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
|
@ -14,6 +14,7 @@ export const MAX_TAG_NAME_LENGTH = 24;
|
|||
export const DUPLICATE_MODAL_KEY = 'duplicate';
|
||||
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
||||
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
||||
export const VERSIONS_MODAL_KEY = 'versions';
|
||||
|
||||
// breakpoints
|
||||
export const BREAKPOINT_SM = 768;
|
||||
|
@ -48,3 +49,6 @@ export const HIDDEN_NODES = ['n8n-nodes-base.start'];
|
|||
export const WEBHOOK_NODE_NAME = 'n8n-nodes-base.webhook';
|
||||
export const HTTP_REQUEST_NODE_NAME = 'n8n-nodes-base.httpRequest';
|
||||
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
||||
|
||||
// versions
|
||||
export const VERSIONS_BASE_URL = `https://api-staging.n8n.io/versions/`;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
import Vue from 'vue';
|
||||
import { ActionContext, Module } from 'vuex';
|
||||
import {
|
||||
|
@ -19,6 +19,9 @@ const module: Module<IUiState, IRootState> = {
|
|||
[WORKLOW_OPEN_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[VERSIONS_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
},
|
||||
modalStack: [],
|
||||
sidebarMenuCollapsed: true,
|
||||
|
@ -58,6 +61,9 @@ const module: Module<IUiState, IRootState> = {
|
|||
openDuplicateModal: async (context: ActionContext<IUiState, IRootState>) => {
|
||||
context.commit('openModal', DUPLICATE_MODAL_KEY);
|
||||
},
|
||||
openVersionsModal: async (context: ActionContext<IUiState, IRootState>) => {
|
||||
context.commit('openModal', VERSIONS_MODAL_KEY);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
42
packages/editor-ui/src/modules/versions.ts
Normal file
42
packages/editor-ui/src/modules/versions.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { getNextVersions } from '@/api/versions';
|
||||
import { versions } from 'process';
|
||||
import { ActionContext, Module } from 'vuex';
|
||||
import {
|
||||
IRootState,
|
||||
IVersion,
|
||||
IVersionsState,
|
||||
} from '../Interface';
|
||||
|
||||
const module: Module<IVersionsState, IRootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
nextVersions: [],
|
||||
currentVersion: undefined,
|
||||
},
|
||||
getters: {
|
||||
hasVersionUpdates(state: IVersionsState) {
|
||||
return state.nextVersions.length > 0;
|
||||
},
|
||||
nextVersions(state: IVersionsState) {
|
||||
return state.nextVersions;
|
||||
},
|
||||
currentVersion(state: IVersionsState) {
|
||||
return state.currentVersion;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setVersions(state: IVersionsState, {versions, currentVersion}: {versions: IVersion[], currentVersion: string}) {
|
||||
state.nextVersions = versions.filter((version) => version.name !== currentVersion);
|
||||
state.currentVersion = versions.find((version) => version.name === currentVersion);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async fetchVersions(context: ActionContext<IVersionsState, IRootState>) {
|
||||
const currentVersion = context.rootState.versionCli;
|
||||
const versions = await getNextVersions(currentVersion);
|
||||
context.commit('setVersions', {versions, currentVersion});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default module;
|
|
@ -87,3 +87,6 @@ $--node-creator-description-color: #7d7d87;
|
|||
// trigger icon
|
||||
$--trigger-icon-border-color: #dcdfe6;
|
||||
$--trigger-icon-background-color: #fff;
|
||||
|
||||
// drawer
|
||||
$--drawer-background-color: #fff;
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
import tags from './modules/tags';
|
||||
import ui from './modules/ui';
|
||||
import workflows from './modules/workflows';
|
||||
import versions from './modules/versions';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
@ -92,6 +93,7 @@ const modules = {
|
|||
tags,
|
||||
ui,
|
||||
workflows,
|
||||
versions,
|
||||
};
|
||||
|
||||
export const store = new Vuex.Store({
|
||||
|
|
|
@ -2234,6 +2234,10 @@ export default mixins(
|
|||
this.$showError(error, 'Init Problem', 'There was a problem initializing the workflow:');
|
||||
}
|
||||
this.stopLoading();
|
||||
|
||||
setTimeout(() => {
|
||||
this.$store.dispatch('versions/fetchVersions');
|
||||
}, 0);
|
||||
});
|
||||
|
||||
this.$externalHooks().run('nodeView.mount');
|
||||
|
|
Loading…
Reference in a new issue