Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Principal } from '@dfinity/principal';
import { describe, expect, it } from 'vitest';
import WasmMemoryPersistenceSelect from '~/components/inputs/WasmMemoryPersistenceSelect.vue';
import { mount } from '~/test.utils';
import CanisterInstallForm from './CanisterInstallForm.vue';
import { CanisterInstallModel } from './external-canisters.types';

describe('CanisterInstallForm', () => {
it('hides the canisterId when display is set to false', () => {
Expand All @@ -27,4 +29,64 @@ describe('CanisterInstallForm', () => {

expect(canisterIdInput.exists()).toBe(true);
});

it('shows the upgrade options only for the upgrade mode', () => {
const upgradeForm = mount(CanisterInstallForm, {
props: {
modelValue: { canisterId: Principal.anonymous(), mode: { upgrade: [] } },
},
});
expect(upgradeForm.find('[name="wasm_memory_persistence"]').exists()).toBe(true);
expect(upgradeForm.find('[name="skip_pre_upgrade"]').exists()).toBe(true);

const installForm = mount(CanisterInstallForm, {
props: {
modelValue: { canisterId: Principal.anonymous(), mode: { install: null } },
},
});
expect(installForm.find('[name="wasm_memory_persistence"]').exists()).toBe(false);
expect(installForm.find('[name="skip_pre_upgrade"]').exists()).toBe(false);
});

it('writes the selected wasm memory persistence into the upgrade mode', async () => {
const form = mount(CanisterInstallForm, {
props: {
modelValue: { canisterId: Principal.anonymous(), mode: { upgrade: [] } },
},
});

const select = form.findComponent(WasmMemoryPersistenceSelect);
expect(select.exists()).toBe(true);

select.vm.$emit('update:modelValue', { keep: null });
await form.vm.$nextTick();

const emitted = form.emitted('update:modelValue') as CanisterInstallModel[][] | undefined;
expect(emitted).toBeTruthy();
const latest = emitted![emitted!.length - 1][0];
expect(latest.mode).toEqual({
upgrade: [{ wasm_memory_persistence: [{ keep: null }], skip_pre_upgrade: [] }],
});
});

it('collapses the upgrade options back to an empty upgrade when cleared', async () => {
const form = mount(CanisterInstallForm, {
props: {
modelValue: {
canisterId: Principal.anonymous(),
mode: { upgrade: [{ wasm_memory_persistence: [{ keep: null }], skip_pre_upgrade: [] }] },
},
},
});

const select = form.findComponent(WasmMemoryPersistenceSelect);
// Vuetify's `clearable` emits `undefined`/`null`.
select.vm.$emit('update:modelValue', undefined);
await form.vm.$nextTick();

const emitted = form.emitted('update:modelValue') as CanisterInstallModel[][] | undefined;
expect(emitted).toBeTruthy();
const latest = emitted![emitted!.length - 1][0];
expect(latest.mode).toEqual({ upgrade: [] });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@
<VCol cols="12" class="pb-0">
<CanisterInstallModeSelect v-model="model.mode" :readonly="props.readonly" required />
</VCol>
<template v-if="isUpgrade">
<VCol cols="12" class="pb-0">
<WasmMemoryPersistenceSelect
v-model="wasmMemoryPersistence"
:readonly="props.readonly"
name="wasm_memory_persistence"
/>
</VCol>
<VCol cols="12" class="pb-0">
<VCheckbox
v-model="skipPreUpgrade"
name="skip_pre_upgrade"
:label="$t('external_canisters.skip_pre_upgrade')"
:readonly="props.readonly"
density="comfortable"
hide-details
/>
</VCol>
</template>
<VCol cols="12" class="pb-0">
<CanisterWasmModuleField
v-model="model.wasmModule"
Expand All @@ -39,11 +58,14 @@
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { VCol, VContainer, VForm, VRow } from 'vuetify/components';
import { VCheckbox, VCol, VContainer, VForm, VRow } from 'vuetify/components';
import CanisterArgumentField from '~/components/inputs/CanisterArgumentField.vue';
import CanisterInstallModeSelect from '~/components/inputs/CanisterInstallModeSelect.vue';
import CanisterWasmModuleField from '~/components/inputs/CanisterWasmModuleField.vue';
import WasmMemoryPersistenceSelect from '~/components/inputs/WasmMemoryPersistenceSelect.vue';
import { VFormValidation } from '~/types/helper.types';
import { WasmMemoryPersistence } from '~/types/station.types';
import { variantIs } from '~/utils/helper.utils';
import CanisterIdField from '../inputs/CanisterIdField.vue';
import { CanisterIcSettingsModel, CanisterInstallModel } from './external-canisters.types';

Expand Down Expand Up @@ -83,6 +105,46 @@ const model = computed({
set: value => emit('update:modelValue', value),
});

const isUpgrade = computed(
() => model.value.mode !== undefined && variantIs(model.value.mode, 'upgrade'),
);

// The upgrade options (wasm memory persistence, skip pre-upgrade) live inside
// the `upgrade` variant of `model.mode`.
const upgradeOptions = computed(() => {
const mode = model.value.mode;
return mode && variantIs(mode, 'upgrade') ? mode.upgrade[0] : undefined;
});

const setUpgradeOptions = (
wasmMemoryPersistence: [] | [WasmMemoryPersistence],
skipPreUpgrade: [] | [boolean],
): void => {
// Collapse to `{ upgrade: [] }` when no option is set so the request matches
// one created without any upgrade options.
const hasOptions = wasmMemoryPersistence.length > 0 || skipPreUpgrade.length > 0;
model.value = {
...model.value,
mode: {
upgrade: hasOptions
? [{ wasm_memory_persistence: wasmMemoryPersistence, skip_pre_upgrade: skipPreUpgrade }]
: [],
},
};
};

const wasmMemoryPersistence = computed<WasmMemoryPersistence | undefined>({
get: () => upgradeOptions.value?.wasm_memory_persistence[0],
set: value =>
setUpgradeOptions(value ? [value] : [], upgradeOptions.value?.skip_pre_upgrade ?? []),
});

const skipPreUpgrade = computed<boolean>({
get: () => upgradeOptions.value?.skip_pre_upgrade[0] ?? false,
set: value =>
setUpgradeOptions(upgradeOptions.value?.wasm_memory_persistence ?? [], value ? [true] : []),
});

const triggerSubmit = computed({
get: () => props.triggerSubmit,
set: value => emit('update:triggerSubmit', value),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useI18n } from 'vue-i18n';
import { VSelect } from 'vuetify/components';
import { CanisterInstallMode } from '~/generated/station/station.did';
import { requiredRule } from '~/utils/form.utils';
import { variantIs } from '~/utils/helper.utils';

const props = withDefaults(
defineProps<{
Expand All @@ -43,7 +44,13 @@ const emit = defineEmits<{
}>();

const model = computed({
get: () => props.modelValue,
// The `upgrade` variant can carry options (e.g. wasm memory persistence),
// but those are edited elsewhere in the form. Strip them here so the value
// still deep-equals the `{ upgrade: [] }` select item and stays selected.
get: () =>
props.modelValue && variantIs(props.modelValue, 'upgrade')
? ({ upgrade: [] } as CanisterInstallMode)
: props.modelValue,
set: value => emit('update:modelValue', value),
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<template>
<VSelect
v-model="model"
:label="label"
:variant="props.variant"
:density="props.density"
:readonly="props.readonly"
:items="items"
:prepend-icon="mdiMemory"
:hint="hint"
persistent-hint
clearable
/>
</template>
<script setup lang="ts">
import { mdiMemory } from '@mdi/js';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { VSelect } from 'vuetify/components';
import { WasmMemoryPersistence } from '~/types/station.types';

const props = withDefaults(
defineProps<{
modelValue?: WasmMemoryPersistence;
readonly?: boolean;
label?: string;
density?: 'comfortable' | 'compact' | 'default';
variant?: 'filled' | 'outlined' | 'plain' | 'solo' | 'underlined';
}>(),
{
modelValue: undefined,
readonly: false,
label: undefined,
density: 'comfortable',
variant: 'filled',
},
);

const emit = defineEmits<{
(event: 'update:modelValue', payload?: WasmMemoryPersistence): void;
}>();

const model = computed({
get: () => props.modelValue,
// Vuetify's `clearable` emits `null`; normalize it back to `undefined`.
set: value => emit('update:modelValue', value ?? undefined),
});

const i18n = useI18n();
const label = computed(
() => props.label ?? i18n.t('external_canisters.wasm_memory_persistence.label'),
);
const hint = computed(() => i18n.t('external_canisters.wasm_memory_persistence.hint'));

const items = computed<
{
title: string;
value: WasmMemoryPersistence;
}[]
>(() => [
{ title: i18n.t('external_canisters.wasm_memory_persistence.keep'), value: { keep: null } },
{ title: i18n.t('external_canisters.wasm_memory_persistence.replace'), value: { replace: null } },
]);
</script>
7 changes: 7 additions & 0 deletions apps/wallet/src/locales/en.locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,13 @@ export default {
upgrade: 'Upgrade',
install: 'Install',
},
wasm_memory_persistence: {
label: 'Wasm memory persistence',
keep: 'Keep',
replace: 'Replace',
hint: 'Use "Keep" to preserve the canister main memory on upgrade, as required by Motoko canisters that use Enhanced Orthogonal Persistence.',
},
skip_pre_upgrade: 'Skip pre-upgrade hook',
},
terms: {
license: 'License',
Expand Down
7 changes: 7 additions & 0 deletions apps/wallet/src/locales/fr.locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,13 @@ export default {
upgrade: 'Mettre à jour',
install: 'Installer',
},
wasm_memory_persistence: {
label: 'Persistance de la mémoire Wasm',
keep: 'Conserver',
replace: 'Remplacer',
hint: 'Utilisez « Conserver » pour préserver la mémoire principale du canister lors de la mise à jour, comme requis par les canisters Motoko utilisant la persistance orthogonale améliorée.',
},
skip_pre_upgrade: 'Ignorer le hook pre-upgrade',
},
terms: {
license: 'Licence',
Expand Down
7 changes: 7 additions & 0 deletions apps/wallet/src/locales/pt.locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,13 @@ export default {
upgrade: 'Atualizar',
install: 'Instalar',
},
wasm_memory_persistence: {
label: 'Persistência da memória Wasm',
keep: 'Manter',
replace: 'Substituir',
hint: 'Use "Manter" para preservar a memória principal do canister na atualização, conforme exigido por canisters Motoko que usam Persistência Ortogonal Aprimorada.',
},
skip_pre_upgrade: 'Ignorar o hook pre-upgrade',
},
terms: {
license: 'Licença',
Expand Down
8 changes: 8 additions & 0 deletions apps/wallet/src/types/station.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import {
ListExternalCanistersSortInput,
} from '~/generated/station/station.did';

/**
* The wasm memory persistence value nested inside the `upgrade` variant of
* `CanisterInstallMode`. Candid inlines this variant, so the generated
* bindings expose no named type for it; this mirrors the candid definition and
* is validated wherever it is assigned into a `CanisterInstallMode`.
*/
export type WasmMemoryPersistence = { keep: null } | { replace: null };

export enum AccountTransferStatus {
Created = 'created',
Failed = 'failed',
Expand Down
Loading
Loading