Browse Source

feat: add team workflow

Ahmad Kholid 3 years ago
parent
commit
f39ce0b171

+ 2 - 1
src/components/newtab/shared/SharedPermissionsModal.vue

@@ -42,7 +42,7 @@ const props = defineProps({
     default: () => [],
     default: () => [],
   },
   },
 });
 });
-const emit = defineEmits(['update:modelValue']);
+const emit = defineEmits(['update:modelValue', 'granted']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
 
 
@@ -58,6 +58,7 @@ function requestPermission() {
     .request({ permissions: toRaw(props.permissions) })
     .request({ permissions: toRaw(props.permissions) })
     .then(() => {
     .then(() => {
       emit('update:modelValue', false);
       emit('update:modelValue', false);
+      emit('granted', true);
     });
     });
 }
 }
 </script>
 </script>

+ 3 - 1
src/components/newtab/shared/SharedWysiwyg.vue

@@ -2,7 +2,7 @@
   <div class="wysiwyg-editor">
   <div class="wysiwyg-editor">
     <slot v-if="editor" name="prepend" :editor="editor" />
     <slot v-if="editor" name="prepend" :editor="editor" />
     <div
     <div
-      v-if="editor"
+      v-if="editor && !readonly"
       class="p-2 rounded-lg backdrop-blur flex items-center sticky top-0 z-50 bg-box-transparent space-x-1 mb-2"
       class="p-2 rounded-lg backdrop-blur flex items-center sticky top-0 z-50 bg-box-transparent space-x-1 mb-2"
     >
     >
       <button
       <button
@@ -115,6 +115,7 @@ const props = defineProps({
     type: Object,
     type: Object,
     default: () => ({}),
     default: () => ({}),
   },
   },
+  readonly: Boolean,
 });
 });
 const emit = defineEmits(['update:modelValue', 'count', 'change']);
 const emit = defineEmits(['update:modelValue', 'count', 'change']);
 
 
