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
51 changes: 51 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/craftcms-cp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@storybook/addon-vitest": "^10.3.6",
"@storybook/web-components-vite": "^10.3.6",
"@total-typescript/tsconfig": "^1.0.4",
"@types/humanize-duration": "^3.27.4",
"@types/jquery": "^4.0.0",
"@types/node": "^25.6.0",
"@vitest/browser": "^4.1.5",
Expand All @@ -83,6 +84,7 @@
"esbuild": "^0.28.0",
"globby": "^16.2.0",
"happy-dom": "^20.9.0",
"humanize-duration": "^3.33.2",
"lit": "^3.3.2",
"ora": "^9.4.0",
"playwright": "^1.59.1",
Expand All @@ -99,6 +101,7 @@
},
"dependencies": {
"@chromatic-com/storybook": "^5.1.2",
"@formatjs/intl-durationformat": "^0.10.8",
"@github/relative-time-element": "^5.0.0",
"axios": "^1.15.2"
},
Expand Down
16 changes: 14 additions & 2 deletions packages/craftcms-cp/src/utilities/format.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import humanizeDuration from 'humanize-duration';

// @TODO grab this from the config
const defaultCpLocale = 'en';

export function formatNumber(number: number | string, format?: string) {
// If d3 is available, use it for compatibility
if (
Expand Down Expand Up @@ -27,17 +32,24 @@ export function formatNumber(number: number | string, format?: string) {
? parseInt(decimalMatch[1]!, 10)
: 0;

return new Intl.NumberFormat('en-US', {
return new Intl.NumberFormat(defaultCpLocale, {
useGrouping: hasThousandSeparator,
minimumFractionDigits: maximumFractionDigits,
maximumFractionDigits: maximumFractionDigits,
}).format(num);
}

// Default: format with thousand separators, no decimal places
return new Intl.NumberFormat('en-US', {
return new Intl.NumberFormat(defaultCpLocale, {
useGrouping: true,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(num);
}

export const cpHumanizer = humanizeDuration.humanizer({
language: defaultCpLocale,
fallbacks: ['en'],
largest: 2,
round: true,
});
5 changes: 5 additions & 0 deletions resources/js/bootstrap/cp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import AssetIndexes from '@/components/utilities/AssetIndexes/AssetIndexes.vue';
import SystemMessages from '@/components/utilities/SystemMessages/SystemMessages.vue';
import DeprecationErrorsToolbar from '@/components/utilities/DeprecationErrors/DeprecationErrorsToolbar.vue';
import {setTranslations} from '@craftcms/cp/utilities/translate.ts.mjs';
import TotpForm from '@/components/Auth/TotpForm.vue';
import RecoveryCodesForm from '@/components/Auth/RecoveryCodesForm.vue';

let bootedCallbacks: Array<(instance: any) => void> = [];
let bootingCallbacks: Array<(instance: any) => void> = [];
Expand Down Expand Up @@ -97,6 +99,9 @@ const Cp = {
app.component('ProjectConfig', ProjectConfig);
app.component('AssetIndexes', AssetIndexes);
app.component('SystemMessages', SystemMessages);

app.component('TotpForm', TotpForm);
app.component('RecoveryCodesForm', RecoveryCodesForm);
},
});

Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/ActionMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {t} from '@craftcms/cp/utilities/translate.ts.mjs';
import type {VariantKey} from '@craftcms/cp/types/index.ts';
import {type Component, computed, type VNode} from 'vue';
import VarDump from '@/components/VarDump.vue';

interface ActionItemHr {
type: 'hr';
Expand Down Expand Up @@ -73,7 +74,7 @@

<template>
<craft-action-menu>
<slot name="invoker" :label="label">
<slot name="invoker" :label="label" :attributes="{slot: 'invoker'}">
<craft-button
type="button"
slot="invoker"
Expand Down
146 changes: 146 additions & 0 deletions resources/js/components/Auth/LoginForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<script setup lang="ts">
import {t} from '@craftcms/cp';
import LoginController from '@actions/Auth/LoginController';
import {Form, useForm, usePage} from '@inertiajs/vue3';
import CraftInput from '@craftcms/cp/vue/CraftInput.vue';
import CraftInputPassword from '@craftcms/cp/vue/CraftInputPassword.vue';
import CraftCheckbox from '@craftcms/cp/vue/CraftCheckbox.vue';
import {computed} from 'vue';
import useCraftData from '@/composables/useCraftData';
import {cpHumanizer} from '@craftcms/cp/utilities/format.ts.mjs';

const props = withDefaults(
defineProps<{
showPasswordReset?: boolean;
showRememberCheckbox?: boolean;
staticEmail?: string | null;
username?: string;
}>(),
{
showPasswordReset: true,
showRememberCheckbox: true,
staticEmail: null,
username: '',
}
);

const page = usePage<{
errors?: {
loginName?: string;
password?: string;
rememberMe?: string;
} | null;
flash: {
error: string;
};
}>();
const {general} = useCraftData();
const fieldErrors = computed(() => page.props.errors);
const formError = computed(() => page.props.flash.error);

const usernameProps = computed(() => {
if (general.useEmailAsUsername) {
return {
label: t('Email'),
type: 'email',
};
}

return {
label: t('Username or Email'),
type: 'text',
};
});

const initialUsername = computed(() => {
return props.staticEmail ?? props.username ?? '';
});

const form = useForm({
loginName: initialUsername.value,
password: '',
rememberMe: false,
});

function handleSubmit() {
form.clearErrors().submit(LoginController.attemptLogin(), {
onSuccess: () => {
form.reset('password');
},
});
}

const humanizedDuration = computed(() =>
cpHumanizer(general.rememberedUserSessionDuration * 1000)
);
</script>

<template>
<form @submit.prevent="handleSubmit()">
<div class="grid gap-3">
<template v-if="formError">
<div class="mt-2">
<craft-callout variant="danger" appearance="fill">{{
formError
}}</craft-callout>
</div>
</template>
<template v-if="staticEmail">
<input type="hidden" name="username" :value="staticEmail" />
</template>
<template v-else>
<CraftInput
:label="usernameProps.label"
:type="usernameProps.type"
name="username"
autocomplete="username"
:autocapitalize="false"
:required="true"
v-model="form.loginName"
:error="fieldErrors?.loginName"
/>
</template>
<div>
<CraftInputPassword
name="password"
label="Password"
v-model="form.password"
:required="true"
autocomplete="current-password"
:error="fieldErrors?.password"
/>
<template v-if="showPasswordReset">
<div class="mt-2">
<a href="">{{ t('Forgot password?') }}</a>
</div>
</template>
</div>

<template
v-if="showRememberCheckbox && general.rememberedUserSessionDuration"
>
<CraftCheckbox
:label="
t('Stay signed in for {duration}', {
duration: humanizedDuration,
})
"
v-model="form.rememberMe"
name="rememberMe"
/>
</template>
</div>

<div class="mt-4">
<craft-button
type="submit"
variant="primary"
:loading="form.processing"
class="w-full"
>{{ t('Sign in') }}</craft-button
>
</div>
</form>
</template>

<style scoped lang="scss"></style>
36 changes: 36 additions & 0 deletions resources/js/components/Auth/RecoveryCodesForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import {t} from '@craftcms/cp';
import {Form} from '@inertiajs/vue3';
import CraftInput from '@craftcms/cp/vue/CraftInput.vue';

defineProps<{
action: string;
returnUrl?: string;
}>();
</script>

<template>
<Form :action="action" #default="{processing, errors}" method="post">
<div class="flex gap-2 items-end">
<input
type="hidden"
name="redirect"
:value="returnUrl"
v-if="returnUrl"
/>
<CraftInput
:label="t('Recovery Code')"
id="recovery-code"
name="code"
class="w-full"
:error="errors.code"
/>

<craft-button type="submit" :loading="processing" variant="primary">
{{ t('Verify') }}
</craft-button>
</div>
</Form>
</template>

<style scoped lang="scss"></style>
Loading
Loading