Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions frappe/AutomationRule/AddBlock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<template>
<div class="gap-x-3 flex">
<!-- plus button rounded -->
<Button class="rounded-2xl" variant="subtle" size="sm">
<LucidePlus class="size-4 text-ink-gray-6" />
</Button>
<template v-for="action in actions">
<button
:key="action.label"
class="flex items-center gap-1 group transition-all"
@click="action.onClick"
v-if="action.condition"
>
<component
:is="action.icon"
class="text-ink-gray-4 transition-colors"
:class="action.colorClass"
/>
<span
class="text-ink-gray-4 text-p-sm group-hover:text-ink-gray-8 transition-colors"
>
{{ action.label }}
</span>
</button>
</template>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import Button from '../../src/components/Button/Button.vue'
import ActionIcon from '../Icons/ActionIcon.vue'
import BellIcon from '../Icons/BellIcon.vue'
import ConditionIcon from '../Icons/ConditionIcon.vue'
import { useAutomationState } from './automation'
const state = useAutomationState()

const hasElseBlock = computed(() => state.rule.some((r) => r.type === 'else'))
const hasIfBlock = computed(() => state.rule.some((r) => r.type === 'if'))

const actions = computed(() => [
{
label: 'Condition',
icon: ConditionIcon,
colorClass: 'group-hover:text-[#7757EE]',
onClick: () => addConditionBlock(),
condition: true,
},
{
label: 'Else',
icon: ConditionIcon,
colorClass: 'group-hover:text-[#7757EE]',
onClick: () => addElseBlock(),
condition: !hasElseBlock.value && hasIfBlock.value,
},
{
label: 'Action',
icon: ActionIcon,
colorClass: 'group-hover:text-[#278F5E]',
onClick: () => addSetFieldBlock(),
condition: true,
},
{
label: 'Notification',
icon: BellIcon,
colorClass: 'group-hover:text-[#318AD8]',
onClick: () => addNotificationBlock(),
condition: true,
},
])

function addConditionBlock() {
insertBeforeElse({
type: 'if',
conditions: [['', '', '']],
actions: [],
})
}

function addElseBlock() {
// add this at the last of the rule array
if (hasElseBlock.value) return
state.rule.push({
type: 'else',
condition: 'True',
actions: [],
})
}

function addSetFieldBlock() {
insertBeforeElse({
type: 'set',
field: '',
value: '',
})
}

function addNotificationBlock() {
insertBeforeElse({
type: 'email',
to: '',
via: '',
template: '',
text: '',
})
}

function insertBeforeElse(block: any) {
const elseIdx = state.rule.findIndex((r) => r.type === 'else')
if (elseIdx === -1) {
// No else block, push to end
state.rule.push(block)
} else {
// Insert before else block
state.rule.splice(elseIdx, 0, block)
}
}
</script>

<style scoped></style>
221 changes: 221 additions & 0 deletions frappe/AutomationRule/Automation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<template>
<SettingsLayoutBase>
<template #title>
<div class="flex items-center gap-2">
<Button
variant="ghost"
icon-left="chevron-left"
:label="dependencyLabel"
size="md"
@click="$emit('update:step', 'list')"
class="cursor-pointer -ml-4 hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-ink-gray-7 text-lg hover:opacity-70 !pr-0"
/>
<!-- <Badge v-if="isDirty" theme="orange"> {{ __("Unsaved") }} </Badge> -->
</div>
</template>
<template #header-actions>
<div class="flex gap-4 items-center">
<div class="flex gap-2 items-center">
<Switch v-model="state.enabled" class="!w-fit" />
<span class="text-p-base text-ink-gray-6">
{{ 'Enabled' }}
</span>
</div>
<Button
:label="'Save'"
variant="solid"
size="sm"
@click="handleSubmit"
:disabled="resource.loading"
/>
</div>
</template>
<template #content>
<div class="flex flex-col gap-6" v-if="!resource.loading">
<NameBlock v-model="state.name" />
<ScopeBlock
:doctypes="[
{ label: 'Tickets', value: 'HD Ticket' },
{ label: 'ToDo', value: 'ToDo' },
]"
/>
<WhenBlock v-if="state.dt" />
<RuleBlock v-if="state.dt" />
<!-- RuleBlock -->
<!-- Can include -->
<!-- ConditionBlock -->
<!-- NotificationBlock -->
<!-- ActionBlock -->
<AddBlock />
</div>
<div v-else class="flex items-center justify-center h-full">
<LoadingIndicator class="w-6" />
</div>
</template>
</SettingsLayoutBase>
</template>