@@ -172,6 +173,7 @@ watch(
 onMounted(() => {
 onMounted(() => {
   editor.value = new Editor({
   editor.value = new Editor({
     content: props.modelValue,
     content: props.modelValue,
+    editable: !props.readonly,
     onUpdate: () => {
     onUpdate: () => {
       const editorValue = editor.value.getJSON();
       const editorValue = editor.value.getJSON();
 
 

+ 25 - 6
src/components/newtab/workflow/WorkflowEditor.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <vue-flow
   <vue-flow
     :id="props.id"
     :id="props.id"
-    :class="{ disabled: options.disabled }"
+    :class="{ disabled: isDisabled }"
     :default-edge-options="{
     :default-edge-options="{
       updatable: true,
       updatable: true,
       selectable: true,
       selectable: true,
@@ -55,7 +55,7 @@
   </vue-flow>
   </vue-flow>
 </template>
 </template>
 <script setup>
 <script setup>
-import { onMounted, onBeforeUnmount } from 'vue';
+import { onMounted, onBeforeUnmount, watch, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import {
 import {
   VueFlow,
   VueFlow,
@@ -96,6 +96,7 @@ const props = defineProps({
     type: Boolean,
     type: Boolean,
     default: true,
     default: true,
   },
   },
+  disabled: Boolean,
 });
 });
 const emit = defineEmits(['edit', 'init', 'update:node', 'delete:node']);
 const emit = defineEmits(['edit', 'init', 'update:node', 'delete:node']);
 
 
@@ -150,6 +151,7 @@ editor.onEdgeUpdate(({ edge, connection }) => {
 });
 });
 
 
 const settings = store.settings.editor;
 const settings = store.settings.editor;
+const isDisabled = computed(() => props.options.disabled ?? props.disabled);
 
 
 function minimapNodeClassName({ label }) {
 function minimapNodeClassName({ label }) {
   const { category } = tasks[label];
   const { category } = tasks[label];
@@ -158,7 +160,7 @@ function minimapNodeClassName({ label }) {
   return color;
   return color;
 }
 }
 function updateBlockData(nodeId, data = {}) {
 function updateBlockData(nodeId, data = {}) {
-  if (props.options.disabled) return;
+  if (isDisabled.value) return;
 
 
   const node = editor.getNode.value(nodeId);
   const node = editor.getNode.value(nodeId);
   node.data = { ...node.data, ...data };
   node.data = { ...node.data, ...data };
@@ -166,7 +168,7 @@ function updateBlockData(nodeId, data = {}) {
   emit('update:node', node);
   emit('update:node', node);
 }
 }
 function editBlock({ id, label, data }, additionalData = {}) {
 function editBlock({ id, label, data }, additionalData = {}) {
-  if (props.options.disabled) return;
+  if (isDisabled.value) return;
 
 
   emit('edit', {
   emit('edit', {
     id: label,
     id: label,
@@ -176,13 +178,13 @@ function editBlock({ id, label, data }, additionalData = {}) {
   });
   });
 }
 }
 function deleteBlock(nodeId) {
 function deleteBlock(nodeId) {
-  if (props.options.disabled) return;
+  if (isDisabled.value) return;
 
 
   editor.removeNodes([nodeId]);
   editor.removeNodes([nodeId]);
   emit('delete:node', nodeId);
   emit('delete:node', nodeId);
 }
 }
 function onMousedown(event) {
 function onMousedown(event) {
-  if (props.options.disabled && event.shiftKey) {
+  if (isDisabled.value && event.shiftKey) {
     event.stopPropagation();
     event.stopPropagation();
     event.preventDefault();
     event.preventDefault();
   }
   }
@@ -202,6 +204,23 @@ function applyFlowData() {
   });
   });
 }
 }
 
 
+watch(
+  () => props.disabled,
+  (value) => {
+    const keys = [
+      'nodesDraggable',
+      'edgesUpdatable',
+      'nodesConnectable',
+      'elementsSelectable',
+    ];
+
+    keys.forEach((key) => {
+      editor[key].value = !value;
+    });
+  },
+  { immediate: true }
+);
+
 onMounted(() => {
 onMounted(() => {
   applyFlowData();
   applyFlowData();
   window.addEventListener('mousedown', onMousedown, true);
   window.addEventListener('mousedown', onMousedown, true);

+ 54 - 40
src/components/newtab/workflow/WorkflowShareTeam.vue

@@ -1,9 +1,12 @@
 <template>
 <template>
   <ui-card class="w-full max-w-4xl share-workflow overflow-auto scroll">
   <ui-card class="w-full max-w-4xl share-workflow overflow-auto scroll">
-    <h1 class="text-xl font-semibold">Share workflow with team</h1>
-    <p class="text-gray-600 dark:text-gray-200">
-      This workflow will be shared with your team
-    </p>
+    <template v-if="!isUpdate">
+      <h1 class="text-xl font-semibold">Share workflow with team</h1>
+      <p class="text-gray-600 dark:text-gray-200">
+        This workflow will be shared with your team
+      </p>
+    </template>
+    <p v-else class="font-semibold">Update workflow</p>
     <div class="flex items-start mt-4">
     <div class="flex items-start mt-4">
       <div class="flex-1 mr-8">
       <div class="flex-1 mr-8">
         <ui-input
         <ui-input
@@ -51,32 +54,46 @@
         </shared-wysiwyg>
         </shared-wysiwyg>
       </div>
       </div>
       <div class="w-64 sticky top-4 pb-4">
       <div class="w-64 sticky top-4 pb-4">
-        <div class="flex">
+        <template v-if="isUpdate">
           <ui-button
           <ui-button
-            v-tooltip="'Save as draft'"
-            :disabled="state.isPublishing"
-            icon
-            @click="saveDraft"
+            variant="accent"
+            class="w-full"
+            @click="$emit('update', state.workflow)"
           >
           >
-            <v-remixicon name="riSaveLine" />
+            Update
           </ui-button>
           </ui-button>
+          <ui-button class="w-full mt-2" @click="$emit('close')">
+            {{ t('common.cancel') }}
+          </ui-button>
+        </template>
+        <template v-else>
+          <div class="flex">
+            <ui-button
+              v-tooltip="'Save as draft'"
+              :disabled="state.isPublishing"
+              icon
+              @click="saveDraft"
+            >
+              <v-remixicon name="riSaveLine" />
+            </ui-button>
+            <ui-button
+              :loading="state.isPublishing"
+              :disabled="!state.workflow.name.trim()"
+              variant="accent"
+              class="w-full ml-2"
+              @click="publishWorkflow"
+            >
+              Publish
+            </ui-button>
+          </div>
           <ui-button
           <ui-button
-            :loading="state.isPublishing"
-            :disabled="!state.workflow.name.trim()"
-            variant="accent"
-            class="w-full ml-2"
-            @click="publishWorkflow"
+            :disabled="state.isPublishing"
+            class="mt-2 w-full"
+            @click="$emit('close')"
           >
           >
-            Publish
+            Cancel
           </ui-button>
           </ui-button>
-        </div>
-        <ui-button
-          :disabled="state.isPublishing"
-          class="mt-2 w-full"
-          @click="$emit('close')"
-        >
-          Cancel
-        </ui-button>
+        </template>
         <ui-select
         <ui-select
           v-model="state.workflow.category"
           v-model="state.workflow.category"
           class="mt-4 w-full"
           class="mt-4 w-full"
@@ -109,7 +126,7 @@ import { useToast } from 'vue-toastification';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import { fetchApi } from '@/utils/api';
 import { fetchApi } from '@/utils/api';
 import { useUserStore } from '@/stores/user';
 import { useUserStore } from '@/stores/user';
-import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
+import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { workflowCategories } from '@/utils/shared';
 import { workflowCategories } from '@/utils/shared';
 import { parseJSON, debounce } from '@/utils/helper';
 import { parseJSON, debounce } from '@/utils/helper';
 import { convertWorkflow } from '@/utils/workflowData';
 import { convertWorkflow } from '@/utils/workflowData';
@@ -122,12 +139,12 @@ const props = defineProps({
   },
   },
   isUpdate: Boolean,
   isUpdate: Boolean,
 });
 });
-const emit = defineEmits(['close', 'publish', 'change']);
+const emit = defineEmits(['close', 'publish', 'change', 'update']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
 const toast = useToast();
 const toast = useToast();
 const userStore = useUserStore();
 const userStore = useUserStore();
-const sharedWorkflowStore = useSharedWorkflowStore();
+const teamWorkflowStore = useTeamWorkflowStore();
 
 
 const state = reactive({
 const state = reactive({
   contentLength: 0,
   contentLength: 0,
@@ -166,12 +183,7 @@ async function publishWorkflow() {
 
 
     workflow.drawflow = props.workflow.drawflow;
     workflow.drawflow = props.workflow.drawflow;
 
 
-    sharedWorkflowStore.insert(workflow);
-    sessionStorage.setItem(
-      'shared-workflows',
-      JSON.stringify(sharedWorkflowStore.shared)
-    );
-
+    teamWorkflowStore.insert(workflow);
     state.isPublishing = false;
     state.isPublishing = false;
 
 
     emit('publish');
     emit('publish');
@@ -210,14 +222,16 @@ watch(
 );
 );
 
 
 onMounted(() => {
 onMounted(() => {
-  const key = `draft-team:${props.workflow.id}`;
-  browser.storage.local.get(key).then((data) => {
-    Object.assign(state.workflow, data[key]);
+  if (!props.isUpdate) {
+    const key = `draft-team:${props.workflow.id}`;
+    browser.storage.local.get(key).then((data) => {
+      Object.assign(state.workflow, data[key]);
 
 
-    if (!state.workflow.tag) {
-      state.workflow.tag = 'stage';
-    }
-  });
+      if (!state.workflow.tag) {
+        state.workflow.tag = 'stage';
+      }
+    });
+  }
 });
 });
 </script>
 </script>
 <style scoped>
 <style scoped>

+ 234 - 17
src/components/newtab/workflow/editor/EditorLocalActions.vue

@@ -1,5 +1,12 @@
 <template>
 <template>
-  <ui-card padding="p-1 pointer-events-auto mr-4">
+  <span
+    v-if="isTeam && workflow.tag"
+    :class="tagColors[workflow.tag]"
+    class="text-sm rounded-md text-black capitalize p-1 mr-2"
+  >
+    {{ workflow.tag }}
+  </span>
+  <ui-card v-if="!isTeam || !canEdit" padding="p-1 pointer-events-auto">
     <button
     <button
       v-tooltip.group="'Workflow note'"
       v-tooltip.group="'Workflow note'"
       class="hoverable p-2 rounded-lg"
       class="hoverable p-2 rounded-lg"
@@ -8,7 +15,11 @@
       <v-remixicon name="riFileEditLine" />
       <v-remixicon name="riFileEditLine" />
     </button>
     </button>
   </ui-card>
   </ui-card>
-  <ui-card padding="p-1" class="flex items-center pointer-events-auto">
+  <ui-card
+    v-if="!isTeam"
+    padding="p-1"
+    class="flex items-center pointer-events-auto ml-4"
+  >
     <ui-popover>
     <ui-popover>
       <template #trigger>
       <template #trigger>
         <button
         <button
@@ -120,14 +131,30 @@
       {{ t('common.disabled') }}
       {{ t('common.disabled') }}
     </button>
     </button>
   </ui-card>
   </ui-card>
-  <ui-card padding="p-1 ml-4 space-x-1 pointer-events-auto">
-    <ui-popover>
+  <ui-card padding="p-1 ml-4 space-x-1 pointer-events-auto flex items-center">
+    <button
+      v-if="!canEdit"
+      v-tooltip.group="state.triggerText"
+      class="p-2 hoverable rounded-lg"
+    >
+      <v-remixicon name="riFlashlightLine" />
+    </button>
+    <ui-popover v-if="canEdit">
       <template #trigger>
       <template #trigger>
         <button class="rounded-lg p-2 hoverable">
         <button class="rounded-lg p-2 hoverable">
           <v-remixicon name="riMore2Line" />
           <v-remixicon name="riMore2Line" />
         </button>
         </button>
       </template>
       </template>
-      <ui-list class="w-36">
+      <ui-list style="min-width: 9rem">
+        <ui-list-item
+          v-if="isTeam && canEdit"
+          v-close-popover
+          class="cursor-pointer"
+          @click="syncWorkflow"
+        >
+          <v-remixicon name="riRefreshLine" class="mr-2 -ml-1" />
+          <span>{{ t('workflow.host.sync.title') }}</span>
+        </ui-list-item>
         <ui-list-item
         <ui-list-item
           class="cursor-pointer"
           class="cursor-pointer"
           @click="updateWorkflow({ isDisabled: !workflow.isDisabled })"
           @click="updateWorkflow({ isDisabled: !workflow.isDisabled })"
@@ -148,6 +175,7 @@
       </ui-list>
       </ui-list>
     </ui-popover>
     </ui-popover>
     <ui-button
     <ui-button
+      v-if="!isTeam"
       :title="shortcuts['editor:save'].readable"
       :title="shortcuts['editor:save'].readable"
       variant="accent"
       variant="accent"
       class="relative"
       class="relative"
@@ -167,7 +195,56 @@
       <v-remixicon name="riSaveLine" class="mr-2 -ml-1 my-1" />
       <v-remixicon name="riSaveLine" class="mr-2 -ml-1 my-1" />
       {{ t('common.save') }}
       {{ t('common.save') }}
     </ui-button>
     </ui-button>
+    <ui-button
+      v-else-if="!canEdit"
+      v-tooltip.group="'Sync workflow'"
+      :loading="state.loadingSync"
+      variant="accent"
+      @click="syncWorkflow"
+    >
+      <v-remixicon name="riRefreshLine" class="mr-2 -ml-1" />
+      <span>
+        {{ t('workflow.host.sync.title') }}
+      </span>
+    </ui-button>
+    <template v-else>
+      <ui-button
+        v-tooltip="`Save workflow (${shortcuts['editor:save'].readable})`"
+        class="mr-2"
+        icon
+        @click="saveWorkflow"
+      >
+        <span
+          v-if="isDataChanged"
+          class="flex h-3 w-3 absolute top-0 left-0 -ml-1 -mt-1"
+        >
+          <span
+            class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"
+          ></span>
+          <span
+            class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"
+          ></span>
+        </span>
+        <v-remixicon name="riSaveLine" />
+      </ui-button>
+      <ui-button
+        v-tooltip="'Publish workflow update'"
+        :loading="state.isPublishing"
+        variant="accent"
+        @click="publishWorkflow"
+      >
+        Publish
+      </ui-button>
+    </template>
   </ui-card>
   </ui-card>
+  <ui-modal v-model="state.showEditDescription" persist blur custom-content>
+    <workflow-share-team
+      :workflow="workflow"
+      :is-update="true"
+      @update="updateWorkflowDescription"
+      @close="state.showEditDescription = false"
+    />
+  </ui-modal>
   <ui-modal v-model="renameState.showModal" title="Workflow">
   <ui-modal v-model="renameState.showModal" title="Workflow">
     <ui-input
     <ui-input
       v-model="renameState.name"
       v-model="renameState.name"
@@ -204,6 +281,7 @@
     <shared-wysiwyg
     <shared-wysiwyg
       :model-value="workflow.content || ''"
       :model-value="workflow.content || ''"
       :limit="1000"
       :limit="1000"
+      :readonly="!canEdit"
       class="bg-box-transparent p-4 rounded-lg overflow-auto scroll"
       class="bg-box-transparent p-4 rounded-lg overflow-auto scroll"
       placeholder="Write note here..."
       placeholder="Write note here..."
       style="max-height: calc(100vh - 12rem); min-height: 400px"
       style="max-height: calc(100vh - 12rem); min-height: 400px"
@@ -221,14 +299,19 @@ import { sendMessage } from '@/utils/message';
 import { fetchApi } from '@/utils/api';
 import { fetchApi } from '@/utils/api';
 import { useUserStore } from '@/stores/user';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useWorkflowStore } from '@/stores/workflow';
+import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { useDialog } from '@/composable/dialog';
 import { useDialog } from '@/composable/dialog';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { useShortcut, getShortcut } from '@/composable/shortcut';
-import { parseJSON } from '@/utils/helper';
+import { tagColors } from '@/utils/shared';
+import { parseJSON, findTriggerBlock } from '@/utils/helper';
 import { exportWorkflow, convertWorkflow } from '@/utils/workflowData';
 import { exportWorkflow, convertWorkflow } from '@/utils/workflowData';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
+import getTriggerText from '@/utils/triggerText';
+import convertWorkflowData from '@/utils/convertWorkflowData';
 import SharedWysiwyg from '@/components/newtab/shared/SharedWysiwyg.vue';
 import SharedWysiwyg from '@/components/newtab/shared/SharedWysiwyg.vue';
+import WorkflowShareTeam from '@/components/newtab/workflow/WorkflowShareTeam.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   isDataChanged: {
   isDataChanged: {
@@ -243,8 +326,17 @@ const props = defineProps({
     type: Object,
     type: Object,
     default: () => ({}),
     default: () => ({}),
   },
   },
+  changedData: {
+    type: Object,
+    default: () => ({}),
+  },
+  canEdit: {
+    type: Boolean,
+    default: true,
+  },
+  isTeam: Boolean,
 });
 });
-const emit = defineEmits(['modal', 'change', 'update']);
+const emit = defineEmits(['modal', 'change', 'update', 'permission']);
 
 
 useGroupTooltip();
 useGroupTooltip();
 
 
@@ -254,6 +346,7 @@ const router = useRouter();
 const dialog = useDialog();
 const dialog = useDialog();
 const userStore = useUserStore();
 const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
 const workflowStore = useWorkflowStore();
+const teamWorkflow = useTeamWorkflowStore();
 const sharedWorkflowStore = useSharedWorkflowStore();
 const sharedWorkflowStore = useSharedWorkflowStore();
 const shortcuts = useShortcut([
 const shortcuts = useShortcut([
   /* eslint-disable-next-line */
   /* eslint-disable-next-line */
@@ -263,8 +356,12 @@ const shortcuts = useShortcut([
 ]);
 ]);
 
 
 const state = reactive({
 const state = reactive({
+  triggerText: '',
+  loadingSync: false,
+  isPublishing: false,
   showNoteModal: false,
   showNoteModal: false,
   isUploadingHost: false,
   isUploadingHost: false,
+  showEditDescription: false,
 });
 });
 const renameState = reactive({
 const renameState = reactive({
   name: '',
   name: '',
@@ -279,16 +376,37 @@ const userDontHaveTeamAccess = computed(
 );
 );
 
 
 function updateWorkflow(data = {}, changedIndicator = false) {
 function updateWorkflow(data = {}, changedIndicator = false) {
-  return workflowStore
-    .update({
+  let store = null;
+
+  if (props.isTeam) {
+    store = teamWorkflow.update({
+      data,
+      id: props.workflow.id,
+      teamId: router.currentRoute.value.params.teamId,
+    });
+  } else {
+    store = workflowStore.update({
       data,
       data,
       id: props.workflow.id,
       id: props.workflow.id,
-    })
-    .then((result) => {
-      emit('update', { data, changedIndicator });
-
-      return result;
     });
     });
+  }
+
+  return store.then((result) => {
+    emit('update', { data, changedIndicator });
+
+    return result;
+  });
+}
+function updateWorkflowDescription(value) {
+  const keys = ['description', 'category', 'content', 'tag', 'name'];
+  const payload = {};
+
+  keys.forEach((key) => {
+    payload[key] = value[key];
+  });
+
+  updateWorkflow(payload);
+  state.showEditDescription = false;
 }
 }
 function executeWorkflow() {
 function executeWorkflow() {
   sendMessage(
   sendMessage(
@@ -391,7 +509,45 @@ function clearRenameModal() {
     showModal: false,
     showModal: false,
   });
   });
 }
 }
+async function publishWorkflow() {
+  if (!props.canEdit) return;
+
+  const workflowPaylod = convertWorkflow(props.workflow, ['id']);
+  workflowPaylod.drawflow = parseJSON(
+    props.workflow.drawflow,
+    props.workflow.drawflow
+  );
+  delete workflowPaylod.id;
+  delete workflowPaylod.extVersion;
+
+  state.isPublishing = true;
+
+  try {
+    const response = await fetchApi(
+      `/teams/${userStore.user.team.id}/workflows/${props.workflow.id}`,
+      {
+        method: 'PATCH',
+        body: JSON.stringify({ workflow: workflowPaylod }),
+      }
+    );
+    const result = await response.json();
+
+    if (!response.ok) {
+      throw new Error(result.message);
+    }
+  } catch (error) {
+    console.error(error);
+    toast.error('Something went wrong');
+  } finally {
+    state.isPublishing = false;
+  }
+}
 function initRenameWorkflow() {
 function initRenameWorkflow() {
+  if (props.isTeam) {
+    state.showEditDescription = true;
+    return;
+  }
+
   Object.assign(renameState, {
   Object.assign(renameState, {
     showModal: true,
     showModal: true,
     name: `${props.workflow.name}`,
     name: `${props.workflow.name}`,
@@ -447,34 +603,95 @@ async function saveWorkflow() {
     console.error(error);
     console.error(error);
   }
   }
 }
 }
+async function retrieveTriggerText() {
+  if (props.canEdit) return;
+
+  const triggerBlock = findTriggerBlock(props.workflow.drawflow);
+  if (!triggerBlock) return;
+
+  state.triggerText = await getTriggerText(
+    triggerBlock.data,
+    t,
+    router.currentRoute.value.params.id,
+    true
+  );
+}
+async function syncWorkflow() {
+  state.loadingSync = true;
+
+  if (props.canEdit)
+    toast('Syncing workflow...', { timeout: false, id: 'sync' });
+
+  try {
+    const response = await fetchApi(
+      `/teams/${userStore.user.team.id}/workflows/${props.workflow.id}`
+    );
+    const result = await response.json();
+
+    if (!response.ok) {
+      throw new Error(result.message);
+    }
+
+    await teamWorkflow.update({
+      data: result,
+      id: props.workflow.id,
+      teamId: router.currentRoute.value.params.teamId,
+    });
+
+    const convertedData = convertWorkflowData(result);
+    props.editor.setNodes(convertedData.drawflow.nodes || []);
+    props.editor.setEdges(convertedData.drawflow.edges || []);
+    props.editor.fitView();
+
+    await retrieveTriggerText();
+
+    const triggerBlock = convertedData.drawflow.nodes.find(
+      (node) => node.label === 'trigger'
+    );
+    registerWorkflowTrigger(props.workflow.id, triggerBlock);
+    emit('permission');
+  } catch (error) {
+    toast.error(error.message);
+    console.error(error);
+  } finally {
+    state.loadingSync = false;
+    toast.dismiss('sync');
+  }
+}
+
+retrieveTriggerText();
+
 const modalActions = [
 const modalActions = [
   {
   {
     id: 'table',
     id: 'table',
+    hasAccess: props.canEdit,
     name: t('workflow.table.title'),
     name: t('workflow.table.title'),
     icon: 'riTable2',
     icon: 'riTable2',
   },
   },
   {
   {
+    hasAccess: true,
     id: 'global-data',
     id: 'global-data',
     name: t('common.globalData'),
     name: t('common.globalData'),
     icon: 'riDatabase2Line',
     icon: 'riDatabase2Line',
   },
   },
   {
   {
     id: 'settings',
     id: 'settings',
+    hasAccess: props.canEdit,
     name: t('common.settings'),
     name: t('common.settings'),
     icon: 'riSettings3Line',
     icon: 'riSettings3Line',
   },
   },
-];
+].filter((item) => item.hasAccess);
 const moreActions = [
 const moreActions = [
   {
   {
     id: 'export',
     id: 'export',
-    name: t('common.export'),
     icon: 'riDownloadLine',
     icon: 'riDownloadLine',
+    name: t('common.export'),
     action: () => exportWorkflow(props.workflow),
     action: () => exportWorkflow(props.workflow),
   },
   },
   {
   {
     id: 'rename',
     id: 'rename',
     icon: 'riPencilLine',
     icon: 'riPencilLine',
-    name: t('common.rename'),
+    name: props.isTeam ? 'Edit detail' : t('common.rename'),
     action: initRenameWorkflow,
     action: initRenameWorkflow,
   },
   },
   {
   {

+ 2 - 5
src/components/newtab/workflows/WorkflowsUserTeam.vue

@@ -38,7 +38,7 @@
       <template #footer-content>
       <template #footer-content>
         <span
         <span
           :class="tagColors[workflow.tag]"
           :class="tagColors[workflow.tag]"
-          class="text-sm rounded-md capitalize p-1"
+          class="text-sm rounded-md text-black capitalize p-1"
         >
         >
           {{ workflow.tag }}
           {{ workflow.tag }}
         </span>
         </span>
@@ -54,6 +54,7 @@ import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { sendMessage } from '@/utils/message';
 import { sendMessage } from '@/utils/message';
 import { arraySorter } from '@/utils/helper';
 import { arraySorter } from '@/utils/helper';
 import { useDialog } from '@/composable/dialog';
 import { useDialog } from '@/composable/dialog';
+import { tagColors } from '@/utils/shared';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 
 const props = defineProps({
 const props = defineProps({
@@ -70,10 +71,6 @@ const props = defineProps({
   },
   },
 });
 });
 
 
-const tagColors = {
-  stage: 'bg-yellow-200',
-  production: 'bg-green-200',
-};
 const menu = [
 const menu = [
   {
   {
     id: 'delete',
     id: 'delete',

+ 1 - 2
src/newtab/App.vue

@@ -219,11 +219,10 @@ browser.runtime.onMessage.addListener(({ type, data }) => {
     await setI18nLanguage(store.settings.locale);
     await setI18nLanguage(store.settings.locale);
 
 
     await dataMigration();
     await dataMigration();
+    await userStore.loadUser();
 
 
     retrieved.value = true;
     retrieved.value = true;
 
 
-    await userStore.loadUser();
-
     await Promise.allSettled([
     await Promise.allSettled([
       sharedWorkflowStore.fetchWorkflows(),
       sharedWorkflowStore.fetchWorkflows(),
       fetchUserData(),
       fetchUserData(),

+ 1 - 1
src/newtab/pages/Workflows.vue

@@ -251,7 +251,7 @@ const savedSorts = JSON.parse(localStorage.getItem('workflow-sorts') || '{}');
 const state = shallowReactive({
 const state = shallowReactive({
   query: '',
   query: '',
   activeFolder: '',
   activeFolder: '',
-  activeTab: 'team-workflows',
+  activeTab: 'local',
   perPage: savedSorts.perPage || 18,
   perPage: savedSorts.perPage || 18,
   sortBy: savedSorts.sortBy || 'createdAt',
   sortBy: savedSorts.sortBy || 'createdAt',
   sortOrder: savedSorts.sortOrder || 'desc',
   sortOrder: savedSorts.sortOrder || 'desc',

+ 0 - 3
src/newtab/pages/teams/[teamId]/workflows/[id].vue

@@ -1,3 +0,0 @@
-<template>
-  <p>hola?</p>
-</template>

+ 91 - 18
src/newtab/pages/workflows/[id].vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <div v-if="workflow" class="flex h-screen">
   <div v-if="workflow" class="flex h-screen">
     <div
     <div
-      v-if="state.showSidebar"
+      v-if="state.showSidebar && haveEditAccess"
       class="w-80 bg-white dark:bg-gray-800 py-6 relative border-l border-gray-100 dark:border-gray-700 dark:border-opacity-50 flex flex-col"
       class="w-80 bg-white dark:bg-gray-800 py-6 relative border-l border-gray-100 dark:border-gray-700 dark:border-opacity-50 flex flex-col"
     >
     >
       <workflow-edit-block
       <workflow-edit-block
@@ -24,11 +24,41 @@
       <div
       <div
         class="absolute w-full flex items-center z-10 left-0 p-4 top-0 pointer-events-none"
         class="absolute w-full flex items-center z-10 left-0 p-4 top-0 pointer-events-none"
       >
       >
+        <ui-card
+          v-if="!haveEditAccess"
+          padding="px-2 mr-4"
+          class="flex items-center overflow-hidden"
+          style="min-width: 150px; height: 48px"
+        >
+          <span class="inline-block">
+            <ui-img
+              v-if="workflow.icon.startsWith('http')"
+              :src="workflow.icon"
+              class="w-8 h-8"
+            />
+            <v-remixicon v-else :name="workflow.icon" size="26" />
+          </span>
+          <div class="ml-2 max-w-sm">
+            <p
+              :class="{ 'text-lg': !workflow.description }"
+              class="font-semibold leading-tight text-overflow"
+            >
+              {{ workflow.name }}
+            </p>
+            <p
+              :class="{ 'text-sm': workflow.description }"
+              class="text-gray-600 leading-tight dark:text-gray-200 text-overflow"
+            >
+              {{ workflow.description }}
+            </p>
+          </div>
+        </ui-card>
         <ui-tabs
         <ui-tabs
           v-model="state.activeTab"
           v-model="state.activeTab"
           class="border-none px-2 rounded-lg h-full space-x-1 bg-white dark:bg-gray-800 pointer-events-auto"
           class="border-none px-2 rounded-lg h-full space-x-1 bg-white dark:bg-gray-800 pointer-events-auto"
         >
         >
           <button
           <button
+            v-if="haveEditAccess"
             v-tooltip="
             v-tooltip="
               `${t('workflow.toggleSidebar')} (${
               `${t('workflow.toggleSidebar')} (${
                 shortcut['editor:toggle-sidebar'].readable
                 shortcut['editor:toggle-sidebar'].readable
@@ -58,7 +88,10 @@
           :editor="editor"
           :editor="editor"
           :workflow="workflow"
           :workflow="workflow"
           :is-data-changed="state.dataChanged"
           :is-data-changed="state.dataChanged"
+          :is-team="isTeamWorkflow"
+          :can-edit="haveEditAccess"
           @update="onActionUpdated"
           @update="onActionUpdated"
+          @permission="checkWorkflowPermission"
           @modal="(modalState.name = $event), (modalState.show = true)"
           @modal="(modalState.name = $event), (modalState.show = true)"
         />
         />
       </div>
       </div>
@@ -74,6 +107,7 @@
             v-if="state.workflowConverted"
             v-if="state.workflowConverted"
             :id="route.params.id"
             :id="route.params.id"
             :data="workflow.drawflow"
             :data="workflow.drawflow"
+            :disabled="isTeamWorkflow && !haveEditAccess"
             :class="{ 'animate-blocks': state.animateBlocks }"
             :class="{ 'animate-blocks': state.animateBlocks }"
             class="h-screen"
             class="h-screen"
             @init="onEditorInit"
             @init="onEditorInit"
@@ -81,7 +115,7 @@
             @update:node="state.dataChanged = true"
             @update:node="state.dataChanged = true"
             @delete:node="state.dataChanged = true"
             @delete:node="state.dataChanged = true"
           >
           >
-            <template #controls-append>
+            <template v-if="!isTeamWorkflow || haveEditAccess" #controls-append>
               <button
               <button
                 v-tooltip="t('workflow.autoAlign.title')"
                 v-tooltip="t('workflow.autoAlign.title')"
                 class="control-button hoverable ml-2"
                 class="control-button hoverable ml-2"
@@ -160,6 +194,7 @@
   <shared-permissions-modal
   <shared-permissions-modal
     v-model="permissionState.showModal"
     v-model="permissionState.showModal"
     :permissions="permissionState.items"
     :permissions="permissionState.items"
+    @granted="registerTrigger"
   />
   />
 </template>
 </template>
 <script setup>
 <script setup>
@@ -182,6 +217,7 @@ import dagre from 'dagre';
 import { useStore } from '@/stores/main';
 import { useStore } from '@/stores/main';
 import { useUserStore } from '@/stores/user';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useWorkflowStore } from '@/stores/workflow';
+import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import {
 import {
   useShortcut,
   useShortcut,
   getShortcut,
   getShortcut,
@@ -193,6 +229,7 @@ import { fetchApi } from '@/utils/api';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useCommandManager } from '@/composable/commandManager';
 import { useCommandManager } from '@/composable/commandManager';
 import { debounce, parseJSON, throttle } from '@/utils/helper';
 import { debounce, parseJSON, throttle } from '@/utils/helper';
+import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import dbStorage from '@/db/storage';
 import dbStorage from '@/db/storage';
 import DroppedNode from '@/utils/editor/DroppedNode';
 import DroppedNode from '@/utils/editor/DroppedNode';
@@ -224,6 +261,10 @@ const router = useRouter();
 const userStore = useUserStore();
 const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
 const workflowStore = useWorkflowStore();
 const commandManager = useCommandManager();
 const commandManager = useCommandManager();
+const teamWorkflowStore = useTeamWorkflowStore();
+
+const { teamId, id: workflowId } = route.params;
+const isTeamWorkflow = route.name === 'team-workflows';
 
 
 const editor = shallowRef(null);
 const editor = shallowRef(null);
 const connectedTable = shallowRef(null);
 const connectedTable = shallowRef(null);
@@ -334,7 +375,18 @@ const workflowModals = {
   },
   },
 };
 };
 
 
-const workflow = computed(() => workflowStore.getById(route.params.id));
+const haveEditAccess = computed(() => {
+  if (!isTeamWorkflow) return true;
+
+  return userStore.validateTeamAccess(['edit', 'owner', 'create']);
+});
+const workflow = computed(() => {
+  if (isTeamWorkflow) {
+    return teamWorkflowStore.getById(teamId, workflowId);
+  }
+
+  return workflowStore.getById(workflowId);
+});
 const workflowStates = computed(() =>
 const workflowStates = computed(() =>
   workflowStore.getWorkflowStates(route.params.id)
   workflowStore.getWorkflowStates(route.params.id)
 );
 );
@@ -357,6 +409,7 @@ provide('workflow', {
 provide('workflow-editor', editor);
 provide('workflow-editor', editor);
 
 
 const updateBlockData = debounce((data) => {
 const updateBlockData = debounce((data) => {
+  if (!haveEditAccess.value) return;
   const node = editor.value.getNode.value(editState.blockData.blockId);
   const node = editor.value.getNode.value(editState.blockData.blockId);
   const dataCopy = JSON.parse(JSON.stringify(data));
   const dataCopy = JSON.parse(JSON.stringify(data));
 
 
@@ -375,6 +428,7 @@ const updateBlockData = debounce((data) => {
   state.dataChanged = true;
   state.dataChanged = true;
 }, 250);
 }, 250);
 const updateHostedWorkflow = throttle(async () => {
 const updateHostedWorkflow = throttle(async () => {
+  if (isTeamWorkflow) return;
   if (!userStore.user || workflowPayload.isUpdating) return;
   if (!userStore.user || workflowPayload.isUpdating) return;
 
 
   const isHosted = userStore.hostedWorkflows[route.params.id];
   const isHosted = userStore.hostedWorkflows[route.params.id];
@@ -462,6 +516,12 @@ const onEdgesChange = debounce((changes) => {
   // if (command) commandManager.add(command);
   // if (command) commandManager.add(command);
 }, 250);
 }, 250);
 
 
+function registerTrigger() {
+  const triggerBlock = workflow.value.drawflow.nodes.find(
+    (node) => node.label === 'trigger'
+  );
+  registerWorkflowTrigger(workflowId, triggerBlock);
+}
 function executeCommand(type) {
 function executeCommand(type) {
   state.isExecuteCommand = true;
   state.isExecuteCommand = true;
 
 
@@ -597,12 +657,23 @@ function initEditBlock(data) {
 }
 }
 async function updateWorkflow(data) {
 async function updateWorkflow(data) {
   try {
   try {
-    await workflowStore.update({
-      data,
-      id: route.params.id,
-    });
+    if (isTeamWorkflow) {
+      if (!haveEditAccess.value && !data.globalData) return;
+      await teamWorkflowStore.update({
+        data,
+        teamId,
+        id: workflowId,
+      });
+    } else {
+      await workflowStore.update({
+        data,
+        id: route.params.id,
+      });
+    }
+
     workflowPayload.data = { ...workflowPayload.data, ...data };
     workflowPayload.data = { ...workflowPayload.data, ...data };
-    await updateHostedWorkflow();
+
+    if (!isTeamWorkflow) await updateHostedWorkflow();
   } catch (error) {
   } catch (error) {
     console.error(error);
     console.error(error);
   }
   }
@@ -935,6 +1006,14 @@ async function fetchConnectedTable() {
 
 
   connectedTable.value = table;
   connectedTable.value = table;
 }
 }
+function checkWorkflowPermission() {
+  getWorkflowPermissions(workflow.value.drawflow).then((permissions) => {
+    if (permissions.length === 0) return;
+
+    permissionState.items = permissions;
+    permissionState.showModal = true;
+  });
+}
 
 
 const shortcut = useShortcut([
 const shortcut = useShortcut([
   getShortcut('editor:toggle-sidebar', toggleSidebar),
   getShortcut('editor:toggle-sidebar', toggleSidebar),
@@ -961,7 +1040,7 @@ watch(
 onBeforeRouteLeave(() => {
 onBeforeRouteLeave(() => {
   updateHostedWorkflow();
   updateHostedWorkflow();
 
 
-  if (!state.dataChanged) return;
+  if (!state.dataChanged || !haveEditAccess.value) return;
 
 
   const confirm = window.confirm(t('message.notSaved'));
   const confirm = window.confirm(t('message.notSaved'));
 
 
@@ -981,14 +1060,8 @@ onMounted(() => {
     state.workflowConverted = true;
     state.workflowConverted = true;
   });
   });
 
 
-  if (route.query.permission) {
-    getWorkflowPermissions(workflow.value.drawflow).then((permissions) => {
-      if (permissions.length === 0) return;
-
-      permissionState.items = permissions;
-      permissionState.showModal = true;
-    });
-  }
+  if (route.query.permission || (isTeamWorkflow && !haveEditAccess.value))
+    checkWorkflowPermission();
 
 
   if (workflow.value.connectedTable) {
   if (workflow.value.connectedTable) {
     fetchConnectedTable();
     fetchConnectedTable();
@@ -997,7 +1070,7 @@ onMounted(() => {
   window.onbeforeunload = () => {
   window.onbeforeunload = () => {
     updateHostedWorkflow();
     updateHostedWorkflow();
 
 
-    if (state.dataChanged) {
+    if (state.dataChanged && haveEditAccess.value) {
       return t('message.notSaved');
       return t('message.notSaved');
     }
     }
   };
   };

+ 1 - 2
src/newtab/router.js

@@ -16,7 +16,6 @@ import SettingsAbout from './pages/settings/SettingsAbout.vue';
 import SettingsShortcuts from './pages/settings/SettingsShortcuts.vue';
 import SettingsShortcuts from './pages/settings/SettingsShortcuts.vue';
 import SettingsBackup from './pages/settings/SettingsBackup.vue';
 import SettingsBackup from './pages/settings/SettingsBackup.vue';
 import SettingsEditor from './pages/settings/SettingsEditor.vue';
 import SettingsEditor from './pages/settings/SettingsEditor.vue';
-import TeamWorkflows from './pages/teams/[teamId]/workflows/[id].vue';
 
 
 const routes = [
 const routes = [
   {
   {
@@ -43,7 +42,7 @@ const routes = [
   {
   {
     name: 'team-workflows',
     name: 'team-workflows',
     path: '/teams/:teamId/workflows/:id',
     path: '/teams/:teamId/workflows/:id',
-    component: TeamWorkflows,
+    component: WorkflowDetails,
   },
   },
   {
   {
     name: 'workflows-details',
     name: 'workflows-details',

+ 18 - 5
src/stores/teamWorkflow.js

@@ -1,5 +1,6 @@
 import { defineStore } from 'pinia';
 import { defineStore } from 'pinia';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
+import lodashDeepmerge from 'lodash.merge';
 
 
 export const useTeamWorkflowStore = defineStore('team-workflows', {
 export const useTeamWorkflowStore = defineStore('team-workflows', {
   storageMap: {
   storageMap: {
@@ -23,7 +24,7 @@ export const useTeamWorkflowStore = defineStore('team-workflows', {
     },
     },
   },
   },
   actions: {
   actions: {
-    insert(teamId, data) {
+    async insert(teamId, data) {
       if (!this.workflows[teamId]) this.workflows[teamId] = {};
       if (!this.workflows[teamId]) this.workflows[teamId] = {};
 
 
       if (Array.isArray(data)) {
       if (Array.isArray(data)) {
@@ -33,13 +34,25 @@ export const useTeamWorkflowStore = defineStore('team-workflows', {
       } else {
       } else {
         this.workflows[data.id] = data;
         this.workflows[data.id] = data;
       }
       }
+
+      await this.saveToStorage('workflows');
     },
     },
-    update({ id, data }) {
-      if (!this.workflows[id]) return null;
+    async update({ teamId, id, data, deepmerge = false }) {
+      const isWorkflowExists = Boolean(this.workflows[teamId]?.[id]);
+      if (!isWorkflowExists) return null;
+
+      if (deepmerge) {
+        this.workflows[teamId][id] = lodashDeepmerge(
+          this.workflows[teamId][id],
+          data
+        );
+      } else {
+        Object.assign(this.workflows[teamId][id], data);
+      }
 
 
-      Object.assign(this.workflows[id], data);
+      await this.saveToStorage('workflows');
 
 
-      return this.workflows[id];
+      return this.workflows[teamId][id];
     },
     },
     async delete(teamId, id) {
     async delete(teamId, id) {
       if (!this.workflows[teamId]) return;
       if (!this.workflows[teamId]) return;

+ 5 - 1
src/stores/user.js

@@ -39,7 +39,10 @@ export const useUserStore = defineStore('user', {
             'lastBackup',
             'lastBackup',
           ]);
           ]);
 
 
-          if (!user) return;
+          if (!user) {
+            this.retrieved = true;
+            return;
+          }
         }
         }
 
 
         localStorage.setItem('username', user?.username);
         localStorage.setItem('username', user?.username);
@@ -50,6 +53,7 @@ export const useUserStore = defineStore('user', {
         this.user = user;
         this.user = user;
         this.retrieved = true;
         this.retrieved = true;
       } catch (error) {
       } catch (error) {
+        this.retrieved = true;
         console.error(error);
         console.error(error);
       }
       }
     },
     },

+ 5 - 0
src/utils/shared.js

@@ -1214,6 +1214,11 @@ export const categories = {
   },
   },
 };
 };
 
 
+export const tagColors = {
+  stage: 'bg-yellow-200 dark:bg-yellow-300',
+  production: 'bg-green-200 dark:bg-green-300',
+};
+
 export const eventList = [
 export const eventList = [
   { id: 'click', name: 'Click', type: 'mouse-event' },
   { id: 'click', name: 'Click', type: 'mouse-event' },
   { id: 'dblclick', name: 'Double Click', type: 'mouse-event' },
   { id: 'dblclick', name: 'Double Click', type: 'mouse-event' },

+ 5 - 1
src/utils/workflowTrigger.js

@@ -13,7 +13,10 @@ export function registerContextMenu(workflowId, data) {
     const isFirefox = BROWSER_TYPE === 'firefox';
     const isFirefox = BROWSER_TYPE === 'firefox';
     const browserContext = isFirefox ? browser.menus : browser.contextMenus;
     const browserContext = isFirefox ? browser.menus : browser.contextMenus;
 
 
-    if (!browserContext) return;
+    if (!browserContext) {
+      reject(new Error("Don't have context menu permission"));
+      return;
+    }
 
 
     browserContext.create(
     browserContext.create(
       {
       {
@@ -235,6 +238,7 @@ export async function registerWorkflowTrigger(workflowId, { data }) {
     }
     }
   } catch (error) {
   } catch (error) {
     console.error(error);
     console.error(error);
+    throw error;
   }
   }
 }
 }