directus-extension-flow-manager 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,18 @@
1
1
  # directus-extension-flow-manager
2
+ This directus module extension allows you to manage your flow content from directus.
3
+
4
+ You can install it via ``npm install directus-extension-flow-manager``
5
+
6
+ - [x] Duplicate flow
7
+ - [x] Export and import flow
8
+ - [ ] Add flow validation when Restore
9
+
10
+ Screenshoots
11
+ ![Alt text](screenshoots/image.png)
12
+
13
+ Changelogs:
14
+ - 1.0.0: (13 July 2023)
15
+ * Initial release
16
+ - 1.1.0: (21 July 2023)
17
+ * Add Backup and Restore feature
18
+ * Allow user to click the flow row and bring to flow detail page
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{useStores as e,useApi as t,defineModule as o}from"@directus/extensions-sdk";import{defineComponent as n,ref as i,unref as a,resolveComponent as l,resolveDirective as r,openBlock as s,createBlock as c,withCtx as u,withDirectives as d,createVNode as p,createCommentVNode as f,normalizeClass as v,createTextVNode as m,toDisplayString as g,createElementVNode as y}from"vue";import{useRouter as w}from"vue-router";var h=n({setup(){const{useFlowsStore:o,useNotificationsStore:n}=e(),l=o(),r=n(),s=t(),c=w(),u=i(l.flows),d=i(null);return{headers:i([{text:"",value:"icon",width:50,sortable:!1},{text:"Status",value:"status"},{text:"Name",value:"name",width:400}]),flows:u,restoredFile:d,duplicate:p,backup:async function(e){const t={name:e.name,icon:e.icon,color:e.color,description:e.description,trigger:e.trigger,options:e.options,operation:e.operation,operations:e.operations.map((e=>({id:e.id,name:e.name,key:e.key,type:e.type,position_x:e.position_x,position_y:e.position_y,options:e.options,resolve:e.resolve,reject:e.reject})))},o=new Blob([JSON.stringify(t,null,2)],{type:"application/json"});var n=window.URL.createObjectURL(o),i=document.createElement("a");i.href=n,i.setAttribute("download",`${function(){const e=new Date,t=e.getFullYear(),o=(e.getMonth()+1).toString().padStart(2,"0"),n=e.getDate().toString().padStart(2,"0"),i=e.getHours().toString().padStart(2,"0"),a=e.getMinutes().toString().padStart(2,"0"),l=e.getSeconds().toString().padStart(2,"0");return`${t}${o}${n}${i}${a}${l}`}()}-${e.name}.json`),document.body.appendChild(i),i.click()},onRestoredFileChanged:function(e){var t;const o=null==(t=null==e?void 0:e.target)?void 0:t.files[0];if(!o)return;const n=new FileReader;n.onload=async e=>{var t;try{const o=null==(t=e.target)?void 0:t.result,n=JSON.parse(o);console.log({parsedResult:n}),await p(n,!1)}catch(e){console.log(e)}},n.readAsText(o)},onRestoreButtonClicked:function(){var e;console.log("restore button clicked"),null==(e=d.value)||e.click()},goToFlow:function({item:e}){console.log("go to flow",e),c.push(`/settings/flows/${e.id}`)}};async function p(e,t=!0){try{let o=function(t){const o={};function i(e){return t.find((t=>t.id===e))}function a(e){if(!e)return null;return{name:e.name,position_x:e.position_x,position_y:e.position_y,key:e.key,type:e.type,options:e.options,flow:n.data.data.id,resolve:a(i(e.resolve)),reject:a(i(e.reject))}}const l=i(e.operation);return o.name=null==l?void 0:l.name,o.position_x=null==l?void 0:l.position_x,o.position_y=null==l?void 0:l.position_y,o.key=null==l?void 0:l.key,o.type=null==l?void 0:l.type,o.options=null==l?void 0:l.options,o.flow=n.data.data.id,o.resolve=a(i((null==l?void 0:l.resolve)||null)),o.reject=a(i((null==l?void 0:l.reject)||null)),o};const n=await s.post("/flows",{name:t?`${e.name} - Copy`:e.name,status:"inactive",icon:e.icon,accountability:e.accountability,description:e.description,trigger:e.trigger,options:e.options}),i=o(e.operations);await s.patch(`/flows/${n.data.data.id}`,{operation:e.operation?i:null}),await l.hydrate(),u.value=a(l.flows),r.add({type:"success",title:t?"Flow Duplicated successfully":`Flow ${e.name} restored successfully`,closeable:!0,persist:!0})}catch(e){console.log(e)}}}});var b=[],k=[];!function(e,t){if(e&&"undefined"!=typeof document){var o,n=!0===t.prepend?"prepend":"append",i=!0===t.singleTag,a="string"==typeof t.container?document.querySelector(t.container):document.getElementsByTagName("head")[0];if(i){var l=b.indexOf(a);-1===l&&(l=b.push(a)-1,k[l]={}),o=k[l]&&k[l][n]?k[l][n]:k[l][n]=r()}else o=r();65279===e.charCodeAt(0)&&(e=e.substring(1)),o.styleSheet?o.styleSheet.cssText+=e:o.appendChild(document.createTextNode(e))}function r(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),t.attributes)for(var o=Object.keys(t.attributes),i=0;i<o.length;i++)e.setAttribute(o[i],t.attributes[o[i]]);var l="prepend"===n?"afterbegin":"beforeend";return a.insertAdjacentElement(l,e),e}}(".active-chip[data-v-b52a34f6] {\n background-color: var(--primary);\n}\n.inactive-chip[data-v-b52a34f6] {\n background-color: var(--foreground-subdued);\n}",{}),h.render=function(e,t,o,n,i,a){const w=l("v-icon"),h=l("v-chip"),b=l("v-list-item-icon"),k=l("v-list-item-content"),_=l("v-list-item"),C=l("v-list"),S=l("v-menu"),x=l("v-table"),F=l("v-button"),j=l("private-view"),$=r("tooltip");return s(),c(j,{title:"Flow Manager"},{actions:u((()=>[d((s(),c(F,{rounded:"",icon:"",onClick:e.onRestoreButtonClicked},{default:u((()=>[p(w,{name:"file_upload"})])),_:1},8,["onClick"])),[[$,"Restore",void 0,{bottom:!0}]])])),default:u((()=>[p(x,{items:e.flows,headers:e.headers,"onUpdate:headers":t[0]||(t[0]=t=>e.headers=t),"onClick:row":e.goToFlow},{"item.icon":u((({item:e})=>[e.icon?(s(),c(w,{key:0,name:e.icon},null,8,["name"])):f("v-if",!0)])),"item.status":u((({item:e})=>[p(h,{rounded:"",class:v("active"===e.status?"active-chip":"inactive-chip")},{default:u((()=>[m(g(e.status),1)])),_:2},1032,["class"])])),"item-append":u((({item:t})=>[p(S,{placement:"bottom-end","show-arrow":"","close-on-content-click":!0},{activator:u((({toggle:e})=>[p(w,{name:"more_vert",class:"ctx-toggle",onClick:e},null,8,["onClick"])])),default:u((()=>[p(C,null,{default:u((()=>[p(_,{clickable:"",onClick:o=>e.duplicate(t)},{default:u((()=>[p(b,null,{default:u((()=>[p(w,{name:"content_copy"})])),_:1}),p(k,null,{default:u((()=>[m(" Duplicate ")])),_:1})])),_:2},1032,["onClick"]),p(_,{clickable:"",onClick:o=>e.backup(t)},{default:u((()=>[p(b,null,{default:u((()=>[p(w,{name:"file_download"})])),_:1}),p(k,null,{default:u((()=>[m(" Backup ")])),_:1})])),_:2},1032,["onClick"])])),_:2},1024)])),_:2},1024)])),_:2},1032,["items","headers","onClick:row"]),y("input",{ref:"restoredFile",type:"file",accept:"application/json",onChange:t[1]||(t[1]=(...t)=>e.onRestoredFileChanged&&e.onRestoredFileChanged(...t)),style:{display:"none"}},null,544)])),_:1})},h.__scopeId="data-v-b52a34f6",h.__file="src/module.vue";var _=o({id:"flow-manager",name:"Flow Manager",icon:"bolt",routes:[{path:"",component:h}]});export{_ as default};
package/package.json CHANGED
@@ -2,8 +2,10 @@
2
2
  "name": "directus-extension-flow-manager",
