Add warning for unsaved changes before leaving edit resource pages

This commit is contained in:
Kelani Tolulope
2023-06-23 12:00:47 +01:00
parent 60cfddabe7
commit 68460a85f9
41 changed files with 765 additions and 82 deletions
@@ -58,6 +58,7 @@
data-test-id="file-logo-upload"
accept="image/*"
:placeholder="$t('logo.placeholder')"
@change="$emit('change-detected')"
/>
</b-form-group>
@@ -154,7 +154,7 @@ export default {
this.$set(this.members, i, { ...this.members[i], label: label, dirty: true })
}
this.memberUsers.push({ value: member.userID, label })
this.memberUsers.push(member)
}
},
@@ -48,6 +48,7 @@
</b-container>
</template>
<script>
import { isEqual, cloneDeep } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CWorkflowEditorInfo from 'corteza-webapp-admin/src/components/Workflow/CWorkflowEditorInfo'
import CWorkflowEditorTriggers from 'corteza-webapp-admin/src/components/Workflow/CWorkflowEditorTriggers'
@@ -79,6 +80,7 @@ export default {
data () {
return {
workflow: undefined,
initialWorkflowState: undefined,
triggers: [],
info: {
@@ -113,6 +115,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
watch: {
workflowID: {
immediate: true,
@@ -129,6 +139,8 @@ export default {
name: '',
},
}
this.initialWorkflowState = cloneDeep(this.workflow)
}
},
},
@@ -218,6 +230,17 @@ export default {
prepare (workflow = {}) {
this.workflow = workflow
this.initialWorkflowState = cloneDeep(this.workflow)
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.workflow, this.initialWorkflowState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
@@ -26,6 +26,7 @@
</template>
<script>
import { isEqual, cloneDeep } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CComposeEditorBasic from 'corteza-webapp-admin/src/components/Settings/Compose/CComposeEditorBasic'
import CComposeEditorUI from 'corteza-webapp-admin/src/components/Settings/Compose/CComposeEditorUI'
@@ -51,6 +52,7 @@ export default {
data () {
return {
settings: {},
initialSettingsState: {},
basic: {
processing: false,
@@ -64,6 +66,14 @@ export default {
}
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
computed: {
...mapGetters({
can: 'rbac/can',
@@ -104,6 +114,7 @@ export default {
.then(settings => {
settings.forEach(({ name, value }) => {
this.$set(this.settings, name, value)
this.$set(this.initialSettingsState, name, cloneDeep(value))
})
})
.catch(this.toastErrorHandler(this.$t('notification:settings.compose.fetch.error')))
@@ -111,6 +122,16 @@ export default {
this.decLoader()
})
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.settings, this.initialSettingsState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -131,6 +131,7 @@ import { mapGetters } from 'vuex'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CFederationEditorInfo from 'corteza-webapp-admin/src/components/Federation/CFederationEditorInfo'
import CSubmitButton from 'corteza-webapp-admin/src/components/CSubmitButton'
import { cloneDeep, isEqual } from 'lodash'
export default {
i18nOptions: {
@@ -158,6 +159,7 @@ export default {
data () {
return {
node: {},
initialNodeState: {},
// Processing and success flags for each form
info: {
@@ -175,6 +177,14 @@ export default {
}
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
computed: {
...mapGetters({
can: 'rbac/can',
@@ -201,7 +211,17 @@ export default {
this.fetchNode()
this.fetchGeneratedUrl()
} else {
this.node = {}
this.node = {
name: '',
baseURL: '',
contact: '',
}
this.initialNodeState = {
name: '',
baseURL: '',
contact: '',
}
}
},
},
@@ -214,6 +234,7 @@ export default {
this.$FederationAPI.nodeRead({ nodeID: this.nodeID })
.then(node => {
this.node = node // new federation.Node(node)
this.initialNodeState = cloneDeep(node)
})
.catch(this.toastErrorHandler(this.$t('notification:federation.fetch.error')))
.finally(() => {
@@ -250,6 +271,7 @@ export default {
this.$FederationAPI.nodeUpdate(payload)
.then(node => {
this.node = node
this.initialNodeState = cloneDeep(node)
this.animateSuccess('info')
this.toastSuccess(this.$t('notification:federation.update.success'))
@@ -343,6 +365,16 @@ export default {
copyUrl () {
navigator.clipboard.writeText(this.generate.url)
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.node, this.initialNodeState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -53,6 +53,7 @@
</b-container>
</template>
<script>
import { isEqual, cloneDeep } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CRouteEditorInfo from 'corteza-webapp-admin/src/components/Apigw/CRouteEditorInfo'
import CFiltersStepper from 'corteza-webapp-admin/src/components/Apigw/CFiltersStepper'
@@ -85,6 +86,7 @@ export default {
data () {
return {
route: {},
initialRouteState: {},
routeEndpoint: undefined,
info: {
@@ -98,6 +100,7 @@ export default {
},
filters: [],
initialFiltersState: [],
availableFilters: [],
steps: [],
}
@@ -121,6 +124,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
watch: {
routeID: {
immediate: true,
@@ -133,8 +144,11 @@ export default {
this.fetchFilters()
} else {
this.route = {
endpoint: '',
method: 'GET',
}
this.initialRouteState = cloneDeep(this.route)
}
},
},
@@ -146,6 +160,7 @@ export default {
this.$SystemAPI.apigwRouteRead({ routeID: this.routeID, incFlags: 1 })
.then((api) => {
this.route = api
this.initialRouteState = cloneDeep(api)
this.routeEndpoint = btoa(api.endpoint)
})
.catch(this.toastErrorHandler(this.$t('notification:gateway.fetch.error')))
@@ -286,6 +301,7 @@ export default {
f.enabled = !!filter.enabled
return { ...f }
})
this.initialFiltersState = cloneDeep(this.filters)
})
},
@@ -325,6 +341,19 @@ export default {
fetchSteps () {
this.steps = ['prefilter', 'processer', 'postfilter']
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
const routeState = !isEqual(this.route, this.initialRouteState)
const filtersState = !isEqual(this.filters, this.initialFiltersState)
next((routeState || filtersState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -7,10 +7,11 @@
:title="title"
>
<span
v-if="applicationID"
class="text-nowrap"
>
<b-button
v-if="applicationID && canCreate"
v-if="canCreate"
data-test-id="button-new-application"
variant="primary"
:to="{ name: 'system.application.new' }"
@@ -18,7 +19,7 @@
{{ $t('new') }}
</b-button>
<c-permissions-button
v-if="applicationID && canGrant"
v-if="canGrant"
:title="application.name || applicationID"
:target="application.name || applicationID"
:resource="`corteza::system:application/${applicationID}`"
@@ -41,21 +42,24 @@
/>
<c-application-editor-unify
v-if="application.unify && application.applicationID"
v-if="applicationID && application.unify && application.applicationID"
class="mt-3"
:unify="application.unify"
:application="application"
:can-pin="canPin"
:processing="unify.processing"
:success="unify.success"
@change-detected="unifyAssetStateChange = true"
@submit="onUnifySubmit"
/>
</b-container>
</template>
<script>
import { isEqual } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CApplicationEditorInfo from 'corteza-webapp-admin/src/components/Application/CApplicationEditorInfo'
import CApplicationEditorUnify from 'corteza-webapp-admin/src/components/Application/CApplicationEditorUnify'
import { system } from '@cortezaproject/corteza-js'
import { mapGetters } from 'vuex'
export default {
@@ -84,6 +88,7 @@ export default {
data () {
return {
application: undefined,
initialApplicationState: undefined,
info: {
processing: false,
@@ -93,6 +98,7 @@ export default {
processing: false,
success: false,
},
unifyAssetStateChange: false,
}
},
@@ -118,6 +124,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
watch: {
applicationID: {
immediate: true,
@@ -125,7 +139,9 @@ export default {
if (this.applicationID) {
this.fetchApplication()
} else {
this.application = {}
this.application = new system.Application()
this.initialApplicationState = this.application.clone()
}
},
},
@@ -136,7 +152,25 @@ export default {
this.incLoader()
this.$SystemAPI.applicationRead({ applicationID: this.applicationID, incFlags: 1 })
.then(this.prepare)
.then((application = {}) => {
if (!application.unify) {
application.unify = {
listed: true,
pinned: false,
name: this.application.name,
config: '',
icon: '',
logo: '',
url: '',
}
}
application.unify.pinned = (application.flags || []).includes('pinned')
application.unify.name = application.unify.name ? application.unify.name : application.name
this.application = new system.Application(application)
this.initialApplicationState = this.application.clone()
},)
.catch(this.toastErrorHandler(this.$t('notification:application.fetch.error')))
.finally(() => {
this.decLoader()
@@ -205,6 +239,7 @@ export default {
return this.$SystemAPI.applicationUpdate({ ...this.application, unify })
.then(() => {
this.unifyAssetStateChange = false
this.fetchApplication()
this.toastSuccess(this.$t('notification:application.update.success'))
@@ -284,22 +319,14 @@ export default {
}
},
prepare (application = {}) {
if (!application.unify) {
application.unify = {
listed: true,
pinned: false,
name: this.application.name,
config: '',
icon: '',
logo: '',
url: '',
}
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.application, this.initialApplicationState) || this.unifyAssetStateChange ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
application.unify.pinned = (application.flags || []).includes('pinned')
this.application = application
},
},
}
@@ -47,37 +47,12 @@
</b-container>
</template>
<script>
import { isEqual } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CAuthclientEditorInfo from 'corteza-webapp-admin/src/components/Authclient/CAuthclientEditorInfo'
import { system } from '@cortezaproject/corteza-js'
import { mapGetters } from 'vuex'
const defSecurity = {
impersonateUser: '0',
permittedRoles: [],
prohibitedRoles: [],
forcedRoles: [],
}
// @todo move this to corteza-js and follow the pattern we use with other resource types
const makeNewAuthClient = () => JSON.parse(JSON.stringify({
scope: 'profile api',
enabled: true,
validGrant: 'authorization_code',
trusted: false,
handle: '',
meta: {
name: '',
description: '',
},
redirectURI: '',
security: {
...defSecurity,
},
}))
export default {
components: {
CAuthclientEditorInfo,
@@ -103,6 +78,7 @@ export default {
data () {
return {
authclient: undefined,
initialAuthclientState: undefined,
secret: '',
info: {
@@ -130,6 +106,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
watch: {
authClientID: {
immediate: true,
@@ -137,7 +121,8 @@ export default {
if (this.authClientID) {
this.fetchAuthclient()
} else {
this.authclient = makeNewAuthClient()
this.authclient = new system.AuthClient()
this.initialAuthclientState = this.authclient.clone()
}
},
},
@@ -149,7 +134,8 @@ export default {
this.$SystemAPI.authClientRead({ clientID: this.authClientID })
.then(ac => {
this.authclient = ac
this.authclient = new system.AuthClient(ac)
this.initialAuthclientState = this.authclient.clone()
})
.catch(this.toastErrorHandler(this.$t('notification:authclient.fetch.error')))
.finally(() => {
@@ -166,7 +152,8 @@ export default {
this.$SystemAPI.authClientUpdate({ clientID, ...authclient })
.then(ac => {
this.authclient = ac
this.authclient = new system.AuthClient(ac)
this.initialAuthclientState = this.authclient.clone()
this.toastSuccess(this.$t('notification:authclient.update.success'))
})
@@ -177,7 +164,8 @@ export default {
} else {
this.$SystemAPI.authClientCreate({ ...authclient })
.then((ac) => {
this.authclient = ac
this.authclient = new system.AuthClient(ac)
this.initialAuthclientState = this.authclient.clone()
const { authClientID } = ac
this.animateSuccess('info')
this.toastSuccess(this.$t('notification:authclient.create.success'))
@@ -229,6 +217,16 @@ export default {
.authClientRegenerateSecret(({ clientID }))
.then(newSecret => { this.secret = newSecret })
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.authclient, this.initialAuthclientState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -63,6 +63,7 @@
</template>
<script>
import { isEqual } from 'lodash'
import { system, NoID } from '@cortezaproject/corteza-js'
import { handle } from '@cortezaproject/corteza-vue'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
@@ -102,6 +103,7 @@ export default {
return {
processing: false,
connection: undefined,
initialConnectionState: undefined,
sensitivityLevels: undefined,
}
@@ -146,6 +148,14 @@ export default {
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
watch: {
connectionID: {
immediate: true,
@@ -154,6 +164,7 @@ export default {
this.fetchConnection(connectionID)
} else {
this.connection = new system.DalConnection()
this.initialConnectionState = this.connection.clone()
}
},
},
@@ -168,6 +179,7 @@ export default {
this.incLoader()
return this.$SystemAPI.dalConnectionRead({ connectionID }).then(connection => {
this.connection = new system.DalConnection(connection)
this.initialConnectionState = this.connection.clone()
}).catch(this.toastErrorHandler(this.$t('notification:connection.fetch.error')))
.finally(async () => {
this.decLoader()
@@ -204,6 +216,7 @@ export default {
this.$router.push({ name: `system.connection.edit`, params: { connectionID } })
} else {
this.connection = new system.DalConnection(connection)
this.initialConnectionState = this.connection.clone()
}
})
.catch(this.toastErrorHandler(this.$t(`notification:connection.${op}.error`)))
@@ -239,6 +252,16 @@ export default {
this.processing = false
})
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.connection, this.initialConnectionState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -34,6 +34,7 @@
</template>
<script>
import { isEqual, cloneDeep } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CQueueEditorInfo from 'corteza-webapp-admin/src/components/Queues/CQueueEditorInfo'
import { mapGetters } from 'vuex'
@@ -63,6 +64,7 @@ export default {
data () {
return {
queue: undefined,
initialQueueState: undefined,
consumers: [],
@@ -87,6 +89,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
watch: {
queueID: {
immediate: true,
@@ -102,6 +112,16 @@ export default {
poll_delay: '',
dispatch_events: false,
},
queue: '',
}
this.initialQueueState = {
consumer: 'corteza',
meta: {
poll_delay: '',
dispatch_events: false,
},
queue: '',
}
}
},
@@ -113,7 +133,10 @@ export default {
this.incLoader()
this.$SystemAPI.queuesRead({ queueID: this.queueID })
.then(q => { this.queue = q })
.then(q => {
this.queue = q
this.initialQueueState = cloneDeep(q)
})
.catch(this.toastErrorHandler(this.$t('notification:queue.fetch.error')))
.finally(() => {
this.decLoader()
@@ -139,6 +162,7 @@ export default {
this.$SystemAPI.queuesUpdate(queue)
.then(queue => {
this.queue = queue
this.initialQueueState = cloneDeep(queue)
this.animateSuccess('info')
this.toastSuccess(this.$t('notification:queue.update.success'))
@@ -179,6 +203,16 @@ export default {
this.decLoader()
})
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.queue, this.initialQueueState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -70,6 +70,7 @@
</template>
<script>
import { isEqual } from 'lodash'
import { system } from '@cortezaproject/corteza-js'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CRoleEditorInfo from 'corteza-webapp-admin/src/components/Role/CRoleEditorInfo'
@@ -104,9 +105,10 @@ export default {
data () {
return {
role: undefined,
initialRoleState: undefined,
isContext: false,
roleMembers: null,
roleMembers: [],
info: {
processing: false,
@@ -119,6 +121,14 @@ export default {
}
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
computed: {
...mapGetters({
can: 'rbac/can',
@@ -154,6 +164,7 @@ export default {
this.fetchRole()
} else {
this.role = new system.Role()
this.initialRoleState = this.role.clone()
this.isContext = false
}
},
@@ -181,6 +192,8 @@ export default {
this.$SystemAPI.roleRead({ roleID: this.roleID })
.then(r => {
this.role = new system.Role(r)
this.initialRoleState = this.role.clone()
this.isContext = !!this.role.isContext
if (this.role.canManageMembersOnRole && !this.role.isContext && !this.role.isClosed) {
@@ -315,6 +328,17 @@ export default {
})
}
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
const isDirty = this.roleMembers.some(m => m.dirty !== m.current) || !isEqual(this.role, this.initialRoleState)
next(isDirty ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -32,6 +32,7 @@
</b-container>
</template>
<script>
import { isEqual, cloneDeep } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CSensitivityLevelEditorInfo from 'corteza-webapp-admin/src/components/SensitivityLevel/CSensitivityLevelEditorInfo'
import { mapGetters } from 'vuex'
@@ -61,6 +62,7 @@ export default {
data () {
return {
sensitivityLevel: undefined,
initialSensitivityLevelState: undefined,
info: {
processing: false,
@@ -87,6 +89,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
watch: {
sensitivityLevelID: {
immediate: true,
@@ -102,6 +112,8 @@ export default {
description: '',
},
}
this.initialSensitivityLevelState = cloneDeep(this.sensitivityLevel)
}
},
},
@@ -114,6 +126,7 @@ export default {
this.$SystemAPI.dalSensitivityLevelRead({ sensitivityLevelID })
.then(sensitivityLevel => {
this.sensitivityLevel = sensitivityLevel
this.initialSensitivityLevelState = cloneDeep(sensitivityLevel)
})
.catch(this.toastErrorHandler(this.$t('notification:sensitivityLevel.fetch.error')))
.finally(() => {
@@ -128,6 +141,7 @@ export default {
this.$SystemAPI.dalSensitivityLevelUpdate(sensitivityLevel)
.then(sensitivityLevel => {
this.sensitivityLevel = sensitivityLevel
this.initialSensitivityLevelState = cloneDeep(sensitivityLevel)
this.toastSuccess(this.$t('notification:sensitivityLevel.update.success'))
})
@@ -139,6 +153,8 @@ export default {
this.$SystemAPI.dalSensitivityLevelCreate(sensitivityLevel)
.then(sensitivityLevel => {
this.sensitivityLevel = sensitivityLevel
this.initialSensitivityLevelState = cloneDeep(sensitivityLevel)
const { sensitivityLevelID } = sensitivityLevel
this.animateSuccess('info')
this.toastSuccess(this.$t('notification:sensitivityLevel.create.success'))
@@ -178,6 +194,16 @@ export default {
.finally(() => this.decLoader())
}
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.sensitivityLevel, this.initialSensitivityLevelState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -62,6 +62,7 @@
</template>
<script>
import { isEqual } from 'lodash'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CTemplateEditorInfo from 'corteza-webapp-admin/src/components/Template/CTemplateEditorInfo'
import CTemplateEditorContent from 'corteza-webapp-admin/src/components/Template/CTemplateEditorContent/Index'
@@ -94,6 +95,7 @@ export default {
data () {
return {
template: undefined,
initialTemplateState: undefined,
info: {
processing: false,
@@ -131,11 +133,20 @@ export default {
this.fetchTemplate()
} else {
this.template = new system.Template()
this.initialTemplateState = this.template.clone()
}
},
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
methods: {
fetchTemplate () {
this.incLoader()
@@ -143,6 +154,7 @@ export default {
this.$SystemAPI.templateRead({ templateID: this.templateID })
.then(t => {
this.template = new system.Template(t)
this.initialTemplateState = this.template.clone()
})
.catch(this.toastErrorHandler(this.$t('notification:template.fetch.error')))
.finally(() => {
@@ -198,7 +210,8 @@ export default {
if (this.templateID) {
this.$SystemAPI.templateUpdate(template)
.then(template => {
this.template = template
this.template = new system.Template(template)
this.initialTemplateState = this.template.clone()
this.toastSuccess(this.$t('notification:template.update.success'))
})
@@ -222,6 +235,16 @@ export default {
})
}
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
next(!isEqual(this.template, this.initialTemplateState) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -100,6 +100,7 @@
</template>
<script>
import { isEqual } from 'lodash'
import { NoID, system } from '@cortezaproject/corteza-js'
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CUserEditorInfo from 'corteza-webapp-admin/src/components/User/CUserEditorInfo'
@@ -140,10 +141,11 @@ export default {
data () {
return {
user: undefined,
initialUserState: undefined,
membership: {
active: [],
original: [],
initial: [],
},
externalAuthProviders: [],
@@ -200,11 +202,20 @@ export default {
this.fetchExternalAuthProviders()
} else {
this.user = new system.User()
this.initialUserState = this.user.clone()
}
},
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChanges(next, to)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChanges(next, to)
},
methods: {
makeEvent (res) {
return system.UserEvent(res)
@@ -216,6 +227,7 @@ export default {
return this.$SystemAPI.userRead({ userID: this.userID })
.then(user => {
this.user = new system.User(user)
this.initialUserState = this.user.clone()
})
.catch(this.toastErrorHandler(this.$t('notification:user.fetch.error')))
.finally(() => {
@@ -227,7 +239,7 @@ export default {
this.incLoader()
return this.$SystemAPI.userMembershipList({ userID: this.userID })
.then((set = []) => {
this.membership = { active: [...set], original: [...set] }
this.membership = { active: [...set], initial: [...set] }
})
.catch(this.toastErrorHandler(this.$t('notification:user.roles.error')))
.finally(() => {
@@ -264,6 +276,7 @@ export default {
this.$SystemAPI.userUpdate(payload)
.then(user => {
this.user = new system.User(user)
this.initialUserState = this.user.clone()
this.animateSuccess('info')
this.toastSuccess(this.$t('notification:user.update.success'))
@@ -404,15 +417,15 @@ export default {
const userID = this.userID
const { active, original } = this.membership
const { active, initial } = this.membership
Promise.all([
// all removed memberships
...original.filter(roleID => !active.includes(roleID)).map(roleID => {
...initial.filter(roleID => !active.includes(roleID)).map(roleID => {
return this.$SystemAPI.userMembershipRemove({ roleID, userID })
}),
// all new memberships
...active.filter(roleID => !original.includes(roleID)).map(roleID => {
...active.filter(roleID => !initial.includes(roleID)).map(roleID => {
return this.$SystemAPI.userMembershipAdd({ roleID, userID })
}),
])
@@ -499,6 +512,19 @@ export default {
})
.catch(this.toastErrorHandler(this.$t('notification:user.avatarDelete.error')))
},
checkUnsavedChanges (next, to) {
const isNewPage = this.$route.path.includes('/new') && to.name.includes('edit')
if (isNewPage) {
next(true)
} else if (!to.name.includes('edit')) {
let userChangesStatus = !isEqual(this.user, this.initialUserState)
let membershipChangesStatus = !isEqual(this.membership.initial, this.membership.active)
next((userChangesStatus || membershipChangesStatus) ? window.confirm(this.$t('general:editor.unsavedChanges')) : true)
}
},
},
}
</script>
@@ -25,7 +25,7 @@
</div>
<div class="mt-3">
<b-table-simple
v-if="rule.constraints.length > 0"
v-if="rule.constraints && rule.constraints.length > 0"
borderless
>
<thead>
@@ -145,12 +145,6 @@ export default {
computed: {
rules: {
get () {
this.module.config.recordDeDup.rules.forEach(rule => {
if (rule.constraints === null) {
rule.constraints = []
}
})
return this.module.config.recordDeDup.rules
},
set (value) {
@@ -185,6 +179,10 @@ export default {
},
updateRuleConstraint (rule) {
if (!rule.constraints) {
rule.constraints = []
}
rule.constraints.push({
attribute: rule.currentField.name,
modifier: 'case-sensitive',
@@ -197,7 +195,7 @@ export default {
},
filterFieldOptions (rule) {
const selectedFields = rule.constraints.map(({ attribute }) => attribute)
const selectedFields = rule.constraints ? rule.constraints.map(({ attribute }) => attribute) : []
return this.module.fields.filter(({ name }) => !selectedFields.includes(name))
},
@@ -311,6 +311,7 @@
</div>
</template>
<script>
import { isEqual, debounce, cloneDeep } from 'lodash'
import { mapGetters, mapActions } from 'vuex'
import EditorToolbar from 'corteza-webapp-compose/src/components/Admin/EditorToolbar'
import { compose, NoID, shared } from '@cortezaproject/corteza-js'
@@ -323,7 +324,7 @@ import Reports from 'corteza-webapp-compose/src/components/Chart/Report'
import { chartConstructor } from 'corteza-webapp-compose/src/lib/charts'
import VueSelect from 'vue-select'
import { evaluatePrefilter } from 'corteza-webapp-compose/src/lib/record-filter'
import { debounce } from 'lodash'
const { CInputCheckbox } = components
const { colorschemes } = shared
@@ -370,6 +371,7 @@ export default {
data () {
return {
chart: undefined,
initialChartState: undefined,
processing: false,
editReportIndex: undefined,
@@ -519,6 +521,8 @@ export default {
immediate: true,
handler (chartID) {
this.chart = undefined
this.initialChartState = undefined
const { namespaceID } = this.namespace
if (chartID === NoID) {
@@ -537,11 +541,13 @@ export default {
break
}
this.chart = c
this.initialChartState = cloneDeep(c)
this.onEditReport(0)
} else {
this.findChartByID({ namespaceID, chartID, force: true }).then((chart) => {
// Make a copy so that we do not change store item by ref
this.chart = chartConstructor(chart)
this.initialChartState = cloneDeep(chartConstructor(chart))
this.onEditReport(0)
}).catch(this.toastErrorHandler(this.$t('notification:chart.loadFailed')))
}
@@ -558,6 +564,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChart(next)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChart(next)
},
methods: {
...mapActions({
findChartByID: 'chart/findByID',
@@ -608,6 +622,7 @@ export default {
if (this.chart.chartID === NoID) {
this.createChart(c).then(({ chartID }) => {
this.toastSuccess(this.$t('notification:chart.saved'))
this.initialChartState = cloneDeep(chartConstructor(this.chart))
if (closeOnSuccess) {
this.redirect()
} else {
@@ -617,6 +632,7 @@ export default {
} else {
this.updateChart(c).then((chart) => {
this.chart = chartConstructor(chart)
this.initialChartState = cloneDeep(chartConstructor(chart))
this.toastSuccess(this.$t('notification:chart.saved'))
if (closeOnSuccess) {
this.redirect()
@@ -654,6 +670,10 @@ export default {
getOptionKey ({ value }) {
return value
},
checkUnsavedChart (next) {
next(!isEqual(this.chart, this.initialChartState) ? window.confirm(this.$t('notification.unsavedChanges')) : true)
},
},
}
</script>
@@ -452,6 +452,7 @@
</template>
<script>
import { isEqual } from 'lodash'
import { mapGetters, mapActions } from 'vuex'
import draggable from 'vuedraggable'
import FieldConfigurator from 'corteza-webapp-compose/src/components/ModuleFields/Configurator'
@@ -516,6 +517,7 @@ export default {
updateField: null,
module: undefined,
initialModuleState: undefined,
hasRecords: true,
processing: false,
@@ -643,6 +645,7 @@ export default {
immediate: true,
handler (moduleID) {
this.module = undefined
this.initialModuleState = undefined
/**
* Every time module changes we switch to the 1st tab
@@ -654,6 +657,7 @@ export default {
{ fields: [new compose.ModuleFieldString({ fieldID: NoID, name: this.$t('general.placeholder.sample') })] },
this.namespace,
)
this.initialModuleState = this.module.clone()
} else {
const params = {
// make sure module is loaded from the API every time!
@@ -665,6 +669,7 @@ export default {
this.findModuleByID(params).then((module) => {
// Make a copy so that we do not change store item by ref
this.module = module.clone()
this.initialModuleState = module.clone()
const { moduleID, namespaceID, issues = [] } = this.module
@@ -692,6 +697,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedModule(next)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedModule(next)
},
methods: {
...mapActions({
findModuleByID: 'module/findByID',
@@ -702,6 +715,10 @@ export default {
deletePage: 'page/delete',
}),
checkUnsavedModule (next) {
next(!isEqual(this.module.clone(), this.initialModuleState.clone()) ? window.confirm(this.$t('general.unsavedChanges')) : true)
},
handleNewField () {
this.module.fields.push(new compose.ModuleFieldString())
},
@@ -766,6 +783,7 @@ export default {
}
this.module = new compose.Module({ ...module }, this.namespace)
this.initialModuleState = this.module.clone()
this.toastSuccess(this.$t('notification:module.created'))
if (closeOnSuccess) {
@@ -780,6 +798,8 @@ export default {
} else {
this.updateModule({ ...this.module, resourceTranslationLanguage }).then(module => {
this.module = new compose.Module({ ...module }, this.namespace)
this.initialModuleState = this.module.clone()
this.toastSuccess(this.$t('notification:module.saved'))
if (closeOnSuccess) {
this.$router.push({ name: 'admin.modules' })
@@ -812,6 +832,7 @@ export default {
this.$SystemAPI.dalConnectionRead({ connectionID })
.then(connection => {
this.connection = connection
this.initialModuleState.config.dal.connectionID = connection.connectionID
})
.catch(this.toastErrorHandler(this.$t('notification:connection.read-failed')))
.finally(() => {
@@ -882,7 +882,6 @@ export default {
}
} catch (error) {
this.toastWarning(this.$t('notification:page.invalidBlock'))
console.log(error)
}
}
},
@@ -755,6 +755,7 @@
</template>
<script>
import { isEqual } from 'lodash'
import { mapGetters, mapActions } from 'vuex'
import EditorToolbar from 'corteza-webapp-compose/src/components/Admin/EditorToolbar'
import PageTranslator from 'corteza-webapp-compose/src/components/Admin/Page/PageTranslator'
@@ -803,6 +804,7 @@ export default {
processing: false,
page: new compose.Page(),
initialPageState: new compose.Page(),
showIconModal: false,
attachments: [],
@@ -949,6 +951,7 @@ export default {
immediate: true,
handler (pageID) {
this.page = undefined
this.initialPageState = undefined
this.layouts = []
this.removedLayouts = new Set()
@@ -959,6 +962,7 @@ export default {
const { namespaceID } = this.namespace
this.findPageByID({ namespaceID, pageID, force: true }).then((page) => {
this.page = page.clone()
this.initialPageState = page.clone()
return this.fetchAttachments()
}).then(this.fetchLayouts)
.finally(() => {
@@ -969,6 +973,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedComposePage(next)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedComposePage(next)
},
created () {
this.fetchRoles()
},
@@ -989,7 +1001,7 @@ export default {
async fetchLayouts () {
const { namespaceID } = this.namespace
return this.findLayoutsByPageID({ namespaceID, pageID: this.pageID, force: true }).then(layouts => {
this.layouts = layouts
this.layouts = layouts.map((layout) => new compose.PageLayout(layout))
})
},
@@ -1065,6 +1077,7 @@ export default {
return this.updatePage({ namespaceID, ...this.page, resourceTranslationLanguage })
}).then(page => {
this.page = page.clone()
this.initialPageState = page.clone()
return this.handleSaveLayouts()
}).then(this.handlePageLayoutReorder)
.then(() => {
@@ -1161,6 +1174,13 @@ export default {
layoutHandleState (layoutHandle) {
return handle.handleState(layoutHandle)
},
checkUnsavedComposePage (next) {
const layoutsStateChange = this.layouts.some((layout) => layout.meta.updated)
const pageStateChange = !isEqual(this.page, this.initialPageState)
next((layoutsStateChange || pageStateChange) ? window.confirm(this.$t('unsavedChanges')) : true)
},
},
}
</script>
@@ -318,6 +318,7 @@
</template>
<script>
import { isEqual } from 'lodash'
import { compose, NoID } from '@cortezaproject/corteza-js'
import { url, handle } from '@cortezaproject/corteza-vue'
import EditorToolbar from 'corteza-webapp-compose/src/components/Admin/EditorToolbar'
@@ -342,14 +343,20 @@ export default {
processing: false,
namespace: new compose.Namespace({ enabled: true }),
initialNamespaceState: new compose.Namespace({ enabled: true }),
namespaceAssets: {
logo: undefined,
icon: undefined,
},
namespaceAssetsInitialState: {
logo: undefined,
icon: undefined,
},
namespaceEnabled: false,
application: undefined,
isApplication: false,
isApplicationInitialState: false,
}
},
@@ -436,6 +443,14 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedNamespace(next)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedNamespace(next)
},
methods: {
...mapActions({
updateNamespace: 'namespace/update',
@@ -452,6 +467,7 @@ export default {
this.application = undefined
this.isApplication = false
this.isApplicationInitialState = this.isApplication
if (namespaceID) {
await this.findNamespace({ namespaceID })
@@ -477,6 +493,8 @@ export default {
...this.namespace.meta,
}
this.initialNamespaceState = this.namespace.clone()
this.processing = false
this.loaded = true
},
@@ -504,6 +522,7 @@ export default {
if (set.length) {
this.application = set[0]
this.isApplication = this.application.enabled
this.isApplicationInitialState = this.isApplication
}
})
.catch(this.toastErrorHandler(this.$t('notification:namespace.application.fetchFailed')))
@@ -525,6 +544,7 @@ export default {
try {
assets = await this.uploadAssets()
meta = { ...meta, ...assets }
this.namespaceAssetsInitialState = this.namespaceAssets
} catch (e) {
const error = JSON.stringify(e) === '{}' ? '' : e
this.toastErrorHandler(this.$t('notification:namespace.assetUploadFailed'))(error)
@@ -587,6 +607,7 @@ export default {
if (closeOnSuccess) {
this.$router.push({ name: 'namespace.manage' })
} else if (!this.isEdit || this.isClone) {
this.initialNamespaceState = this.namespace.clone()
this.$router.push({ name: 'namespace.edit', params: { namespaceID: this.namespace.namespaceID } })
}
@@ -596,6 +617,8 @@ export default {
hideSidebar: false,
...this.namespace.meta,
}
this.initialNamespaceState = this.namespace.clone()
},
handleDelete () {
@@ -705,6 +728,14 @@ export default {
this.namespace.meta.logo = undefined
this.namespace.meta.logoID = undefined
},
checkUnsavedNamespace (next) {
const namespaceState = !isEqual(JSON.stringify(this.namespace), JSON.stringify(this.initialNamespaceState))
const isApplicationState = !(this.isApplication === this.isApplicationInitialState)
const namespaceAssetsState = !isEqual(this.namespaceAssets, this.namespaceAssetsInitialState)
next((namespaceState || isApplicationState || namespaceAssetsState) ? window.confirm(this.$t('manage.unsavedChanges')) : true)
},
},
}
</script>
@@ -165,6 +165,8 @@ export default {
sizes.forEach((size, index) => {
this.block.elements[index].meta.size = size
})
this.$emit('item-updated', this.index)
},
getScenarioDefinition (element) {
@@ -28,6 +28,8 @@
:y="item.y"
:class="{ 'editable-grid-item': editable }"
drag-ignore-from=".gutter"
@moved="onBlockUpdated(index)"
@resized="onBlockUpdated(index)"
>
<slot
:block="blocks[item.i]"
@@ -110,6 +112,12 @@ export default {
},
},
},
methods: {
onBlockUpdated (index) {
this.$emit('item-updated', index)
},
},
}
</script>
+5
View File
@@ -15,6 +15,7 @@ export default {
return this.$SystemAPI.reportRead({ reportID })
.then(report => {
this.report = new system.Report(report)
this.initialReportState = this.report.clone()
})
.catch(this.toastErrorHandler(this.$t('notification:report.fetchFailed')))
.finally(() => {
@@ -44,6 +45,8 @@ export default {
return this.$SystemAPI.reportCreate(report)
.then(report => {
this.report = new system.Report(report)
this.initialReportState = this.report.clone()
this.detectStateChange = false
this.toastSuccess(this.$t('notification:report.created'))
this.$router.push({ name: 'report.edit', params: { reportID: report.reportID } })
})
@@ -55,6 +58,8 @@ export default {
return this.$SystemAPI.reportUpdate(report)
.then(report => {
this.report = new system.Report(report)
this.initialReportState = this.report.clone()
this.detectStateChange = false
this.toastSuccess(this.$t('notification:report.updated'))
})
.catch(this.toastErrorHandler(this.$t('notification:report.updateFailed')))
@@ -85,6 +85,7 @@
v-if="report && canRead && showReport"
:blocks.sync="reportBlocks"
editable
@item-updated="onBlockUpdated"
>
<template
slot-scope="{ block, index }"
@@ -95,6 +96,17 @@
<div
class="toolbox border-0 p-2 m-0 text-light text-center"
>
<div
v-if="unsavedBlocks.has(index)"
:title="$t('tooltip.unsavedChanges')"
class="btn border-0"
>
<font-awesome-icon
:icon="['fas', 'exclamation-triangle']"
class="text-warning"
/>
</div>
<b-button-group>
<b-button
:title="$t('builder:tooltip.add.displayElement')"
@@ -134,6 +146,7 @@
:block="block"
:scenario="currentSelectedScenario"
:report-i-d="reportID"
@item-updated="onBlockUpdated"
/>
</div>
</template>
@@ -411,6 +424,8 @@ export default {
report: undefined,
unsavedBlocks: new Set(),
dataframes: [],
blocks: {
@@ -630,10 +645,19 @@ export default {
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedBlocks(next)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedBlocks(next)
},
watch: {
reportID: {
immediate: true,
handler (reportID) {
this.unsavedBlocks.clear()
this.scenarios.selected = undefined
if (reportID) {
@@ -814,6 +838,7 @@ export default {
.then(() => {
this.mapBlocks()
this.refreshReport()
this.unsavedBlocks.clear()
})
},
@@ -842,6 +867,8 @@ export default {
},
updateBlock () {
this.unsavedBlocks.add(this.blocks.currentIndex)
if (this.currentBlock) {
const elements = this.currentBlock.elements
@@ -860,6 +887,7 @@ export default {
deleteBlock (index = undefined) {
this.reindexBlocks(this.reportBlocks.filter((p, i) => index !== i))
this.unsavedBlocks.add(index)
},
// Display elements
@@ -887,6 +915,8 @@ export default {
this.editBlock(this.blocks.currentIndex)
this.setCurrentDisplayElement(this.currentBlock.elements.length - 1)
this.updateBlock()
},
// Scenarios
@@ -924,6 +954,15 @@ export default {
getOptionKey (scenario) {
return scenario
},
// Trigger browser dialog on page leave to prevent unsaved changes
checkUnsavedBlocks (next) {
next(!this.unsavedBlocks.size || window.confirm(this.$t('builder:unsaved-changes')))
},
onBlockUpdated (index) {
this.unsavedBlocks.add(index)
},
},
}
</script>
@@ -103,6 +103,7 @@
:placeholder="$t('name')"
required
:state="nameState"
@input="handleDetectStateChange"
/>
</b-form-group>
</b-col>
@@ -121,6 +122,7 @@
:placeholder="$t('placeholder-handle')"
required
:state="handleState"
@input="handleDetectStateChange"
/>
<b-form-invalid-feedback
data-test-id="input-handle-invalid-state"
@@ -141,6 +143,7 @@
data-test-id="input-description"
:placeholder="$t('report.description')"
rows="5"
@input="handleDetectStateChange"
/>
</b-form-group>
@@ -178,6 +181,7 @@ import { handle } from '@cortezaproject/corteza-vue'
import report from 'corteza-webapp-reporter/src/mixins/report'
import EditorToolbar from 'corteza-webapp-reporter/src/components/EditorToolbar'
import { mapGetters } from 'vuex'
import { isEqual } from 'lodash'
export default {
name: 'EditReport',
@@ -197,6 +201,9 @@ export default {
processing: false,
report: undefined,
initialReportState: undefined,
detectStateChange: false,
}
},
@@ -268,9 +275,44 @@ export default {
this.fetchReport(reportID)
} else {
this.report = new system.Report()
this.initialReportState = new system.Report()
}
},
},
},
beforeRouteUpdate (to, from, next) {
this.checkUnsavedChart(next)
},
beforeRouteLeave (to, from, next) {
this.checkUnsavedChart(next)
},
methods: {
handleDetectStateChange () {
this.detectStateChange = true
},
checkUnsavedChart (next) {
const reportState = {
handle: this.report.handle,
meta: {
name: this.report.meta.name,
description: this.report.meta.description,
},
}
const initialReportState = {
handle: this.initialReportState.handle,
meta: {
name: this.initialReportState.meta.name,
description: this.initialReportState.meta.description,
},
}
next(!isEqual(reportState, initialReportState) ? window.confirm(this.$t('unsavedChanges')) : true)
},
},
}
</script>
+17 -1
View File
@@ -121,7 +121,23 @@ export class ModuleField {
applyOptions (o?: Partial<Options>): void {
if (!o) return
Apply(this.options, o, Object, 'description', 'hint')
if (o.description) {
this.options.description = {
...this.options.description,
...o.description,
}
this.options.description.edit = this.options.description.edit || undefined
}
if (o.hint) {
this.options.hint = {
...this.options.hint,
...o.hint,
}
this.options.hint.edit = this.options.hint.edit || undefined
}
}
clone (): ModuleField {
+1
View File
@@ -6,6 +6,7 @@ export { Reminder } from './types/reminder'
export { Template } from './types/template'
export { Report } from './types/report'
export { DalConnection } from './types/dalConnection'
export { AuthClient } from './types/authClient'
export {
SystemEvent,
RoleEvent,
+69 -2
View File
@@ -1,4 +1,71 @@
import { Apply, CortezaID, ISO8601Date, NoID } from '../../cast'
import { IsOf } from '../../guards'
interface PartialApplication extends Partial<Omit<Application, 'createdAt' | 'updatedAt' | 'deletedAt' | 'lastUsedAt'>> {
createdAt?: string|number|Date;
updatedAt?: string|number|Date;
}
interface Unify {
name: string;
listed: boolean,
url: string,
config: string,
iconID: string,
logoID: string
}
export class Application {
// @todo port application class here
[_: string]: unknown
public applicationID = undefined
public name = ''
public ownerID?: number = 0;
public enabled = false
public weight?: number = 0;
public unify?: Unify = {
name: '',
listed: false,
url: '',
config: '',
iconID: NoID,
logoID: NoID,
};
public canGrant: boolean = true;
public canUpdateApplication: boolean = true;
public canDeleteApplication: boolean = true;
constructor (r?: PartialApplication) {
this.apply(r)
}
apply (r?: PartialApplication): void {
Apply(this, r, CortezaID, 'applicationID')
Apply(this, r, String, 'name')
Apply(this, r, Number, 'weight', 'ownerID')
Apply(this, r, Boolean, 'enabled', 'canGrant', 'canUpdateApplication', 'canDeleteApplication')
if (r && IsOf(r, 'unify')) {
this.unify = r.unify
}
}
/**
* Returns resource ID
*/
get resourceID (): string {
return `${this.resourceType}:${this.applicationID}`
}
/**
* Resource type
*/
get resourceType (): string {
return 'system:application'
}
clone (): Application {
return new Application(JSON.parse(JSON.stringify(this)))
}
}
+88
View File
@@ -0,0 +1,88 @@
import { Apply, CortezaID, ISO8601Date, NoID } from "../../cast";
import { IsOf } from "../../guards";
interface PartialAuthClient
extends Partial<
Omit<AuthClient, "createdAt" | "updatedAt" | "deletedAt" | "lastUsedAt">
> {
createdAt?: string | number | Date;
updatedAt?: string | number | Date;
deletedAt?: string | number | Date;
lastUsedAt?: string | number | Date;
}
interface AuthClientMeta {
name: string;
description: string;
}
interface DefSecurity {
impersonateUser: string;
permittedRoles: Array<string>;
prohibitedRoles: Array<string>;
forcedRoles: Array<string>;
}
export class AuthClient {
public authClientID = NoID;
public handle = "";
public scope = "profile api";
public redirectURI = "";
public validGrant = 'authorization_code';
public meta: AuthClientMeta = {
name: "",
description: "",
};
public security: DefSecurity = {
impersonateUser: "0",
permittedRoles: [],
prohibitedRoles: [],
forcedRoles: [],
};
public enabled = true;
public trusted = false;
public createdAt?: Date = undefined;
public updatedAt?: Date = undefined;
public deletedAt?: Date = undefined;
public createdBy = NoID;
public updatedBy = NoID;
public deletedBy = NoID;
public canDeleteAuthClient = false;
public canGrant = false;
public canUpdateAuthClient = false;
constructor (o?: PartialAuthClient) {
this.apply(o)
}
apply (o?: PartialAuthClient): void {
Apply(this, o, CortezaID, 'authClientID')
Apply(this, o, ISO8601Date, 'createdAt', 'updatedAt', 'deletedAt');
Apply(this, o, String, 'handle', 'scope', 'redirectURI', 'validGrant');
Apply(this, o, Boolean, 'enabled', 'trusted', 'canDeleteAuthClient', 'canGrant', 'canUpdateAuthClient');
if (IsOf(o, 'meta')) {
this.meta = { ...o.meta }
}
if (IsOf(o, 'security')) {
this.security = {
...this.security,
...o.security
}
}
Apply(this, o, CortezaID, 'createdBy', 'updatedBy', 'deletedBy');
}
clone(): AuthClient {
return new AuthClient(JSON.parse(JSON.stringify(this)));
}
}
+14 -4
View File
@@ -89,9 +89,9 @@ export class DalConnection {
public updatedAt?: Date = undefined
public deletedAt?: Date = undefined
public createdBy = ''
public updatedBy = ''
public deletedBy = ''
public createdBy = NoID
public updatedBy = NoID
public deletedBy = NoID
public canDeleteConnection = false
public canManageDalConfig = false
@@ -112,7 +112,7 @@ export class DalConnection {
}
if (IsOf(dc, 'config')) {
this.config = { ...dc.config.privacy }
this.config = { ...dc.config }
if (this.connectionID !== NoID && this.canManageDalConfig) {
this.config = {
@@ -124,6 +124,12 @@ export class DalConnection {
},
...dc.config,
}
if (!this.config.privacy.sensitivityLevelID) {
this.config.privacy = {
sensitivityLevelID: NoID,
}
}
}
}
@@ -141,4 +147,8 @@ export class DalConnection {
}
}
}
clone (): DalConnection {
return new DalConnection(JSON.parse(JSON.stringify(this)))
}
}
+8 -1
View File
@@ -35,7 +35,10 @@ interface ReportScenario {
export class Report {
public reportID = NoID
public handle = ''
public meta: Meta = {}
public meta: Meta = {
name: '',
description: '',
}
public sources: Array<ReportDataSource> = []
public blocks: Array<Block> = []
public scenarios: Array<ReportScenario> = []
@@ -102,4 +105,8 @@ export class Report {
'canRunReport',
)
}
clone (): Report {
return new Report(JSON.parse(JSON.stringify(this)))
}
}
+4
View File
@@ -100,4 +100,8 @@ export class Role {
get isContext (): boolean {
return this.meta?.context?.expr?.length > 0
}
clone (): Role {
return new Role(JSON.parse(JSON.stringify(this)))
}
}
+8 -1
View File
@@ -19,7 +19,10 @@ export class Template {
public language = ''
public type = 'text/html'
public partial = false
public meta: Meta = {}
public meta: Meta = {
short: '',
description: '',
}
public template = ''
public labels: object = {}
public ownerID = NoID
@@ -63,4 +66,8 @@ export class Template {
get resourceType (): string {
return 'system:template'
}
clone (): Template {
return new Template(JSON.parse(JSON.stringify(this)))
}
}
+4
View File
@@ -106,4 +106,8 @@ export class User {
this.userID,
].join(' ').toLocaleLowerCase()
}
clone (): User {
return new User(JSON.parse(JSON.stringify(this)))
}
}
+2 -1
View File
@@ -47,4 +47,5 @@ label:
delete: Delete
undelete: Undelete
fileTypeNotAllowed: File type not allowed
editor:
unsavedChanges: Unsaved changes will be lost. Do you wish to leave the page?
@@ -220,6 +220,7 @@ name: Chart name *
handle: Handle
generalSettings: General settings
notification:
unsavedChanges: Unsaved changes will be lost. Do you wish to leave the page?
loadFailed: Could not load chart
saveFailed: Could not save this chart
saved: Chart saved
@@ -193,6 +193,7 @@ forModule:
recordPage: Record page for module
general:
fields: Module fields
unsavedChanges: Unsaved changes will be lost. Do you wish to leave the page?
label:
attributes: Attributes
handle: Handle
@@ -58,6 +58,7 @@ manage:
title: Manage namespaces
list-view: Back to namespace list
delete: Delete
unsavedChanges: Unsaved changes will be lost. Do you wish to leave the page?
disabled: Disabled
@@ -88,6 +88,7 @@ moduleEdit: Edit module
navigation:
page: Pages
viewPage: View Page
unsavedChanges: Unsaved changes will be lost. Do you wish to leave the page?
newPlaceholder: Page title
noBlock: No block added yet
noPages: No pages
@@ -73,6 +73,7 @@ select-all: Select all
unselect-all: Unselect all
value: Value
name: Name
unsaved-changes: Unsaved changes will be lost. Do you wish to leave the page?
filter:
operators:
like: Like
@@ -5,6 +5,7 @@ name: Name
name-with-star: 'Name*'
new-report: New Report
permissions: Permissions
unsavedChanges: Unsaved changes will be lost. Do you wish to leave the page?
report:
create: Create Report
builder: Report Builder