<script setup lang="ts">
import {
call,
createResource,
LoadingIndicator,
Switch,
toast,
} from 'frappe-ui'
import { computed, onMounted, provide, reactive, ref } from 'vue'
import SettingsLayoutBase from '../../src/components/SettingsLayoutBase.vue'
import AddBlock from './AddBlock.vue'
import NameBlock from './NameBlock.vue'
import RuleBlock from './RuleBlock.vue'
import ScopeBlock from './ScopeBlock.vue'
import WhenBlock from './WhenBlock.vue'
import { AutomationStateSymbol } from './types'

const props = defineProps<{
automationName?: string | null
}>()

const _automationName = ref(props.automationName)

const DOCTYPE_NAME = 'Automation Rule'

const isNew = computed(() => !_automationName.value)
const dependencyLabel = computed(() => {
if (isNew.value) return 'New Automation'
return _automationName.value
})

const eventMap = {
created: 'On Creation',
updated: 'On Update',
time: 'Timer',
}
const reverseEventMap = {
'On Creation': 'created',
'On Update': 'updated',
Timer: 'time',
}

const state = reactive({
name: '',
enabled: false,
dt: '',
eventType: 'created' as 'created' | 'updated' | 'time',
selectedTimerOption: 0,
presets: [],
rule: [],
})

async function handleSubmit(): Promise<void> {
if (isNew.value) {
await createAutomation()
} else {
await updateAutomation()
}
}

async function createAutomation() {
const doc = prepareDoc()
await call(
'frappe.client.insert',
{
doc,
},
{
onSuccess: () => {
toast.success('Automation created')
},
},
)
resource.submit({ doctype: DOCTYPE_NAME, name: state.name })
_automationName.value = doc.name
}

const hasNameChanged = computed(() => resource.data.name !== state.name)

async function updateAutomation() {
if (hasNameChanged.value) {
const newName = await call('frappe.client.rename_doc', {
doctype: DOCTYPE_NAME,
old_name: _automationName.value,
new_name: state.name,
})
_automationName.value = newName
}

const doc = prepareDoc()
await call(
'frappe.client.set_value',
{
doctype: DOCTYPE_NAME,
name: doc.name,
fieldname: { ...doc },
},
{
onSuccess: () => {
toast.success('Automation upated')
},
},
)
resource.submit({ doctype: DOCTYPE_NAME, name: state.name })
}

function prepareDoc() {
return {
doctype: DOCTYPE_NAME,
name: state.name,
dt: state.dt,
doctype_event: eventMap[state.eventType],
rule: parseRule(),
enabled: state.enabled,
}
}

function parseRule() {
const rule = {
presets: state.presets,
rule: state.rule,
}
return JSON.stringify(rule)
}

function handleRule(rule: string) {
try {
const ruleJson = JSON.parse(rule)
const presets = ruleJson.hasOwnProperty('presets') ? ruleJson.presets : []
const _rule = ruleJson.hasOwnProperty('rule') ? ruleJson.rule : []

return [presets, _rule]
} catch (err) {
console.error(err)
}
}

const resource = createResource({
url: 'frappe.client.get',
params: {
doctype: DOCTYPE_NAME,
name: _automationName.value,
},
onSuccess(data) {
// state
state.name = data.name
state.dt = data.dt
state.eventType = reverseEventMap[data.doctype_event]
state.enabled = Boolean(data.enabled)
const [presets, rule] = handleRule(data.rule)
state.presets = presets
state.rule = rule
},
})

onMounted(() => {
if (isNew.value) return
resource.reload()
})

provide(AutomationStateSymbol, state)
</script>

<style scoped></style>
28 changes: 28 additions & 0 deletions frappe/AutomationRule/AutomationLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<div v-if="step === 'list'" class="h-full">
<AutomationList @update:step="updateStep" />
</div>
<div v-else-if="step === 'view'" class="h-full">
<Automation
@update:step="updateStep"
:automation-name="automationViewName"
/>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Automation from './Automation.vue'
import AutomationList from './AutomationList.vue'

type AutomationStep = 'list' | 'view'

const step = ref<AutomationStep>('list')
const automationViewName = ref<string | null>(null)

function updateStep(newStep: AutomationStep, automationView?: string): void {
step.value = newStep
automationViewName.value = automationView || null
}
</script>
<style scoped></style>
Loading