@webx-ui/module-vacancies 0.2.5 → 0.3.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.
@@ -0,0 +1,378 @@
1
+ import { defineComponent as ge, computed as o, ref as p, watch as X, onMounted as we, onBeforeUnmount as xe, openBlock as k, createElementBlock as q, Fragment as Y, createVNode as r, unref as a, withCtx as n, createTextVNode as T, toDisplayString as w, createBlock as L, createCommentVNode as M, createElementVNode as _e } from "vue";
2
+ import { useRoute as ke, useRouter as Te, onBeforeRouteLeave as Ve } from "vue-router";
3
+ import { useAdmin as We, useDates as Ee, useTranslate as Z, useErrorText as Se, useEditing as Be, provideRelationOwner as Ne, provideRecordAddress as Ce, WxScreenHead as $e, WxEditingAlerts as Ae, WxScreen as Oe, WxSaveState as Re } from "@webx-ui/module-admin";
4
+ import { useLocales as De, localizedValue as Le, confirm as P, WxSkeleton as ee, WxCard as Me, WxBadge as z, WxBreadcrumb as Pe, WxBreadcrumbItem as ae, WxActionBar as Ue, WxButton as j, WxIcon as Fe, toast as d } from "@webx-ui/core";
5
+ import { c as Ie, u as Je, p as qe, a as ze } from "./index-V7QvGgXw.js";
6
+ import { l as je } from "./days-BPKXoD8l.js";
7
+ const Ge = { class: "wx-vacancy-editor__word" }, He = 1200, Ke = /* @__PURE__ */ ge({
8
+ __name: "VacancyEditorPage",
9
+ props: {
10
+ base: { default: "/vacancies" }
11
+ },
12
+ setup(te) {
13
+ const le = te, U = We(), y = Ie(U), G = ke(), H = Te(), ie = De(), ue = Ee();
14
+ Je();
15
+ const l = Z("webx-vacancies"), v = Z("webx-admin"), V = Se(), $ = o(() => Number(G.params.id)), A = o(() => le.base), t = p(null), b = p({}), O = p(""), R = p(null), B = p(null), F = p(!0), x = p(!1), c = p(!1), I = p({}), N = p(""), h = o(() => U.can("vacancies.manage")), u = Be({
16
+ entity: "vacancies",
17
+ id: () => t.value?.id,
18
+ values: b,
19
+ revision: O,
20
+ read: async () => {
21
+ const e = await y.get($.value);
22
+ return { values: e.values, revision: e.revision };
23
+ },
24
+ adopt: (e) => {
25
+ N.value = JSON.stringify(e.values);
26
+ },
27
+ // Published or restored by somebody else: the badge and the address follow, and the form
28
+ // stays as it is.
29
+ refresh: () => oe(),
30
+ restore: async () => {
31
+ t.value && await y.restore(t.value.id);
32
+ },
33
+ canWrite: () => h.value,
34
+ busy: () => x.value,
35
+ screen: "vacancies.form"
36
+ }), W = u.conflict, ne = o(() => !h.value || c.value), D = o(() => JSON.stringify(b.value)), s = o(() => N.value !== "" && D.value !== N.value), re = o(() => x.value ? "saving" : s.value ? "unsaved" : "saved"), J = o(() => {
37
+ const e = b.value.title;
38
+ return Le(e, ie.active.value, t.value?.title ?? "");
39
+ }), se = o(() => {
40
+ const e = t.value;
41
+ if (!e) return "";
42
+ const i = [];
43
+ return e.valid_through !== null && !e.closed && i.push(
44
+ l("editor.open-until", { date: je(e.valid_through, U.i18n.state.locale) })
45
+ ), (e.status === "published" || e.status === "modified") && i.push(l("editor.live-since", { date: ue.short(e.published_at) })), i.join(" · ");
46
+ });
47
+ Ne({ type: "vacancy", id: o(() => t.value?.id ?? null) }), Ce({
48
+ values: b,
49
+ prefix: R,
50
+ path: o(() => t.value?.path),
51
+ moving: () => l("editor.address-moving")
52
+ }), qe({ vacancy: t, canManage: h.value, reload: () => E(!0) });
53
+ function K(e) {
54
+ const i = e.values;
55
+ t.value = e.vacancy, b.value = i, O.value = e.revision, R.value = e.prefix, B.value = e.preview_url, N.value = JSON.stringify(i), u.opened(i), W.value = null;
56
+ }
57
+ async function E(e = !1) {
58
+ e || (F.value = !0);
59
+ try {
60
+ K(await y.get($.value));
61
+ } catch (i) {
62
+ d.danger(V(i)), H.push(A.value);
63
+ } finally {
64
+ F.value = !1;
65
+ }
66
+ }
67
+ async function oe() {
68
+ try {
69
+ const e = await y.get($.value);
70
+ t.value = e.vacancy, R.value = e.prefix, B.value = e.preview_url;
71
+ } catch {
72
+ }
73
+ }
74
+ let C;
75
+ function ce() {
76
+ !h.value || W.value || !s.value || u.stopped.value || (clearTimeout(C), C = setTimeout(() => {
77
+ g();
78
+ }, He));
79
+ }
80
+ function de() {
81
+ !h.value || W.value || !s.value || x.value || u.stopped.value || (clearTimeout(C), g());
82
+ }
83
+ async function g() {
84
+ if (!t.value || x.value || !h.value) return;
85
+ clearTimeout(C);
86
+ const e = D.value, i = b.value;
87
+ x.value = !0, I.value = {};
88
+ let m = !1;
89
+ try {
90
+ const f = await y.save(t.value.id, { values: i, revision: O.value });
91
+ t.value = f.vacancy, O.value = f.revision, R.value = f.prefix, B.value = f.preview_url, W.value = null, D.value === e && (N.value = e), u.opened(i);
92
+ } catch (f) {
93
+ const _ = f;
94
+ if (_.status === 409 && _.body?.data) {
95
+ const S = _.body.data;
96
+ u.refused({
97
+ values: S.values,
98
+ revision: S.revision,
99
+ changed: _.body.changed
100
+ }) && (t.value = S.vacancy, m = !0);
101
+ } else _.body?.errors ? (I.value = _.body.errors, d.danger(l("editor.save-failed"))) : await u.failed(f) || d.danger(V(f));
102
+ } finally {
103
+ x.value = !1;
104
+ }
105
+ m && await g();
106
+ }
107
+ async function ve() {
108
+ await E(!0);
109
+ }
110
+ async function fe() {
111
+ if (!(!t.value || !await P({
112
+ title: v("editor.discard-title"),
113
+ message: v("editor.discard-text"),
114
+ confirmText: l("editor.discard"),
115
+ // Not «Cancel»: beside «Discard changes» the two read as the same word.
116
+ cancelText: v("editor.keep-changes"),
117
+ tone: "danger"
118
+ }))) {
119
+ c.value = !0;
120
+ try {
121
+ K(await y.discard(t.value.id)), d.success(l("editor.discarded"));
122
+ } catch (i) {
123
+ d.danger(V(i));
124
+ } finally {
125
+ c.value = !1;
126
+ }
127
+ }
128
+ }
129
+ async function pe() {
130
+ if (s.value && await g(), W.value || s.value || !t.value) return;
131
+ const e = await u.beforePublish();
132
+ if (!e) return;
133
+ const i = t.value, m = i.next_path ?? i.path, f = i.next_path != null && i.path !== null ? ` ${v("editor.publish-moves", { old: `/${i.path}` })}` : "";
134
+ if (e.asked || await P({
135
+ title: l("editor.publish-title", { title: J.value || i.title }),
136
+ message: m === null ? l("editor.publish-nowhere") : l("editor.publish-text", { address: `/${m}` }) + f,
137
+ confirmText: l("panel.publish"),
138
+ cancelText: l("panel.cancel")
139
+ })) {
140
+ c.value = !0;
141
+ try {
142
+ await y.publish(i.id, e.revision), await E(!0), d.success(l("panel.published"));
143
+ } catch (S) {
144
+ await u.failed(S) || d.danger(V(S));
145
+ } finally {
146
+ c.value = !1;
147
+ }
148
+ }
149
+ }
150
+ async function ye() {
151
+ const e = t.value;
152
+ if (e && (s.value && await g(), !(W.value || s.value))) {
153
+ c.value = !0;
154
+ try {
155
+ const i = await y.duplicate(e.id);
156
+ d.success(l("panel.duplicated")), await H.push({ path: `${A.value}/${i.vacancy.id}`, query: { ...G.query } });
157
+ } catch (i) {
158
+ d.danger(V(i));
159
+ } finally {
160
+ c.value = !1;
161
+ }
162
+ }
163
+ }
164
+ async function he() {
165
+ const e = t.value;
166
+ if (!(!e || !await P({
167
+ title: v("editor.unpublish-title"),
168
+ message: v("editor.unpublish-text"),
169
+ confirmText: v("editor.unpublish"),
170
+ cancelText: v("editor.keep-published")
171
+ }))) {
172
+ c.value = !0;
173
+ try {
174
+ await y.unpublish(e.id), await E(!0), d.success(v("editor.unpublished"));
175
+ } catch (m) {
176
+ d.danger(V(m));
177
+ } finally {
178
+ c.value = !1;
179
+ }
180
+ }
181
+ }
182
+ function me() {
183
+ const e = t.value?.status;
184
+ return e === "published" || e === "modified" ? "success" : "default";
185
+ }
186
+ function Q(e) {
187
+ s.value && e.preventDefault();
188
+ }
189
+ X(D, ce), X($, () => {
190
+ E();
191
+ }), we(() => {
192
+ E(), window.addEventListener("beforeunload", Q);
193
+ }), xe(() => {
194
+ clearTimeout(C), window.removeEventListener("beforeunload", Q);
195
+ }), Ve(async () => !s.value || !h.value || (await g(), !s.value) ? !0 : await P({
196
+ title: l("editor.leave-title"),
197
+ message: l("editor.leave-text"),
198
+ confirmText: l("editor.leave"),
199
+ cancelText: l("panel.cancel"),
200
+ tone: "danger"
201
+ }));
202
+ const be = o(() => {
203
+ const e = [];
204
+ return B.value && e.push({ key: "preview", label: l("editor.preview"), icon: "eye", href: B.value }), t.value?.url && t.value.status !== "draft" && e.push({
205
+ key: "site",
206
+ label: l("panel.open-on-site"),
207
+ icon: "link",
208
+ href: t.value.url
209
+ }), h.value && (t.value?.status === "published" || t.value?.status === "modified") && e.push({
210
+ key: "unpublish",
211
+ label: v("editor.unpublish"),
212
+ icon: "eye-off",
213
+ run: () => {
214
+ he();
215
+ }
216
+ }), t.value?.status === "modified" && e.push({
217
+ key: "discard",
218
+ label: l("editor.discard"),
219
+ icon: "refresh",
220
+ danger: !0,
221
+ menu: !0,
222
+ run: () => {
223
+ fe();
224
+ }
225
+ }), e;
226
+ });
227
+ return (e, i) => (k(), q("div", {
228
+ class: "wx-vacancy-editor",
229
+ onFocusout: de
230
+ }, [
231
+ F.value || !t.value ? (k(), q(Y, { key: 0 }, [
232
+ r(a(ee), {
233
+ class: "wx-vacancy-editor__ghost",
234
+ title: "",
235
+ rows: 1
236
+ }),
237
+ r(a(Me), null, {
238
+ default: n(() => [
239
+ r(a(ee), { rows: 8 })
240
+ ]),
241
+ _: 1
242
+ })
243
+ ], 64)) : (k(), q(Y, { key: 1 }, [
244
+ r(a($e), {
245
+ divider: "",
246
+ back: A.value,
247
+ "back-label": a(l)("module.vacancies"),
248
+ title: J.value || a(l)("editor.untitled"),
249
+ subtitle: se.value,
250
+ actions: be.value
251
+ }, {
252
+ trail: n(() => [
253
+ r(a(Pe), {
254
+ size: "sm",
255
+ label: a(l)("editor.trail")
256
+ }, {
257
+ default: n(() => [
258
+ r(a(ae), {
259
+ as: "router-link",
260
+ to: A.value
261
+ }, {
262
+ default: n(() => [
263
+ T(w(a(l)("module.vacancies")), 1)
264
+ ]),
265
+ _: 1
266
+ }, 8, ["to"]),
267
+ r(a(ae), { current: "" }, {
268
+ default: n(() => [
269
+ T(w(J.value || a(l)("editor.untitled")), 1)
270
+ ]),
271
+ _: 1
272
+ })
273
+ ]),
274
+ _: 1
275
+ }, 8, ["label"])
276
+ ]),
277
+ "title-after": n(() => [
278
+ r(a(z), {
279
+ type: me(),
280
+ dot: ""
281
+ }, {
282
+ default: n(() => [
283
+ T(w(a(l)(`panel.status-${t.value.status}`)), 1)
284
+ ]),
285
+ _: 1
286
+ }, 8, ["type"]),
287
+ t.value.status === "modified" ? (k(), L(a(z), {
288
+ key: 0,
289
+ type: "primary",
290
+ round: ""
291
+ }, {
292
+ default: n(() => [
293
+ T(w(a(l)("panel.edits")), 1)
294
+ ]),
295
+ _: 1
296
+ })) : M("", !0),
297
+ t.value.closed_reason ? (k(), L(a(z), {
298
+ key: 1,
299
+ type: "warning",
300
+ round: ""
301
+ }, {
302
+ default: n(() => [
303
+ T(w(a(l)(`panel.closed-${t.value.closed_reason}`)), 1)
304
+ ]),
305
+ _: 1
306
+ })) : M("", !0)
307
+ ]),
308
+ _: 1
309
+ }, 8, ["back", "back-label", "title", "subtitle", "actions"]),
310
+ r(a(Ae), {
311
+ editing: a(u),
312
+ onSave: g,
313
+ onTheirs: ve
314
+ }, null, 8, ["editing"]),
315
+ r(a(Oe), {
316
+ modelValue: b.value,
317
+ "onUpdate:modelValue": i[0] || (i[0] = (m) => b.value = m),
318
+ name: "vacancies.form",
319
+ errors: I.value,
320
+ disabled: ne.value
321
+ }, null, 8, ["modelValue", "errors", "disabled"]),
322
+ h.value ? (k(), L(a(Ue), { key: 0 }, {
323
+ state: n(() => [
324
+ a(u).stopped.value ? M("", !0) : (k(), L(a(Re), {
325
+ key: 0,
326
+ state: re.value
327
+ }, null, 8, ["state"]))
328
+ ]),
329
+ default: n(() => [
330
+ r(a(j), {
331
+ variant: "text",
332
+ disabled: c.value,
333
+ "aria-label": a(l)("panel.duplicate"),
334
+ onClick: ye
335
+ }, {
336
+ icon: n(() => [
337
+ r(a(Fe), { name: "copy" })
338
+ ]),
339
+ default: n(() => [
340
+ _e("span", Ge, w(a(l)("panel.duplicate")), 1)
341
+ ]),
342
+ _: 1
343
+ }, 8, ["disabled", "aria-label"]),
344
+ r(a(j), {
345
+ variant: "outline",
346
+ loading: x.value,
347
+ disabled: !s.value || a(u).stopped.value,
348
+ title: a(u).blocked.value,
349
+ onClick: g
350
+ }, {
351
+ default: n(() => [
352
+ T(w(a(l)("editor.save")), 1)
353
+ ]),
354
+ _: 1
355
+ }, 8, ["loading", "disabled", "title"]),
356
+ r(a(j), {
357
+ type: "primary",
358
+ loading: c.value,
359
+ disabled: a(u).stopped.value || t.value.status === "published" && !s.value,
360
+ title: a(u).blocked.value,
361
+ onClick: pe
362
+ }, {
363
+ default: n(() => [
364
+ T(w(a(l)("panel.publish")), 1)
365
+ ]),
366
+ _: 1
367
+ }, 8, ["loading", "disabled", "title"])
368
+ ]),
369
+ _: 1
370
+ })) : M("", !0)
371
+ ], 64))
372
+ ], 32));
373
+ }
374
+ }), ta = /* @__PURE__ */ ze(Ke, [["__scopeId", "data-v-d2657864"]]);
375
+ export {
376
+ ta as default
377
+ };
378
+ //# sourceMappingURL=VacancyEditorPage-Dw_pfwyv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VacancyEditorPage-Dw_pfwyv.js","sources":["../src/VacancyEditorPage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'\nimport {\n provideRecordAddress,\n provideRelationOwner,\n useAdmin,\n useDates,\n useEditing,\n useErrorText,\n useTranslate,\n WxEditingAlerts,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxBadge,\n WxBreadcrumb,\n WxBreadcrumbItem,\n WxButton,\n WxCard,\n WxIcon,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createVacanciesApi } from './api'\nimport { longDay } from './days'\nimport { provideVacancyEditor } from './editor'\nimport { useVacanciesMessages } from './i18n'\nimport type { VacancyConflict, VacancyDetail, VacancyRow } from './types'\n\n/**\n * The editor of one vacancy: a head that stays put, the described screen under it, and the bar\n * along the bottom where it is saved, duplicated and published (§4.10).\n *\n * Everything between the head and the bar is `vacancies.form` — the vacancy, its settings, the SEO\n * card `module-seo` patches on and the history — so a project adds its own field with a patch\n * rather than a fork of this file. Saving is by autosave into the draft, and the draft carries\n * the categories, the form and \"closed\" too: the site changes, all of it at once, only on\n * \"Publish\". Edits are guarded by a revision: a save over somebody else's is refused with a 409,\n * merged with theirs, and asked about only where both changed the same place.\n *\n * The two dates are calendar days, not moments (decision 13): the pickers hold `YYYY-MM-DD` as\n * written, and the head prints them from their parts (`days.ts`), so no zone moves them.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/vacancies' })\n\nconst context = useAdmin()\nconst api = createVacanciesApi(context)\nconst route = useRoute()\nconst router = useRouter()\nconst locales = useLocales()\nconst dates = useDates()\nuseVacanciesMessages()\n\nconst t = useTranslate('webx-vacancies')\nconst panel = useTranslate('webx-admin')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\n/** How long after the last keystroke the draft goes to the server. */\nconst PAUSE = 1200\n\nconst id = computed(() => Number(route.params.id))\nconst list = computed(() => props.base)\n\nconst vacancy = ref<VacancyRow | null>(null)\nconst values = ref<ScreenModel>({})\nconst revision = ref('')\nconst prefix = ref<string | null>(null)\nconst previewUrl = ref<string | null>(null)\n\nconst loading = ref(true)\nconst saving = ref(false)\nconst working = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst snapshot = ref('')\n\nconst canManage = computed(() => context.can('vacancies.manage'))\n\n/*\n * Other people on the same vacancy: a refused save is merged with theirs field by field and\n * saved again, only a field both changed is asked about, and a save made meanwhile — by a\n * colleague or an agent — is offered before this editor writes over it.\n */\nconst editing = useEditing<ScreenModel>({\n entity: 'vacancies',\n id: () => vacancy.value?.id,\n values,\n revision,\n read: async () => {\n const detail = await api.get(id.value)\n\n return { values: detail.values, revision: detail.revision }\n },\n adopt: (theirs) => {\n snapshot.value = JSON.stringify(theirs.values)\n },\n // Published or restored by somebody else: the badge and the address follow, and the form\n // stays as it is.\n refresh: () => refresh(),\n restore: async () => {\n if (vacancy.value) await api.restore(vacancy.value.id)\n },\n canWrite: () => canManage.value,\n busy: () => saving.value,\n screen: 'vacancies.form',\n})\n\nconst conflict = editing.conflict\n\n/** Closed for writing: no permission, or a publication in flight. */\nconst locked = computed(() => !canManage.value || working.value)\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value ? 'unsaved' : 'saved'\n})\n\n/** The name follows the field rather than the answer: a rename shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.title as LocalizedValue | string | null | undefined\n\n return localizedValue(written, locales.active.value, vacancy.value?.title ?? '')\n})\n\n/**\n * Under the name: how long it is open, and since when it is on the site — the two facts the\n * badges beside the name cannot carry.\n */\nconst subtitle = computed(() => {\n const row = vacancy.value\n\n if (!row) return ''\n\n const parts: string[] = []\n\n if (row.valid_through !== null && !row.closed) {\n parts.push(\n t('editor.open-until', { date: longDay(row.valid_through, context.i18n.state.locale) }),\n )\n }\n\n if (row.status === 'published' || row.status === 'modified') {\n parts.push(t('editor.live-since', { date: dates.short(row.published_at) }))\n }\n\n return parts.join(' · ')\n})\n\n/* The vacancy as the owner of its relations — the one it has is the response form (decision 10). */\nprovideRelationOwner({ type: 'vacancy', id: computed(() => vacancy.value?.id ?? null) })\n\n/* The address field is the panel's shared one (`wx-slug`), and this is what it prints. */\nprovideRecordAddress({\n values,\n prefix,\n path: computed(() => vacancy.value?.path),\n moving: () => t('editor.address-moving'),\n})\n\nprovideVacancyEditor({ vacancy, canManage: canManage.value, reload: () => load(true) })\n\nfunction take(detail: VacancyDetail): void {\n const taken = detail.values\n\n vacancy.value = detail.vacancy\n values.value = taken\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n snapshot.value = JSON.stringify(taken)\n editing.opened(taken)\n conflict.value = null\n}\n\n/**\n * Read the vacancy again — quietly unless this is the first time: the skeleton replaces the whole\n * screen, and a publication answered with it would throw the editor back onto the first tab.\n */\nasync function load(silent = false): Promise<void> {\n if (!silent) loading.value = true\n\n try {\n take(await api.get(id.value))\n } catch (error) {\n toast.danger(message(error))\n void router.push(list.value)\n } finally {\n loading.value = false\n }\n}\n\n/**\n * What surrounds the form, read again: the row with its status, the address, the preview. Not\n * the values — the form is somebody's work in progress, and a change of its content is the\n * notice's to offer.\n */\nasync function refresh(): Promise<void> {\n try {\n const detail = await api.get(id.value)\n\n vacancy.value = detail.vacancy\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n } catch {\n // The heartbeat says what happened; a row that could not be read again stays as it was.\n }\n}\n\n/* One pending save at a time, and one pending pause. */\nlet timer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(): void {\n if (!canManage.value || conflict.value || !dirty.value || editing.stopped.value) return\n\n clearTimeout(timer)\n timer = setTimeout(() => void save(), PAUSE)\n}\n\n/** A field was left: write now rather than at the end of a pause nobody is waiting through. */\nfunction onFocusOut(): void {\n if (!canManage.value || conflict.value || !dirty.value || saving.value || editing.stopped.value)\n return\n\n clearTimeout(timer)\n void save()\n}\n\nasync function save(): Promise<void> {\n if (!vacancy.value || saving.value || !canManage.value) return\n\n clearTimeout(timer)\n\n // What is being sent, so that whatever is typed while the request is in flight stays dirty and\n // gets its own save rather than being marked as written.\n const sending = current.value\n const sent = values.value\n\n saving.value = true\n errors.value = {}\n\n // A refused save merged clean goes out again — after this one, since `save` stays shut while\n // a request is on its way.\n let merged = false\n\n try {\n const detail = await api.save(vacancy.value.id, { values: sent, revision: revision.value })\n\n vacancy.value = detail.vacancy\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n conflict.value = null\n\n if (current.value === sending) snapshot.value = sending\n\n // What the server holds now is what was sent: the base of the next merge.\n editing.opened(sent)\n } catch (error) {\n const failure = error as {\n status?: number\n body?: VacancyConflict & { errors?: Record<string, string[]> }\n }\n\n if (failure.status === 409 && failure.body?.data) {\n const theirs = failure.body.data\n\n // Nothing overlapping: both edits are in the form now, and they go to the server again.\n if (\n editing.refused({\n values: theirs.values,\n revision: theirs.revision,\n changed: failure.body.changed,\n })\n ) {\n vacancy.value = theirs.vacancy\n merged = true\n }\n } else if (failure.body?.errors) {\n errors.value = failure.body.errors\n toast.danger(t('editor.save-failed'))\n } else if (!(await editing.failed(error))) {\n // In the bin is said by the notice above the form, with the way back; anything else here.\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n\n if (merged) await save()\n}\n\n/** Give up what was typed and take the vacancy as it now is; the component asked first. */\nasync function takeTheirs(): Promise<void> {\n await load(true)\n}\n\n/** Throw away what is waiting — asked about, because it is the one button here that loses writing. */\nasync function discard(): Promise<void> {\n if (!vacancy.value) return\n\n const agreed = await confirm({\n title: panel('editor.discard-title'),\n message: panel('editor.discard-text'),\n confirmText: t('editor.discard'),\n // Not «Cancel»: beside «Discard changes» the two read as the same word.\n cancelText: panel('editor.keep-changes'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n take(await api.discard(vacancy.value.id))\n toast.success(t('editor.discarded'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n/** Publishing is asked about, because it is the one action here that visitors see. */\nasync function publish(): Promise<void> {\n // Saved before asking: the question names the address the draft will publish at, and only a\n // saved draft has one — an address typed but not saved was asked about under the old one.\n if (dirty.value) await save()\n if (conflict.value || dirty.value) return\n\n if (!vacancy.value) return\n\n // The draft on the server may hold somebody else's edit this editor never pulled in: said\n // before it goes on the site, with whose it is and where, rather than published unseen.\n const held = await editing.beforePublish()\n\n if (!held) return\n\n const row = vacancy.value\n\n // Publishing moves a renamed slug, so the question names where the page will be, not where\n // it is — and says the old address will lead there, since that is what happens to it.\n const next = row.next_path ?? row.path\n const old =\n row.next_path != null && row.path !== null\n ? ` ${panel('editor.publish-moves', { old: `/${row.path}` })}`\n : ''\n\n // One question is enough: whoever just agreed to publish somebody else's changes has said yes.\n const agreed =\n held.asked ||\n (await confirm({\n title: t('editor.publish-title', { title: title.value || row.title }),\n message:\n next === null\n ? t('editor.publish-nowhere')\n : t('editor.publish-text', { address: `/${next}` }) + old,\n confirmText: t('panel.publish'),\n cancelText: t('panel.cancel'),\n }))\n\n if (!agreed) return\n\n working.value = true\n\n try {\n await api.publish(row.id, held.revision)\n await load(true)\n toast.success(t('panel.published'))\n } catch (error) {\n if (!(await editing.failed(error))) toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n/**\n * The same position somewhere else: a copy as a draft, opened at once (decision 20). What is typed\n * here goes first — the copy is made from what the server holds, and an edit still in the pause\n * would be in the original and missing from the copy.\n */\nasync function duplicate(): Promise<void> {\n const row = vacancy.value\n\n if (!row) return\n\n if (dirty.value) await save()\n if (conflict.value || dirty.value) return\n\n working.value = true\n\n try {\n const copy = await api.duplicate(row.id)\n\n toast.success(t('panel.duplicated'))\n // The same route with another id: the editor stays and reads the copy (`watch(id)`).\n await router.push({ path: `${list.value}/${copy.vacancy.id}`, query: { ...route.query } })\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n/**\n * Taking it off the site is asked about, as publishing is: visitors are who notice. Nothing\n * written is lost, and «Publish» brings it back at the same address.\n */\nasync function unpublish(): Promise<void> {\n const row = vacancy.value\n\n if (!row) return\n\n const agreed = await confirm({\n title: panel('editor.unpublish-title'),\n message: panel('editor.unpublish-text'),\n confirmText: panel('editor.unpublish'),\n cancelText: panel('editor.keep-published'),\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n await api.unpublish(row.id)\n await load(true)\n toast.success(panel('editor.unpublished'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\nfunction badge(): 'default' | 'success' {\n const status = vacancy.value?.status\n\n return status === 'published' || status === 'modified' ? 'success' : 'default'\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(unload: BeforeUnloadEvent): void {\n if (dirty.value) unload.preventDefault()\n}\n\nwatch(current, schedule)\nwatch(id, () => void load())\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(timer)\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\n/**\n * Leaving flushes the pause rather than asking about it; the question is asked only when the save\n * did not go through — a conflict, a refused field, a server that is not there.\n */\nonBeforeRouteLeave(async () => {\n if (!dirty.value || !canManage.value) return true\n\n await save()\n\n if (!dirty.value) return true\n\n return await confirm({\n title: t('editor.leave-title'),\n message: t('editor.leave-text'),\n confirmText: t('editor.leave'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n})\n\n/* What leads away from the vacancy. On a phone the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() => {\n const leads: ScreenAction[] = []\n\n if (previewUrl.value) {\n leads.push({ key: 'preview', label: t('editor.preview'), icon: 'eye', href: previewUrl.value })\n }\n\n if (vacancy.value?.url && vacancy.value.status !== 'draft') {\n leads.push({\n key: 'site',\n label: t('panel.open-on-site'),\n icon: 'link',\n href: vacancy.value.url,\n })\n }\n\n // On the site now: the way off it is a button of its own beside «Open on the site», not a\n // line in the ···. Hiding loses nothing and is undone by «Publish».\n if (\n canManage.value &&\n (vacancy.value?.status === 'published' || vacancy.value?.status === 'modified')\n ) {\n leads.push({\n key: 'unpublish',\n label: panel('editor.unpublish'),\n icon: 'eye-off',\n run: () => void unpublish(),\n })\n }\n\n // Throwing away writing lives in the ···, never beside \"publish\", and only while there is a\n // difference between what is written and what is on the site.\n if (vacancy.value?.status === 'modified') {\n leads.push({\n key: 'discard',\n label: t('editor.discard'),\n icon: 'refresh',\n danger: true,\n menu: true,\n run: () => void discard(),\n })\n }\n\n return leads\n})\n</script>\n\n<template>\n <div class=\"wx-vacancy-editor\" @focusout=\"onFocusOut\">\n <template v-if=\"loading || !vacancy\">\n <wx-skeleton class=\"wx-vacancy-editor__ghost\" title :rows=\"1\" />\n <wx-card><wx-skeleton :rows=\"8\" /></wx-card>\n </template>\n\n <template v-else>\n <wx-screen-head\n divider\n :back=\"list\"\n :back-label=\"t('module.vacancies')\"\n :title=\"title || t('editor.untitled')\"\n :subtitle=\"subtitle\"\n :actions=\"actions\"\n >\n <template #trail>\n <wx-breadcrumb size=\"sm\" :label=\"t('editor.trail')\">\n <wx-breadcrumb-item :as=\"'router-link'\" :to=\"list\">\n {{ t('module.vacancies') }}\n </wx-breadcrumb-item>\n <wx-breadcrumb-item current>{{ title || t('editor.untitled') }}</wx-breadcrumb-item>\n </wx-breadcrumb>\n </template>\n\n <template #title-after>\n <wx-badge :type=\"badge()\" dot>{{ t(`panel.status-${vacancy.status}`) }}</wx-badge>\n <wx-badge v-if=\"vacancy.status === 'modified'\" type=\"primary\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n <!-- Closed, and still a page of the site: marked, off the lists (decision 4). -->\n <wx-badge v-if=\"vacancy.closed_reason\" type=\"warning\" round>\n {{ t(`panel.closed-${vacancy.closed_reason}`) }}\n </wx-badge>\n </template>\n </wx-screen-head>\n\n <!-- Somebody else wrote while this editor was open. What does not overlap is merged\n without a word; what does is listed place by place. -->\n <wx-editing-alerts :editing=\"editing\" @save=\"save\" @theirs=\"takeTheirs\" />\n\n <wx-screen v-model=\"values\" name=\"vacancies.form\" :errors=\"errors\" :disabled=\"locked\" />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state v-if=\"!editing.stopped.value\" :state=\"state\" />\n </template>\n\n <!-- The same position in another city is a copy, not a vacancy from nothing (decision 20). -->\n <wx-button\n variant=\"text\"\n :disabled=\"working\"\n :aria-label=\"t('panel.duplicate')\"\n @click=\"duplicate\"\n >\n <template #icon><wx-icon name=\"copy\" /></template>\n <span class=\"wx-vacancy-editor__word\">{{ t('panel.duplicate') }}</span>\n </wx-button>\n\n <wx-button\n variant=\"outline\"\n :loading=\"saving\"\n :disabled=\"!dirty || editing.stopped.value\"\n :title=\"editing.blocked.value\"\n @click=\"save\"\n >\n {{ t('editor.save') }}\n </wx-button>\n\n <wx-button\n type=\"primary\"\n :loading=\"working\"\n :disabled=\"editing.stopped.value || (vacancy.status === 'published' && !dirty)\"\n :title=\"editing.blocked.value\"\n @click=\"publish\"\n >\n {{ t('panel.publish') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n/*\n * No constructor here, so nothing needs the height of the window: the page scrolls, and the\n * panel gives a screen with an action bar a floor tall enough for the bar to sit at the bottom\n * (`WxMain`, CLAUDE.md §4 on `data-wx-fill`).\n */\n.wx-vacancy-editor {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-height: 0;\n container-type: inline-size;\n}\n\n.wx-vacancy-editor__ghost {\n max-width: 420px;\n}\n\n/*\n * On a phone the bar holds the state and three buttons, and with the word the third one pushes\n * the state onto a line of its own — a strip of nothing over the buttons (CLAUDE.md §4 on\n * `WxActionBar`). The copy icon is enough there; the word stays for a screen reader.\n */\n@container (max-width: 480px) {\n .wx-vacancy-editor__word {\n display: none;\n }\n}\n</style>\n"],"names":["PAUSE","props","__props","context","useAdmin","api","createVacanciesApi","route","useRoute","router","useRouter","locales","useLocales","dates","useDates","useVacanciesMessages","t","useTranslate","panel","message","useErrorText","id","computed","list","vacancy","ref","values","revision","prefix","previewUrl","loading","saving","working","errors","snapshot","canManage","editing","useEditing","detail","theirs","refresh","conflict","locked","current","dirty","state","title","written","localizedValue","subtitle","row","parts","longDay","provideRelationOwner","provideRecordAddress","provideVacancyEditor","load","take","taken","silent","error","toast","timer","schedule","save","onFocusOut","sending","sent","merged","failure","takeTheirs","discard","confirm","publish","held","next","old","duplicate","copy","unpublish","badge","status","leaveGuard","unload","watch","onMounted","onBeforeUnmount","onBeforeRouteLeave","actions","leads","_createElementBlock","_Fragment","_createVNode","_unref","WxSkeleton","WxCard","WxScreenHead","WxBreadcrumb","WxBreadcrumbItem","_createTextVNode","_toDisplayString","WxBadge","_createBlock","WxEditingAlerts","WxScreen","$event","WxActionBar","WxSaveState","WxButton","WxIcon","_createElementVNode","_hoisted_1"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ const l = /^(\d{4})-(\d{2})-(\d{2})$/;
2
+ function u(n) {
3
+ const t = typeof n == "string" ? l.exec(n.slice(0, 10)) : null;
4
+ if (t === null) return null;
5
+ const [, e, r, o] = t, a = new Date(Number(e), Number(r) - 1, Number(o));
6
+ return Number.isNaN(a.getTime()) ? null : a;
7
+ }
8
+ function m(n, t, e = /* @__PURE__ */ new Date()) {
9
+ const r = u(n);
10
+ return r === null ? "" : new Intl.DateTimeFormat(t, {
11
+ day: "numeric",
12
+ month: "short",
13
+ ...r.getFullYear() === e.getFullYear() ? {} : { year: "numeric" }
14
+ }).format(r);
15
+ }
16
+ function c(n, t) {
17
+ const e = u(n);
18
+ return e === null ? "" : new Intl.DateTimeFormat(t, { day: "numeric", month: "long", year: "numeric" }).format(e);
19
+ }
20
+ export {
21
+ m as f,
22
+ c as l
23
+ };
24
+ //# sourceMappingURL=days-BPKXoD8l.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"days-BPKXoD8l.js","sources":["../src/days.ts"],"sourcesContent":["/**\n * A vacancy's dates are calendar days (decision 13): `valid_through` is the last day it is open,\n * `posted_at` the day it was first put up, both `YYYY-MM-DD` with no time and no zone.\n *\n * Nothing here may turn such a day into a moment. `new Date('2026-11-30')` is midnight *UTC*, and\n * west of Greenwich that is still the 29th — the column would print the day before the one the\n * editor picked, and the site (which reads the day as written) would disagree with the panel. So\n * the day is built from its parts, at local midnight, and formatted from there: the same three\n * numbers in every zone.\n *\n * The pickers of `vacancies.form` do the same by themselves: `valueFormat: \"yyyy-MM-dd\"` parses\n * and formats in the browser's own zone, so a day goes in and comes out unchanged.\n */\n\nconst DAY = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/** The day as a local date, or `null` for anything that is not a `YYYY-MM-DD`. */\nexport function dayOf(value: string | null | undefined): Date | null {\n const parts = typeof value === 'string' ? DAY.exec(value.slice(0, 10)) : null\n\n if (parts === null) return null\n\n const [, year, month, day] = parts\n const at = new Date(Number(year), Number(month) - 1, Number(day))\n\n return Number.isNaN(at.getTime()) ? null : at\n}\n\n/**\n * The day in the panel's language — \"30 Nov\", and with the year when it is not this one. Empty\n * for no day at all.\n */\nexport function formatDay(\n value: string | null | undefined,\n locale: string,\n now = new Date(),\n): string {\n const at = dayOf(value)\n\n if (at === null) return ''\n\n return new Intl.DateTimeFormat(locale, {\n day: 'numeric',\n month: 'short',\n ...(at.getFullYear() === now.getFullYear() ? {} : { year: 'numeric' }),\n }).format(at)\n}\n\n/** The same day, in full — for a tip and for the head of the editor. */\nexport function longDay(value: string | null | undefined, locale: string): string {\n const at = dayOf(value)\n\n return at === null\n ? ''\n : new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'long', year: 'numeric' }).format(at)\n}\n"],"names":["DAY","dayOf","value","parts","year","month","day","at","formatDay","locale","now","longDay"],"mappings":"AAcA,MAAMA,IAAM;AAGL,SAASC,EAAMC,GAA+C;AACnE,QAAMC,IAAQ,OAAOD,KAAU,WAAWF,EAAI,KAAKE,EAAM,MAAM,GAAG,EAAE,CAAC,IAAI;AAEzE,MAAIC,MAAU,KAAM,QAAO;AAE3B,QAAM,GAAGC,GAAMC,GAAOC,CAAG,IAAIH,GACvBI,IAAK,IAAI,KAAK,OAAOH,CAAI,GAAG,OAAOC,CAAK,IAAI,GAAG,OAAOC,CAAG,CAAC;AAEhE,SAAO,OAAO,MAAMC,EAAG,QAAA,CAAS,IAAI,OAAOA;AAC7C;AAMO,SAASC,EACdN,GACAO,GACAC,IAAM,oBAAI,QACF;AACR,QAAMH,IAAKN,EAAMC,CAAK;AAEtB,SAAIK,MAAO,OAAa,KAEjB,IAAI,KAAK,eAAeE,GAAQ;AAAA,IACrC,KAAK;AAAA,IACL,OAAO;AAAA,IACP,GAAIF,EAAG,YAAA,MAAkBG,EAAI,YAAA,IAAgB,CAAA,IAAK,EAAE,MAAM,UAAA;AAAA,EAAU,CACrE,EAAE,OAAOH,CAAE;AACd;AAGO,SAASI,EAAQT,GAAkCO,GAAwB;AAChF,QAAMF,IAAKN,EAAMC,CAAK;AAEtB,SAAOK,MAAO,OACV,KACA,IAAI,KAAK,eAAeE,GAAQ,EAAE,KAAK,WAAW,OAAO,QAAQ,MAAM,WAAW,EAAE,OAAOF,CAAE;AACnG;"}