@karibsen/eponyme 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -39
- package/dist/eponyme.mjs +2 -3
- package/dist/module.d.mts +94 -13
- package/dist/module.json +1 -1
- package/dist/module.mjs +23 -6
- package/dist/runtime/assets/icon.ico +0 -0
- package/dist/runtime/assets/logo.png +0 -0
- package/dist/runtime/assets.d.ts +9 -0
- package/dist/runtime/components/editor/EponymeFolderNav.vue +1 -8
- package/dist/runtime/components/editor/EponymeFormPage.vue +2 -2
- package/dist/runtime/components/editor/EponymePreviewPanel.vue +4 -3
- package/dist/runtime/components/editor/EponymeSidebar.vue +7 -2
- package/dist/runtime/components/fields/FieldRenderer.vue +11 -0
- package/dist/runtime/components/fields/PhoneField.d.vue.ts +22 -0
- package/dist/runtime/components/fields/PhoneField.vue +61 -0
- package/dist/runtime/components/fields/PhoneField.vue.d.ts +22 -0
- package/dist/runtime/components/ui/EPLinkEditor.vue +2 -1
- package/dist/runtime/composables/useEponyme.d.ts +22 -14
- package/dist/runtime/composables/useEponyme.js +5 -3
- package/dist/runtime/composables/useEponymeCollection.js +6 -4
- package/dist/runtime/composables/useEponymeFavicon.d.ts +1 -0
- package/dist/runtime/composables/useEponymeFavicon.js +14 -0
- package/dist/runtime/composables/useEponymeForm.d.ts +0 -4
- package/dist/runtime/composables/useEponymeForm.js +1 -8
- package/dist/runtime/fields/index.d.ts +2 -0
- package/dist/runtime/fields/index.js +2 -1
- package/dist/runtime/fields/phone.d.ts +2 -0
- package/dist/runtime/fields/phone.js +3 -0
- package/dist/runtime/middleware/eponyme-auth.d.ts +1 -1
- package/dist/runtime/middleware/eponyme-owner.d.ts +1 -1
- package/dist/runtime/pages/EponymeChangePasswordPage.vue +2 -0
- package/dist/runtime/pages/EponymeDetailPage.vue +2 -0
- package/dist/runtime/pages/EponymeIndexPage.vue +2 -0
- package/dist/runtime/pages/EponymeLoginPage.vue +9 -4
- package/dist/runtime/pages/EponymeUsersPage.vue +2 -0
- package/dist/runtime/server/api/eponyme-forms/[path].post.js +0 -15
- package/dist/runtime/server/middleware/eponyme-no-store.js +4 -1
- package/dist/runtime/server/services/eponyme-form-store.d.ts +1 -0
- package/dist/runtime/server/services/eponyme-form-store.js +4 -3
- package/dist/runtime/server/services/eponyme-store.d.ts +11 -0
- package/dist/runtime/server/services/eponyme-store.js +36 -6
- package/dist/runtime/server/utils/eponyme-cache.d.ts +2 -14
- package/dist/runtime/server/utils/eponyme-cache.js +1 -6
- package/dist/runtime/types/eponyme-config.d.ts +0 -7
- package/dist/runtime/types/eponyme.d.ts +2 -5
- package/dist/runtime/types/field.d.ts +49 -4
- package/dist/runtime/types/index.d.ts +0 -1
- package/dist/runtime/types/index.js +0 -1
- package/dist/runtime/utils/cache-tags.d.ts +46 -0
- package/dist/runtime/utils/cache-tags.js +28 -0
- package/dist/runtime/utils/hydration-cache.d.ts +10 -0
- package/dist/runtime/utils/hydration-cache.js +3 -0
- package/dist/runtime/utils/middle-ellipsis.d.ts +1 -0
- package/dist/runtime/utils/middle-ellipsis.js +8 -0
- package/dist/runtime/utils/normalize-eponyme-phones.d.ts +9 -0
- package/dist/runtime/utils/normalize-eponyme-phones.js +38 -0
- package/dist/runtime/utils/normalize-phone.d.ts +26 -0
- package/dist/runtime/utils/normalize-phone.js +17 -0
- package/dist/runtime/utils/preview.d.ts +10 -0
- package/dist/runtime/utils/preview.js +7 -2
- package/dist/runtime/utils/validate-eponyme-data.js +29 -3
- package/package.json +3 -2
- package/dist/runtime/server/utils/eponyme-captcha.d.ts +0 -6
- package/dist/runtime/server/utils/eponyme-captcha.js +0 -14
- package/dist/runtime/types/captcha.d.ts +0 -16
- package/dist/runtime/types/captcha.js +0 -0
- package/dist/runtime/utils/no-captcha.d.ts +0 -7
- package/dist/runtime/utils/no-captcha.js +0 -5
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# Eponyme
|
|
2
4
|
|
|
3
5
|
[![npm version][npm-version-src]][npm-version-href]
|
|
@@ -11,7 +13,7 @@ Define your content in `eponyme.config.ts`. Eponyme provides defaults, validatio
|
|
|
11
13
|
|
|
12
14
|
## Features
|
|
13
15
|
|
|
14
|
-
- Type-safe fields for text, slugs, rich text, numbers, booleans, images, links, dates, colors, sections, tabs, and arrays
|
|
16
|
+
- Type-safe fields for text, slugs, rich text, numbers, booleans, images, links, phone numbers, dates, colors, sections, tabs, and arrays
|
|
15
17
|
- Declarative defaults and validation
|
|
16
18
|
- Conditional fields, character counters, and sortable arrays
|
|
17
19
|
- Private drafts with explicit publishing
|
|
@@ -42,6 +44,13 @@ Install the module:
|
|
|
42
44
|
pnpm add @karibsen/eponyme
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
Eponyme brings its own components, so `@nuxt/ui` is not required. If your application does use
|
|
48
|
+
it, it has to be `^4.10.0` or newer: both ship Tailwind, and an older `@nuxt/ui` resolves a
|
|
49
|
+
version the dashboard was not built against. What that breaks is not predictable — broken
|
|
50
|
+
dashboard styles are the visible case, but it can also fail the build or misbehave elsewhere with
|
|
51
|
+
no obvious link back to the cause. The module checks the installed version and warns at build
|
|
52
|
+
time.
|
|
53
|
+
|
|
45
54
|
Add it to `nuxt.config.ts`:
|
|
46
55
|
|
|
47
56
|
```ts
|
|
@@ -85,8 +94,16 @@ export default defineEponymeConfig({
|
|
|
85
94
|
label: 'Contact email',
|
|
86
95
|
defaultValue: 'hello@example.com',
|
|
87
96
|
}),
|
|
97
|
+
contactPhone: field.phone({
|
|
98
|
+
label: 'Contact phone',
|
|
99
|
+
defaultCountry: 'FR',
|
|
100
|
+
countries: ['FR', 'BE'],
|
|
101
|
+
autocomplete: 'tel',
|
|
102
|
+
}),
|
|
88
103
|
callToAction: field.url({
|
|
89
104
|
label: 'Call to action',
|
|
105
|
+
// Schemes an external link may use; defaults to ['http', 'https']
|
|
106
|
+
protocols: ['https', 'mailto'],
|
|
90
107
|
defaultValue: {
|
|
91
108
|
href: '/contact',
|
|
92
109
|
type: 'internal',
|
|
@@ -152,6 +169,39 @@ field.string({
|
|
|
152
169
|
})
|
|
153
170
|
```
|
|
154
171
|
|
|
172
|
+
### Phone numbers
|
|
173
|
+
|
|
174
|
+
`field.phone()` stores its value in **E.164** — `+33611131143` — whatever format it was typed
|
|
175
|
+
in. Normalisation happens on the server before the write, so the stored format is a guarantee
|
|
176
|
+
rather than a convention a client could skip.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
phone: field.phone({
|
|
180
|
+
label: 'Phone',
|
|
181
|
+
// ISO 3166-1 alpha-2, autocompleted from libphonenumber-js
|
|
182
|
+
countries: ['FR', 'BE'],
|
|
183
|
+
// Country a number typed without `+` belongs to
|
|
184
|
+
defaultCountry: 'FR',
|
|
185
|
+
autocomplete: 'tel',
|
|
186
|
+
placeholder: '06 11 13 11 43',
|
|
187
|
+
required: true,
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
| Option | Effect |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `countries` | Countries whose numbers are accepted. A number resolving to any other is refused, with an error naming the country it came from. Omitted, every country is accepted |
|
|
194
|
+
| `defaultCountry` | Country assumed for a number written without an international prefix. Without it, only `+…` numbers can be understood |
|
|
195
|
+
| `detectCountry` | `false` requires the international `+…` form, so the country is never guessed. Defaults to `true` |
|
|
196
|
+
| `autocomplete` | `tel`, `tel-national` or `tel-country-code`, set on the input |
|
|
197
|
+
| `defaultValue`, `required`, `placeholder`, `label`, `description` | As on every field |
|
|
198
|
+
|
|
199
|
+
Validation uses the `min` metadata bundle of `libphonenumber-js`, a fraction of the size of the
|
|
200
|
+
full one. It parses and formats every country, and is slightly more permissive: a few numbers
|
|
201
|
+
that are impossible for their region still pass.
|
|
202
|
+
|
|
203
|
+
A phone field is also allowed in a public `form()`.
|
|
204
|
+
|
|
155
205
|
## Prisma
|
|
156
206
|
|
|
157
207
|
Eponyme uses the Prisma client owned by your application. It does not create the connection or run migrations.
|
|
@@ -740,44 +790,6 @@ Two protections apply to the public submission route:
|
|
|
740
790
|
- a **body size limit**, 64 KB by default, configurable with `maxBodyBytes`. An oversized
|
|
741
791
|
body is refused with `413` before it is parsed.
|
|
742
792
|
|
|
743
|
-
### Captcha
|
|
744
|
-
|
|
745
|
-
Set `captcha: true` on the form, then install an adapter — Eponyme defines the contract
|
|
746
|
-
but ships no provider:
|
|
747
|
-
|
|
748
|
-
```bash
|
|
749
|
-
pnpm add @karibsen/eponyme-captcha
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
```ts
|
|
753
|
-
export default defineNuxtConfig({
|
|
754
|
-
modules: ['@karibsen/eponyme', '@karibsen/eponyme-captcha'],
|
|
755
|
-
eponymeCaptcha: { siteKey: '<your-turnstile-site-key>' },
|
|
756
|
-
})
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
Render the widget and bind its token; `useEponymeForm` sends it alongside the fields and
|
|
760
|
-
the server verifies it before validating anything:
|
|
761
|
-
|
|
762
|
-
```vue
|
|
763
|
-
<script setup lang="ts">
|
|
764
|
-
const { captchaToken, requiresCaptcha, submit } = useEponymeForm('contact')
|
|
765
|
-
</script>
|
|
766
|
-
|
|
767
|
-
<template>
|
|
768
|
-
<NuxtTurnstile v-if="requiresCaptcha" v-model="captchaToken" />
|
|
769
|
-
</template>
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
**It fails closed.** Without an adapter, a form declaring `captcha: true` answers `500`
|
|
773
|
-
rather than accepting submissions unprotected. A rejected token answers `422` with a
|
|
774
|
-
`_form` error; the provider's reason stays in the server log, since telling a bot which
|
|
775
|
-
check it failed only helps it.
|
|
776
|
-
|
|
777
|
-
Writing an adapter for another provider means supplying a `name` and an
|
|
778
|
-
`async verify(token, { ip })` returning `{ success, reason? }`, then pointing the
|
|
779
|
-
`#eponyme/captcha` alias at it.
|
|
780
|
-
|
|
781
793
|
Rate limiting and file uploads are not implemented yet.
|
|
782
794
|
|
|
783
795
|
## Hooks
|
package/dist/eponyme.mjs
CHANGED
|
@@ -11,7 +11,7 @@ function collection(options) {
|
|
|
11
11
|
return { __eponymeCollection: true, ...options };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const PUBLIC_FIELD_TYPES = ["string", "textarea", "email", "url", "number", "boolean", "select", "radio", "checkboxGroup"];
|
|
14
|
+
const PUBLIC_FIELD_TYPES = ["string", "textarea", "email", "phone", "url", "number", "boolean", "select", "radio", "checkboxGroup"];
|
|
15
15
|
const DEFAULT_HONEYPOT = "_eponyme_hp";
|
|
16
16
|
const DEFAULT_MAX_BODY_BYTES = 64 * 1024;
|
|
17
17
|
function form(options) {
|
|
@@ -31,8 +31,7 @@ function form(options) {
|
|
|
31
31
|
// Storing a submission has to be an explicit decision, so `custom` is the default.
|
|
32
32
|
submission: { mode: options.submission?.mode ?? "custom" },
|
|
33
33
|
honeypot,
|
|
34
|
-
maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
35
|
-
captcha: options.captcha ?? false
|
|
34
|
+
maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
36
35
|
};
|
|
37
36
|
}
|
|
38
37
|
|
package/dist/module.d.mts
CHANGED
|
@@ -5,37 +5,118 @@ export { EPONYME_ROLES, canEditEponyme, isEponymeRole } from '../dist/runtime/ty
|
|
|
5
5
|
export { ArrayFieldDefinition, ArrayFieldOptions, ArrayItemDefinition, ArrayItemFieldDefinition, ArrayItemSchema, ArrayItemValue, BooleanFieldDefinition, BooleanFieldOptions, CheckboxGroupFieldDefinition, CheckboxGroupFieldOptions, CollectionSlugField, ColorFieldDefinition, ColorFieldOptions, DateFieldDefinition, DateFieldOptions, EmailFieldDefinition, EmailFieldOptions, EponymeAuthSession, EponymeAuthUser, EponymeCollectionDataByName, EponymeCollectionDefinition, EponymeCollectionDefinitionBase, EponymeCollectionName, EponymeCollectionOptions, EponymeConfig, EponymeData, EponymeDataByName, EponymeFormDataByName, EponymeFormDefinition, EponymeFormDefinitionBase, EponymeFormFieldDefinition, EponymeFormMode, EponymeFormName, EponymeFormOptions, EponymeFormSchema, EponymeManagedUserResult, EponymeName, EponymeRole, EponymeSchema, EponymeVariableDefinition, EponymeVariableSummary, EponymeVariableValue, EponymeVariables, FieldDefinition, FieldValidationResult, FieldValidator, FieldVisibilityCondition, ImageFieldDefinition, ImageFieldOptions, NumberFieldDefinition, NumberFieldOptions, RadioFieldDefinition, RadioFieldOptions, RichTextFieldDefinition, RichTextFieldOptions, SectionFieldDefinition, SectionFieldOptions, SectionItemFieldDefinition, SectionItemValue, SectionSchema, SectionValue, SelectFieldDefinition, SelectFieldOptions, SelectOption, SlugFieldDefinition, SlugFieldOptions, StringFieldDefinition, StringFieldOptions, TabDefinition, TabFieldDefinition, TabFieldOptions, TabsSchema, TabsValue, TextareaFieldDefinition, TextareaFieldOptions, TodayDateDefault, UrlFieldDefinition, UrlFieldOptions, UrlType, UrlValue } from '../dist/runtime/types/index.js';
|
|
6
6
|
import '../dist/runtime/types/variables.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Options read from the `eponyme` key of `nuxt.config`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* export default defineNuxtConfig({
|
|
14
|
+
* modules: ['@karibsen/eponyme'],
|
|
15
|
+
* eponyme: {
|
|
16
|
+
* prismaClient: '~~/server/utils/prisma',
|
|
17
|
+
* previewPaths: {
|
|
18
|
+
* homepage: '/',
|
|
19
|
+
* articles: '/articles/:slug',
|
|
20
|
+
* },
|
|
21
|
+
* },
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
8
25
|
interface ModuleOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Route the dashboard is served on. Surrounding slashes are normalised, so `__eponyme` and
|
|
28
|
+
* `/__eponyme/` are equivalent.
|
|
29
|
+
*
|
|
30
|
+
* @default "/__eponyme"
|
|
31
|
+
*/
|
|
9
32
|
dashboardPath?: string;
|
|
10
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Public route rendering each entry, keyed by singleton name or by collection name. A
|
|
35
|
+
* collection value must contain `:slug`, which is replaced by the entry slug.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* An entry left out of this map has no preview, no sitemap entry, and no cache tag on the page
|
|
39
|
+
* that renders it.
|
|
40
|
+
*
|
|
41
|
+
* @default {}
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* previewPaths: {
|
|
45
|
+
* homepage: '/',
|
|
46
|
+
* articles: '/articles/:slug',
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
11
50
|
previewPaths?: Record<string, string>;
|
|
12
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Module specifier of a server file whose default export is an initialised `PrismaClient`.
|
|
53
|
+
* Nuxt aliases apply, and keep their usual meaning: a server file lives under `~~/`, not `~/`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* prismaClient: '~~/server/utils/prisma'
|
|
58
|
+
* ```
|
|
59
|
+
* @see https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/introduction
|
|
60
|
+
*/
|
|
13
61
|
prismaClient: string;
|
|
14
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Palette offered by every color field, which a field overrides with its own `presets`.
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* A bare string is labelled from its value. An entry that is not a valid hex colour is dropped.
|
|
67
|
+
*
|
|
68
|
+
* @default []
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* colorPresets: [
|
|
72
|
+
* { label: 'Ink', value: '#171714' },
|
|
73
|
+
* '#ffffff',
|
|
74
|
+
* ]
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
15
77
|
colorPresets?: Array<string | {
|
|
16
78
|
label: string;
|
|
17
79
|
value: string;
|
|
18
80
|
}>;
|
|
19
81
|
auth?: {
|
|
20
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Lifetime of an authenticated session, in days, counted from sign-in and never extended by
|
|
84
|
+
* activity.
|
|
85
|
+
*
|
|
86
|
+
* @default 7
|
|
87
|
+
*/
|
|
21
88
|
sessionDurationDays?: number;
|
|
22
89
|
};
|
|
23
90
|
/**
|
|
24
|
-
* How long a server instance may reuse content it has already read, in seconds.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
91
|
+
* How long a server instance may reuse content it has already read, in seconds. `0` disables it.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* A save only clears the instance that served it, so this bounds how long another instance can
|
|
95
|
+
* still answer with the previous content.
|
|
96
|
+
*
|
|
97
|
+
* @default 5
|
|
27
98
|
*/
|
|
28
99
|
cacheSeconds?: number;
|
|
29
100
|
/**
|
|
30
|
-
* How long a browser may reuse published content, in seconds.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
101
|
+
* How long a browser may reuse published content, in seconds. `0` disables it.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* A browser cache cannot be purged: a visitor holding a copy keeps it until it expires, whatever
|
|
105
|
+
* is published meanwhile.
|
|
106
|
+
*
|
|
107
|
+
* @default 30
|
|
108
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
|
|
33
109
|
*/
|
|
34
110
|
browserCacheSeconds?: number;
|
|
35
111
|
/**
|
|
36
|
-
* How long a CDN may reuse published content, in seconds.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
112
|
+
* How long a CDN may reuse published content, in seconds. `0` disables it.
|
|
113
|
+
*
|
|
114
|
+
* @remarks
|
|
115
|
+
* Sent as `s-maxage`, with `stale-while-revalidate` twelve times longer. Cached responses carry
|
|
116
|
+
* cache tags, so a publication can purge them from the `eponyme:entry:published` hook.
|
|
117
|
+
*
|
|
118
|
+
* @default 300
|
|
119
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
|
|
39
120
|
*/
|
|
40
121
|
cdnCacheSeconds?: number;
|
|
41
122
|
}
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import { defineNuxtModule, useLogger, createResolver, findPath, addImports, addImportsDir, addServerImports, addRouteMiddleware, addTypeTemplate, addServerHandler, addServerPlugin } from '@nuxt/kit';
|
|
1
|
+
import { defineNuxtModule, useLogger, createResolver, findPath, addImports, addImportsDir, addServerImports, addRouteMiddleware, addTypeTemplate, addServerHandler, addServerPlugin, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion } from '@nuxt/kit';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
+
import { tagPreviewPathRoutes } from '../dist/runtime/utils/cache-tags.js';
|
|
5
6
|
|
|
7
|
+
const MINIMUM_NUXT_UI = "4.10.0";
|
|
8
|
+
async function warnOnIncompatibleNuxtUi(nuxt, logger) {
|
|
9
|
+
if (!hasNuxtModule("@nuxt/ui", nuxt)) return;
|
|
10
|
+
if (await hasNuxtModuleCompatibility("@nuxt/ui", `>=${MINIMUM_NUXT_UI}`, nuxt)) return;
|
|
11
|
+
const found = await getNuxtModuleVersion("@nuxt/ui", nuxt);
|
|
12
|
+
logger.warn(
|
|
13
|
+
`@nuxt/ui ${found || "(unknown version)"} is older than ${MINIMUM_NUXT_UI}, which Eponyme needs in order to share Tailwind with it.
|
|
14
|
+
Upgrade it with \`npm i @nuxt/ui@^${MINIMUM_NUXT_UI}\`. Keeping an older one resolves a Tailwind this module was not built
|
|
15
|
+
against, and what that breaks is not predictable: broken dashboard styles are the visible case, but it can also fail the
|
|
16
|
+
build or misbehave somewhere with no obvious link back to this warning.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
6
19
|
const module$1 = defineNuxtModule({
|
|
7
20
|
meta: {
|
|
8
21
|
name: "@karibsen/eponyme",
|
|
@@ -63,6 +76,7 @@ const module$1 = defineNuxtModule({
|
|
|
63
76
|
async setup(options, nuxt) {
|
|
64
77
|
const logger = useLogger("eponyme");
|
|
65
78
|
const resolver = createResolver(import.meta.url);
|
|
79
|
+
await warnOnIncompatibleNuxtUi(nuxt, logger);
|
|
66
80
|
const dashboardPath = `/${(options.dashboardPath || "__eponyme").replace(/^\/+|\/+$/g, "")}`;
|
|
67
81
|
const publicRuntimeConfig = nuxt.options.runtimeConfig.public;
|
|
68
82
|
publicRuntimeConfig.eponyme = {
|
|
@@ -78,6 +92,14 @@ const module$1 = defineNuxtModule({
|
|
|
78
92
|
browserCacheSeconds: options.browserCacheSeconds ?? 30,
|
|
79
93
|
cdnCacheSeconds: options.cdnCacheSeconds ?? 300
|
|
80
94
|
};
|
|
95
|
+
const tagged = tagPreviewPathRoutes(options.previewPaths ?? {}, nuxt.options.routeRules ??= {});
|
|
96
|
+
if (tagged.length) {
|
|
97
|
+
const width = Math.max(...tagged.map(({ route }) => route.length));
|
|
98
|
+
logger.info(
|
|
99
|
+
`Cache tags added to ${tagged.length} public route${tagged.length > 1 ? "s" : ""}, so publishing can purge them:
|
|
100
|
+
` + tagged.map(({ route, tag }) => ` ${route.padEnd(width)} ${tag}`).join("\n")
|
|
101
|
+
);
|
|
102
|
+
}
|
|
81
103
|
const preparing = Boolean(nuxt.options._prepare);
|
|
82
104
|
if (!options.prismaClient) {
|
|
83
105
|
if (!preparing) throw new Error(`${pc.red("[Eponyme]")} eponyme.prismaClient is required.`);
|
|
@@ -141,7 +163,6 @@ Note that '~/' points at ${nuxt.options.srcDir}, so a server module needs '~~/'.
|
|
|
141
163
|
nuxt.options.alias["#eponyme/prisma"] = prismaPath;
|
|
142
164
|
const variablesPath = await findPath(resolve(nuxt.options.rootDir, "eponyme.variables.ts"));
|
|
143
165
|
nuxt.options.alias["#eponyme/variables"] = variablesPath ?? resolver.resolve("./runtime/utils/empty-variables");
|
|
144
|
-
nuxt.options.alias["#eponyme/captcha"] ??= resolver.resolve("./runtime/utils/no-captcha");
|
|
145
166
|
addTypeTemplate({
|
|
146
167
|
filename: "types/eponyme-config.d.ts",
|
|
147
168
|
getContents: () => `declare module '#eponyme/config' {
|
|
@@ -152,10 +173,6 @@ declare module '#eponyme/variables' {
|
|
|
152
173
|
const variables: typeof import(${JSON.stringify(nuxt.options.alias["#eponyme/variables"])})['default']
|
|
153
174
|
export default variables
|
|
154
175
|
}
|
|
155
|
-
declare module '#eponyme/captcha' {
|
|
156
|
-
const verifier: import('${resolver.resolve("./runtime/types/captcha")}').EponymeCaptchaVerifier
|
|
157
|
-
export default verifier
|
|
158
|
-
}
|
|
159
176
|
`
|
|
160
177
|
});
|
|
161
178
|
addServerHandler({ middleware: true, handler: resolver.resolve("./runtime/server/middleware/eponyme-no-store") });
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { middleEllipsis } from "../../utils/middle-ellipsis";
|
|
3
4
|
const props = defineProps({
|
|
4
5
|
basePath: { type: String, required: true },
|
|
5
6
|
eponyme: { type: String, required: true }
|
|
@@ -24,14 +25,6 @@ const visibleBreadcrumbs = computed(() => {
|
|
|
24
25
|
function label(name) {
|
|
25
26
|
return name.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
26
27
|
}
|
|
27
|
-
function middleEllipsis(value, maxLength = 28) {
|
|
28
|
-
const characters = [...value];
|
|
29
|
-
if (characters.length <= maxLength) return value;
|
|
30
|
-
const available = maxLength - 1;
|
|
31
|
-
const startLength = Math.ceil(available / 2);
|
|
32
|
-
const endLength = Math.floor(available / 2);
|
|
33
|
-
return `${characters.slice(0, startLength).join("")}\u2026${characters.slice(-endLength).join("")}`;
|
|
34
|
-
}
|
|
35
28
|
function updateBreadcrumbMenuScroll(event) {
|
|
36
29
|
const menu = event.currentTarget;
|
|
37
30
|
if (!menu) return;
|
|
@@ -171,11 +171,11 @@ async function deleteSubmission(submission) {
|
|
|
171
171
|
v-for="header in headerGroup.headers"
|
|
172
172
|
:key="header.id"
|
|
173
173
|
scope="col"
|
|
174
|
-
class="ep:border-b ep:border-border-ep ep:p-3 ep:text-[11px] ep:font-semibold ep:
|
|
174
|
+
class="ep:border-b ep:border-border-ep ep:p-3 ep:text-[11px] ep:font-semibold ep:text-muted-ep ep:uppercase"
|
|
175
175
|
>
|
|
176
176
|
<button
|
|
177
177
|
type="button"
|
|
178
|
-
class="ep:flex ep:cursor-pointer ep:items-center ep:gap-1 ep:border-0 ep:bg-transparent ep:p-0 ep:text-inherit
|
|
178
|
+
class="ep:flex ep:cursor-pointer ep:items-center ep:gap-1 ep:border-0 ep:bg-transparent ep:p-0 ep:text-inherit"
|
|
179
179
|
:aria-sort="header.column.getIsSorted() === 'asc' ? 'ascending' : header.column.getIsSorted() === 'desc' ? 'descending' : 'none'"
|
|
180
180
|
@click="header.column.toggleSorting()"
|
|
181
181
|
>
|
|
@@ -3,6 +3,7 @@ import { computed, ref, watch } from "vue";
|
|
|
3
3
|
import EPButton from "../ui/EPButton.vue";
|
|
4
4
|
import EPSelect from "../ui/EPSelect.vue";
|
|
5
5
|
import { formatVersionDate, useEponymeHistory } from "../../composables/useEponymeHistory";
|
|
6
|
+
import { EPONYME_PREVIEW_QUERY, EPONYME_PREVIEW_TOKEN_QUERY, EPONYME_PREVIEW_VERSION_QUERY } from "../../utils/preview";
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
name: { type: String, required: true },
|
|
8
9
|
path: { type: String, required: true },
|
|
@@ -25,9 +26,9 @@ const options = computed(() => [
|
|
|
25
26
|
]);
|
|
26
27
|
const previewUrl = computed(() => {
|
|
27
28
|
const url = new URL(props.path, "http://placeholder");
|
|
28
|
-
url.searchParams.set(
|
|
29
|
-
url.searchParams.set(
|
|
30
|
-
url.searchParams.set(
|
|
29
|
+
url.searchParams.set(EPONYME_PREVIEW_QUERY, props.name);
|
|
30
|
+
url.searchParams.set(EPONYME_PREVIEW_VERSION_QUERY, selected.value);
|
|
31
|
+
url.searchParams.set(EPONYME_PREVIEW_TOKEN_QUERY, previewToken.value);
|
|
31
32
|
return `${url.pathname}${url.search}`;
|
|
32
33
|
});
|
|
33
34
|
async function reload() {
|
|
@@ -12,6 +12,7 @@ import { useEponymeAuth } from "../../composables/useEponymeAuth";
|
|
|
12
12
|
import EPAvatar from "../ui/EPAvatar.vue";
|
|
13
13
|
import EPButton from "../ui/EPButton.vue";
|
|
14
14
|
import EPInputText from "../ui/EPInputText.vue";
|
|
15
|
+
import logoUrl from "../../assets/logo.png?url";
|
|
15
16
|
const props = defineProps({
|
|
16
17
|
basePath: { type: String, required: true }
|
|
17
18
|
});
|
|
@@ -128,9 +129,13 @@ watch(() => route.path, () => {
|
|
|
128
129
|
<NuxtLink
|
|
129
130
|
v-if="!collapsed"
|
|
130
131
|
:to="normalizedBasePath"
|
|
131
|
-
class="ep:flex ep:min-w-0 ep:items-center ep:
|
|
132
|
+
class="ep:flex ep:min-w-0 ep:items-center ep:no-underline"
|
|
132
133
|
>
|
|
133
|
-
|
|
134
|
+
<img
|
|
135
|
+
:src="logoUrl"
|
|
136
|
+
alt="Eponyme"
|
|
137
|
+
class="ep:block ep:size-8"
|
|
138
|
+
>
|
|
134
139
|
</NuxtLink>
|
|
135
140
|
<button
|
|
136
141
|
type="button"
|
|
@@ -5,6 +5,7 @@ import CheckboxGroupField from "./CheckboxGroupField.vue";
|
|
|
5
5
|
import ColorField from "./ColorField.vue";
|
|
6
6
|
import DateField from "./DateField.vue";
|
|
7
7
|
import NumberField from "./NumberField.vue";
|
|
8
|
+
import PhoneField from "./PhoneField.vue";
|
|
8
9
|
import RadioField from "./RadioField.vue";
|
|
9
10
|
import RichTextField from "./RichTextField.vue";
|
|
10
11
|
import SelectField from "./SelectField.vue";
|
|
@@ -59,6 +60,8 @@ const component = computed(() => {
|
|
|
59
60
|
return ColorField;
|
|
60
61
|
case "url":
|
|
61
62
|
return UrlField;
|
|
63
|
+
case "phone":
|
|
64
|
+
return PhoneField;
|
|
62
65
|
default:
|
|
63
66
|
return TextField;
|
|
64
67
|
}
|
|
@@ -95,6 +98,14 @@ const specificProps = computed(() => {
|
|
|
95
98
|
return { presets: field.options.presets };
|
|
96
99
|
case "url":
|
|
97
100
|
return { placeholder: field.options.placeholder };
|
|
101
|
+
case "phone":
|
|
102
|
+
return {
|
|
103
|
+
placeholder: field.options.placeholder,
|
|
104
|
+
countries: field.options.countries,
|
|
105
|
+
defaultCountry: field.options.defaultCountry,
|
|
106
|
+
detectCountry: field.options.detectCountry,
|
|
107
|
+
autocomplete: field.options.autocomplete
|
|
108
|
+
};
|
|
98
109
|
default: {
|
|
99
110
|
const options = field.options;
|
|
100
111
|
return {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PhoneFieldOptions } from '../../types/field.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
modelValue?: unknown;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
countries?: PhoneFieldOptions['countries'];
|
|
10
|
+
defaultCountry?: PhoneFieldOptions['defaultCountry'];
|
|
11
|
+
detectCountry?: boolean;
|
|
12
|
+
autocomplete?: PhoneFieldOptions['autocomplete'];
|
|
13
|
+
errors?: string[];
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
"update:modelValue": (value: string) => any;
|
|
18
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
19
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: typeof __VLS_export;
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import EPFormField from "../ui/EPFormField.vue";
|
|
4
|
+
import EPInputText from "../ui/EPInputText.vue";
|
|
5
|
+
import { normalizeEponymePhone } from "../../utils/normalize-phone";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
id: { type: String, required: true },
|
|
8
|
+
modelValue: { type: null, required: false },
|
|
9
|
+
label: { type: String, required: true },
|
|
10
|
+
description: { type: String, required: false },
|
|
11
|
+
required: { type: Boolean, required: false },
|
|
12
|
+
placeholder: { type: String, required: false },
|
|
13
|
+
countries: { type: Array, required: false },
|
|
14
|
+
defaultCountry: { type: String, required: false },
|
|
15
|
+
detectCountry: { type: Boolean, required: false },
|
|
16
|
+
autocomplete: { type: String, required: false },
|
|
17
|
+
errors: { type: Array, required: false, default: () => [] },
|
|
18
|
+
disabled: { type: Boolean, required: false }
|
|
19
|
+
});
|
|
20
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
21
|
+
const value = computed(() => typeof props.modelValue === "string" ? props.modelValue : "");
|
|
22
|
+
const options = computed(() => ({
|
|
23
|
+
countries: props.countries,
|
|
24
|
+
defaultCountry: props.defaultCountry,
|
|
25
|
+
detectCountry: props.detectCountry
|
|
26
|
+
}));
|
|
27
|
+
const parsed = computed(() => normalizeEponymePhone(value.value, options.value));
|
|
28
|
+
function normalize() {
|
|
29
|
+
const next = parsed.value.e164;
|
|
30
|
+
if (next && next !== value.value) emit("update:modelValue", next);
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<EPFormField
|
|
36
|
+
:id="id"
|
|
37
|
+
:label="label"
|
|
38
|
+
:description="description"
|
|
39
|
+
:required="required"
|
|
40
|
+
:errors="errors"
|
|
41
|
+
>
|
|
42
|
+
<EPInputText
|
|
43
|
+
:id="id"
|
|
44
|
+
:model-value="value"
|
|
45
|
+
type="tel"
|
|
46
|
+
:autocomplete="autocomplete"
|
|
47
|
+
:placeholder="placeholder"
|
|
48
|
+
:required="required"
|
|
49
|
+
:invalid="Boolean(errors.length)"
|
|
50
|
+
:disabled="disabled"
|
|
51
|
+
@update:model-value="emit('update:modelValue', String($event))"
|
|
52
|
+
@blur="normalize"
|
|
53
|
+
/>
|
|
54
|
+
<p
|
|
55
|
+
v-if="parsed.e164 && parsed.country"
|
|
56
|
+
class="ep:mt-1.5 ep:mb-0 ep:text-[11px] ep:text-muted-ep"
|
|
57
|
+
>
|
|
58
|
+
{{ parsed.country }} · {{ parsed.e164 }}
|
|
59
|
+
</p>
|
|
60
|
+
</EPFormField>
|
|
61
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PhoneFieldOptions } from '../../types/field.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
modelValue?: unknown;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
countries?: PhoneFieldOptions['countries'];
|
|
10
|
+
defaultCountry?: PhoneFieldOptions['defaultCountry'];
|
|
11
|
+
detectCountry?: boolean;
|
|
12
|
+
autocomplete?: PhoneFieldOptions['autocomplete'];
|
|
13
|
+
errors?: string[];
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
"update:modelValue": (value: string) => any;
|
|
18
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
19
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: typeof __VLS_export;
|
|
22
|
+
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
|
+
import { middleEllipsis } from "../../utils/middle-ellipsis";
|
|
3
4
|
import EPButton from "./EPButton.vue";
|
|
4
5
|
import EPLinkDialog from "./EPLinkDialog.vue";
|
|
5
6
|
import EPTooltip from "./EPTooltip.vue";
|
|
@@ -51,7 +52,7 @@ function openLink() {
|
|
|
51
52
|
/>
|
|
52
53
|
<div class="ep:min-w-0 ep:flex-1">
|
|
53
54
|
<p class="ep:m-0 ep:truncate ep:text-sm ep:text-white">
|
|
54
|
-
{{ value.href
|
|
55
|
+
{{ value.href ? middleEllipsis(value.href, 40) : "No link configured" }}
|
|
55
56
|
</p>
|
|
56
57
|
<p class="ep:mt-0.5 ep:mb-0 ep:text-[11px] ep:text-muted-ep">
|
|
57
58
|
{{ value.type === "internal" ? "Internal" : "External" }} · {{ value.download ? "Download" : value.openInNewTab ? "New tab" : "Same tab" }}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ref } from 'vue';
|
|
1
|
+
import type { ComputedRef, Ref } from 'vue';
|
|
2
2
|
import type eponymeConfig from '#eponyme/config';
|
|
3
3
|
import type { EponymeDataByName, EponymeName } from '../types/index.js';
|
|
4
4
|
import type { EponymeAction, EponymeStatus, EponymeVersionSelector } from '../server/services/eponyme-store.js';
|
|
@@ -9,18 +9,26 @@ export interface UseEponymeOptions {
|
|
|
9
9
|
/** Keep `{{ variables }}` unresolved. The dashboard editor needs the source text. */
|
|
10
10
|
raw?: boolean;
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Named, and annotated on the function below, rather than left to inference.
|
|
14
|
+
*
|
|
15
|
+
* An inferred return type has to be expanded when the declaration files are generated, and
|
|
16
|
+
* the expansion loses `typeof eponymeConfig`: it came out as `EponymeDataByName<any, Name>`,
|
|
17
|
+
* which collapses every field to the union of all possible field values. Source and
|
|
18
|
+
* playground were unaffected — they read `src` — so only installed versions of the package
|
|
19
|
+
* lost their types, on the one composable everything else goes through.
|
|
20
|
+
*/
|
|
21
|
+
export interface UseEponymeResult<Data extends Record<string, unknown>> {
|
|
22
|
+
data: Ref<Data | undefined>;
|
|
23
|
+
status: ComputedRef<EponymeStatus>;
|
|
24
|
+
publishedAt: ComputedRef<string | null>;
|
|
25
|
+
pending: ComputedRef<boolean>;
|
|
26
|
+
error: Ref<Error | null | undefined>;
|
|
27
|
+
errors: Ref<ValidationErrors>;
|
|
20
28
|
refresh: () => Promise<void>;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
/** Without a patch, saves the data currently held. Publishes unless told otherwise. */
|
|
30
|
+
save: (patch?: Partial<Data>, action?: EponymeAction) => Promise<Data | undefined>;
|
|
31
|
+
}
|
|
32
|
+
/** Read and save a configured eponyme through the public or draft API. */
|
|
33
|
+
export declare function useEponyme<const Name extends ConfigEponymeName>(name: Name, options?: UseEponymeOptions): UseEponymeResult<EponymeDataByName<typeof eponymeConfig, Name>>;
|
|
26
34
|
export {};
|