3
3
  "description": "This is a custom module for managing Flow",
4
4
  "icon": "extension",
5
- "version": "1.0.0",
6
- "author": "Bagus Andreanto<andreanto.bagus@gmail.com>",
5
+ "version": "1.1.0",
6
+ "author": "Bagus Andreanto<andreanto.bagus@gmail.com>",
7
+ "homepage": "https://github.com/baguse/directus-extension-flow-manager",
8
+ "license": "MIT",
7
9
  "keywords": [
8
10
  "directus",
9
11
  "directus-extension",
@@ -26,6 +28,7 @@
26
28
  "vue": "^3.3.4"
27
29
  },
28
30
  "dependencies": {
29
- "sass": "^1.63.6"
31
+ "sass": "^1.63.6",
32
+ "vue-router": "^4.2.4"
30
33
  }
31
34
  }
Binary file
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import ModuleComponent from './module.vue';
4
4
  export default defineModule({
5
5
  id: 'flow-manager',
6
6
  name: 'Flow Manager',
7
- icon: 'box',
7
+ icon: 'bolt',
8
8
  routes: [
9
9
  {
10
10
  path: '',
package/src/module.vue CHANGED
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <private-view title="Flow Manager">
3
- <v-table :items="flows" v-model:headers="headers">
3
+ <v-table :items="flows" v-model:headers="headers" @click:row="goToFlow">
4
4
  <template #[`item.icon`]="{ item }">
5
5
  <v-icon v-if="item.icon" :name="item.icon" />
6
6
  </template>
@@ -21,16 +21,31 @@
21
21
  </v-list-item-icon>
22
22
  <v-list-item-content> Duplicate </v-list-item-content>
23
23
  </v-list-item>
24
+ <v-list-item clickable @click="backup(item)">
25
+ <v-list-item-icon>
26
+ <v-icon name="file_download" />
27
+ </v-list-item-icon>
28
+ <v-list-item-content> Backup </v-list-item-content>
29
+ </v-list-item>
24
30
  </v-list>
25
31
  </v-menu>
26
32
  </template>
27
33
  </v-table>
34
+
35
+ <template #actions>
36
+ <v-button v-tooltip.bottom="'Restore'" rounded icon @click="onRestoreButtonClicked">
37
+ <v-icon name="file_upload" />
38
+ </v-button>
39
+ </template>
40
+
41
+ <input ref="restoredFile" type="file" accept="application/json" @change="onRestoredFileChanged" style="display: none" />
28
42
  </private-view>
29
43
  </template>
30
44
 
31
45
  <script lang="ts">
32
46
  import { defineComponent, ref, unref } from "vue";
33
47
  import { useStores, useApi } from "@directus/extensions-sdk";
48
+ import {useRouter} from 'vue-router'
34
49
 
35
50
  interface IOperation {
36
51
  id: string;
@@ -78,9 +93,12 @@ export default defineComponent({
78
93
  const flowsStore = useFlowsStore();
79
94
  const notificationsStore = useNotificationsStore();
80
95
  const api = useApi();
96
+ const router = useRouter();
81
97
 
82
98
  const flows = ref(flowsStore.flows);
83
99
 
100
+ const restoredFile = ref(null);
101
+
84
102
  const headers = ref([
85
103
  {
86
104
  text: "",
@@ -102,13 +120,18 @@ export default defineComponent({
102
120
  return {
103
121
  headers,
104
122
  flows,
123
+ restoredFile,
105
124
  duplicate,
125
+ backup,
126
+ onRestoredFileChanged,
127
+ onRestoreButtonClicked,
128
+ goToFlow,
106
129
  };
107
130
 
108
- async function duplicate(item: IFlow) {
131
+ async function duplicate(item: IFlow, isDuplicate = true) {
109
132
  try {
110
133
  const response = await api.post("/flows", {
111
- name: `${item.name} - Copy`,
134
+ name: isDuplicate ? `${item.name} - Copy` : item.name,
112
135
  status: "inactive",
113
136
  icon: item.icon,
114
137
  accountability: item.accountability,
@@ -169,13 +192,87 @@ export default defineComponent({
169
192
 
170
193
  notificationsStore.add({
171
194
  type: "success",
172
- text: "Flow duplicated successfully",
195
+ title: isDuplicate ? "Flow Duplicated successfully" : `Flow ${item.name} restored successfully`,
173
196
  closeable: true,
197
+ persist: true,
174
198
  });
175
199
  } catch (error) {
176
200
  console.log(error);
177
201
  }
178
202
  }
203
+
204
+ function getTimestamp() {
205
+ const date = new Date();
206
+ const year = date.getFullYear();
207
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
208
+ const day = date.getDate().toString().padStart(2, "0");
209
+ const hour = date.getHours().toString().padStart(2, "0");
210
+ const minute = date.getMinutes().toString().padStart(2, "0");
211
+ const second = date.getSeconds().toString().padStart(2, "0");
212
+
213
+ return `${year}${month}${day}${hour}${minute}${second}`;
214
+ }
215
+
216
+ async function backup(item: IFlow) {
217
+ interface ISanitizedFlow extends Partial<Omit<IFlow, "operations">> {
218
+ operations: Partial<IOperation>[];
219
+ }
220
+ const sanitizedFlow: ISanitizedFlow = {
221
+ name: item.name,
222
+ icon: item.icon,
223
+ color: item.color,
224
+ description: item.description,
225
+ trigger: item.trigger,
226
+ options: item.options,
227
+ operation: item.operation,
228
+ operations: item.operations.map((operation) => ({
229
+ id: operation.id,
230
+ name: operation.name,
231
+ key: operation.key,
232
+ type: operation.type,
233
+ position_x: operation.position_x,
234
+ position_y: operation.position_y,
235
+ options: operation.options,
236
+ resolve: operation.resolve,
237
+ reject: operation.reject,
238
+ })),
239
+ };
240
+ const blob = new Blob([JSON.stringify(sanitizedFlow, null, 2)], { type: "application/json" });
241
+ var fileObj = window.URL.createObjectURL(blob);
242
+
243
+ var docUrl = document.createElement("a");
244
+ docUrl.href = fileObj;
245
+ docUrl.setAttribute("download", `${getTimestamp()}-${item.name}.json`);
246
+ document.body.appendChild(docUrl);
247
+ docUrl.click();
248
+ }
249
+
250
+ function onRestoredFileChanged($event: Event) {
251
+ const file: File = $event?.target?.files[0];
252
+ if (!file) return;
253
+ const reader = new FileReader();
254
+ reader.onload = async (e) => {
255
+ try {
256
+ const result = e.target?.result;
257
+ const parsedResult = JSON.parse(result as string);
258
+ console.log({ parsedResult });
259
+ await duplicate(parsedResult, false);
260
+ } catch (error) {
261
+ console.log(error);
262
+ }
263
+ };
264
+ reader.readAsText(file);
265
+ }
266
+
267
+ function onRestoreButtonClicked() {
268
+ console.log("restore button clicked");
269
+ restoredFile.value?.click();
270
+ }
271
+
272
+ function goToFlow({ item }: { item: IFlow }) {
273
+ console.log("go to flow", item);
274
+ router.push(`/settings/flows/${item.id}`);
275
+ }
179
276
  },
180
277
  });
181
278
  </script>