Migrate to Vue 3 and Vite

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2022-07-12 10:55:28 +02:00
parent 8f4099ee33
commit ee20e03cc2
464 changed files with 31515 additions and 32758 deletions

View File

@@ -1,34 +1,31 @@
<template>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t("Pick an identity") }}</p>
<div class="p-6">
<header class="">
<h2 class="">{{ $t("Pick an identity") }}</h2>
</header>
<section class="modal-card-body">
<section class="">
<div class="list is-hoverable list-none">
<a
class="list-item"
class="my-2 block dark:bg-violet-3 rounded-xl p-2"
v-for="identity in identities"
:key="identity.id"
:class="{
'is-active': currentIdentity && identity.id === currentIdentity.id,
active: currentIdentity && identity.id === currentIdentity.id,
}"
@click="currentIdentity = identity"
>
<div class="media">
<div class="flex gap-2">
<img
class="media-left image is-48x48"
class="rounded"
v-if="identity.avatar"
:src="identity.avatar.url"
alt=""
width="48"
height="48"
/>
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>@{{ identity.preferredUsername }}</h3>
<AccountCircle v-else :size="48" />
<div class="">
<p>@{{ identity.preferredUsername }}</p>
<small>{{ identity.name }}</small>
</div>
</div>
@@ -38,34 +35,34 @@
<slot name="footer" />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IActor } from "@/types/actor";
import { IDENTITIES } from "@/graphql/actor";
<script lang="ts" setup>
import { IPerson } from "@/types/actor";
import { useCurrentUserIdentities } from "@/composition/apollo/actor";
import { computed } from "vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
@Component({
apollo: {
identities: {
query: IDENTITIES,
},
const { identities } = useCurrentUserIdentities();
const { t } = useI18n({ useScope: "global" });
useHead({
title: computed(() => t("Identities")),
});
const props = defineProps<{
modelValue: IPerson;
}>();
const emit = defineEmits(["update:modelValue"]);
const currentIdentity = computed<IPerson>({
get(): IPerson {
return props.modelValue;
},
metaInfo() {
return {
title: this.$t("Identities") as string,
};
set(identity: IPerson) {
emit("update:modelValue", identity);
},
})
export default class IdentityPicker extends Vue {
@Prop() value!: IActor;
identities: IActor[] = [];
get currentIdentity(): IActor {
return this.value;
}
set currentIdentity(identity: IActor) {
this.$emit("input", identity);
}
}
});
</script>

View File

@@ -1,122 +1,108 @@
<template>
<div class="identity-picker">
<div>
<div
v-if="inline && currentIdentity"
class="inline box"
:class="{
'has-background-grey-lighter': masked,
'no-other-identity': !hasOtherIdentities,
}"
class="inline box cursor-pointer"
@click="activateModal"
>
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="currentIdentity.avatar">
<div class="flex gap-1">
<div class="">
<figure class="" v-if="currentIdentity.avatar">
<img
class="image is-rounded"
class="rounded-full"
:src="currentIdentity.avatar.url"
alt=""
width="48"
height="48"
/>
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
<AccountCircle v-else :size="48" />
</div>
<div class="media-content" v-if="currentIdentity.name">
<p class="is-4">{{ currentIdentity.name }}</p>
<p class="is-6 has-text-grey">
<div class="" v-if="currentIdentity.name">
<p class="">{{ currentIdentity.name }}</p>
<p class="">
{{ `@${currentIdentity.preferredUsername}` }}
<span v-if="masked">{{ $t("(Masked)") }}</span>
</p>
</div>
<div class="media-content" v-else>
<div class="" v-else>
{{ `@${currentIdentity.preferredUsername}` }}
</div>
<b-button
type="is-text"
v-if="identities.length > 1"
<o-button
variant="text"
v-if="identities && identities.length > 1"
@click="activateModal"
>
{{ $t("Change") }}
</b-button>
</o-button>
</div>
</div>
<span v-else-if="currentIdentity" class="block" @click="activateModal">
<figure class="image is-48x48" v-if="currentIdentity.avatar">
<img class="is-rounded" :src="currentIdentity.avatar.url" alt="" />
<span
v-else-if="currentIdentity"
class="cursor-pointer"
@click="activateModal"
>
<figure class="" v-if="currentIdentity.avatar">
<img
class="rounded"
:src="currentIdentity.avatar.url"
alt=""
width="48"
height="48"
/>
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
<AccountCircle v-else :size="48" />
</span>
<b-modal
v-model="isComponentModalActive"
has-modal-card
<o-modal
v-model:active="isComponentModalActive"
:close-button-aria-label="$t('Close')"
>
<identity-picker v-model="currentIdentity" />
</b-modal>
<identity-picker v-if="currentIdentity" v-model="currentIdentity" />
</o-modal>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IDENTITIES } from "@/graphql/actor";
import { IActor } from "../../types/actor";
<script lang="ts" setup>
import { useCurrentUserIdentities } from "@/composition/apollo/actor";
import { computed, ref } from "vue";
import { IPerson } from "../../types/actor";
import IdentityPicker from "./IdentityPicker.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
@Component({
components: { IdentityPicker },
apollo: {
identities: {
query: IDENTITIES,
},
const { identities } = useCurrentUserIdentities();
const props = withDefaults(
defineProps<{
modelValue: IPerson;
inline?: boolean;
masked?: boolean;
}>(),
{
inline: true,
masked: false,
}
);
const emit = defineEmits(["update:modelValue"]);
const isComponentModalActive = ref(false);
const currentIdentity = computed({
get(): IPerson | undefined {
return props.modelValue;
},
})
export default class IdentityPickerWrapper extends Vue {
@Prop() value!: IActor;
set(identity: IPerson | undefined) {
emit("update:modelValue", identity);
isComponentModalActive.value = false;
},
});
@Prop({ default: true, type: Boolean }) inline!: boolean;
const hasOtherIdentities = computed((): boolean => {
return identities.value !== undefined && identities.value.length > 1;
});
@Prop({ type: Boolean, required: false, default: false }) masked!: boolean;
isComponentModalActive = false;
identities: IActor[] = [];
@Watch("value")
updateCurrentActor(value: IActor): void {
this.currentIdentity = value;
const activateModal = (): void => {
if (hasOtherIdentities.value) {
isComponentModalActive.value = true;
}
get currentIdentity(): IActor | undefined {
return this.value;
}
set currentIdentity(identity: IActor | undefined) {
this.$emit("input", identity);
this.isComponentModalActive = false;
}
get hasOtherIdentities(): boolean {
return this.identities.length > 1;
}
activateModal(): void {
if (this.hasOtherIdentities) {
this.isComponentModalActive = true;
}
}
}
};
</script>
<style lang="scss">
.identity-picker {
.block {
cursor: pointer;
}
.inline:not(.no-other-identity) {
cursor: pointer;
}
.media {
border-top: none;
padding-top: 0;
}
}
</style>

View File

@@ -1,43 +1,43 @@
<template>
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title" v-if="userAlreadyActivated">
<section class="container mx-auto">
<div class="">
<div class="">
<h1 class="text-2xl" v-if="userAlreadyActivated">
{{ $t("Congratulations, your account is now created!") }}
</h1>
<h1 class="title" v-else>
<h1 class="text-2xl" v-else>
{{
$t("Register an account on {instanceName}!", {
instanceName: config.name,
instanceName,
})
}}
</h1>
<p class="content" v-if="userAlreadyActivated">
<p class="prose dark:prose-invert" v-if="userAlreadyActivated">
{{ $t("Now, create your first profile:") }}
</p>
<form v-if="!validationSent" @submit.prevent="submit">
<b-field :label="$t('Displayed nickname')">
<b-input
<o-field :label="$t('Displayed nickname')">
<o-input
aria-required="true"
required
v-model="identity.name"
@input="autoUpdateUsername($event)"
@input="autoUpdateUsername"
/>
</b-field>
</o-field>
<b-field
<o-field
:label="$t('Username')"
:type="errors.preferred_username ? 'is-danger' : null"
:message="errors.preferred_username"
>
<b-field
<o-field
:message="
$t(
'Only alphanumeric lowercased characters and underscores are supported.'
)
"
>
<b-input
<o-input
aria-required="true"
required
expanded
@@ -53,8 +53,8 @@
<p class="control">
<span class="button is-static">@{{ host }}</span>
</p>
</b-field>
</b-field>
</o-field>
</o-field>
<p class="description">
{{
$t(
@@ -63,16 +63,16 @@
}}
</p>
<b-field :label="$t('Short bio')">
<b-input
<o-field :label="$t('Short bio')">
<o-input
type="textarea"
maxlength="100"
rows="2"
v-model="identity.summary"
/>
</b-field>
</o-field>
<p class="content">
<p class="prose dark:prose-invert">
{{
$t(
"You will be able to add an avatar and set other options in your account settings."
@@ -81,28 +81,30 @@
</p>
<p class="control has-text-centered">
<b-button
type="is-primary"
size="is-large"
<o-button
variant="primary"
size="large"
native-type="submit"
:disabled="sendingValidation"
>{{ $t("Create my profile") }}</b-button
>{{ $t("Create my profile") }}</o-button
>
</p>
</form>
<div v-if="validationSent && !userAlreadyActivated">
<b-message type="is-success" :closable="false">
<o-notification variant="success" :closable="false">
<h2 class="title">
{{
$t("Your account is nearly ready, {username}", {
username: identity.name || identity.preferredUsername,
username: identity.name ?? identity.preferredUsername,
})
}}
</h2>
<i18n path="A validation email was sent to {email}" tag="p">
<code slot="email">{{ email }}</code>
</i18n>
<i18n-t keypath="A validation email was sent to {email}" tag="p">
<template #email>
<code>{{ email }}</code>
</template>
</i18n-t>
<p>
{{
$t(
@@ -110,120 +112,99 @@
)
}}
</p>
</b-message>
</o-notification>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model";
import { IPerson } from "../../types/actor";
import { IDENTITIES, REGISTER_PERSON } from "../../graphql/actor";
<script lang="ts" setup>
import { Person } from "../../types/actor";
import { MOBILIZON_INSTANCE_HOST } from "../../api/_entrypoint";
import RouteName from "../../router/name";
import { changeIdentity } from "../../utils/auth";
import identityEditionMixin from "../../mixins/identityEdition";
import { ApolloCache, FetchResult } from "@apollo/client/core";
import { ActorType } from "@/types/enums";
import { changeIdentity } from "../../utils/identity";
import { useInstanceName } from "@/composition/apollo/config";
import { ref, computed, onBeforeMount } from "vue";
import { useRouter } from "vue-router";
import { registerAccount } from "@/composition/apollo/user";
import { convertToUsername } from "@/utils/username";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
@Component({
apollo: {
config: CONFIG,
},
metaInfo() {
return {
title: this.$t("Register") as string,
};
},
})
export default class Register extends mixins(identityEditionMixin) {
@Prop({ type: String, required: true }) email!: string;
@Prop({ type: Boolean, required: false, default: false })
userAlreadyActivated!: boolean;
config!: IConfig;
host?: string = MOBILIZON_INSTANCE_HOST;
errors: Record<string, unknown> = {};
validationSent = false;
sendingValidation = false;
async created(): Promise<void> {
// Make sure no one goes to this page if we don't want to
if (!this.email) {
await this.$router.replace({ name: RouteName.PAGE_NOT_FOUND });
}
const props = withDefaults(
defineProps<{
email: string;
userAlreadyActivated?: boolean;
}>(),
{
userAlreadyActivated: false,
}
);
async submit(): Promise<void> {
try {
this.sendingValidation = true;
this.errors = {};
const { data } = await this.$apollo.mutate<{ registerPerson: IPerson }>({
mutation: REGISTER_PERSON,
variables: { email: this.email, ...this.identity },
update: (
store: ApolloCache<{ registerPerson: IPerson }>,
{ data: localData }: FetchResult
) => {
if (this.userAlreadyActivated) {
const identitiesData = store.readQuery<{ identities: IPerson[] }>({
query: IDENTITIES,
});
const { instanceName } = useInstanceName();
if (identitiesData && localData) {
const newPersonData = {
...localData.registerPerson,
type: ActorType.PERSON,
};
const router = useRouter();
store.writeQuery({
query: IDENTITIES,
data: {
...identitiesData,
identities: [...identitiesData.identities, newPersonData],
},
});
}
}
},
});
if (data) {
this.validationSent = true;
window.localStorage.setItem("new-registered-user", "yes");
const { t } = useI18n({ useScope: "global" });
if (this.userAlreadyActivated) {
await changeIdentity(
this.$apollo.provider.defaultClient,
data.registerPerson
);
useHead({
title: computed(() => t("Register")),
});
await this.$router.push({ name: RouteName.HOME });
}
}
} catch (errorCatched: any) {
this.errors = errorCatched.graphQLErrors.reduce(
(acc: { [key: string]: string }, error: any) => {
acc[error.details || error.field] = error.message;
return acc;
},
{}
);
console.error("Error while registering person", errorCatched);
console.error("Errors while registering person", this.errors);
this.sendingValidation = false;
}
const host: string = MOBILIZON_INSTANCE_HOST;
const errors = ref<Record<string, unknown>>({});
const validationSent = ref(false);
const sendingValidation = ref(false);
const identity = ref(new Person());
onBeforeMount(() => {
// Make sure no one goes to this page if we don't want to
if (!props.email) {
router.replace({ name: RouteName.PAGE_NOT_FOUND });
}
}
});
const autoUpdateUsername = () => {
identity.value.preferredUsername = convertToUsername(identity.value.name);
};
const submit = async (): Promise<void> => {
sendingValidation.value = true;
errors.value = {};
const { onDone, onError } = registerAccount(
{ email: props.email, ...identity.value },
props.userAlreadyActivated
);
onDone(async ({ data }) => {
validationSent.value = true;
window.localStorage.setItem("new-registered-user", "yes");
if (data && props.userAlreadyActivated) {
await changeIdentity(data.registerPerson);
await router.push({ name: RouteName.HOME });
}
});
onError((err) => {
errors.value = err.graphQLErrors.reduce(
(acc: { [key: string]: string }, error: any) => {
acc[error.details || error.field] = error.message;
return acc;
},
{}
);
console.error("Error while registering person", err);
console.error("Errors while registering person", errors);
sendingValidation.value = false;
});
};
</script>
<style lang="scss" scoped>

File diff suppressed because it is too large Load Diff