@webx-ui/module-team 0.1.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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/MemberPane.d.ts +21 -0
- package/dist/TeamPage.d.ts +20 -0
- package/dist/api.d.ts +22 -0
- package/dist/i18n.d.ts +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +555 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +7 -0
- package/dist/module.d.ts +12 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +54 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webx-ui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @webx-ui/module-team
|
|
2
|
+
|
|
3
|
+
The front end of the team section of the [WebX UI](https://github.com/webx-ui/webx-ui) admin
|
|
4
|
+
panel: the people of the organisation — a photo, a name, a job title, a few lines of text, their
|
|
5
|
+
social links and, when the site has services, the services they provide.
|
|
6
|
+
|
|
7
|
+
The other half is the Composer package `webx-ui/module-team`, which owns the people, their order,
|
|
8
|
+
the Team block, `team()` for templates and the API. A section appears in the panel when both halves
|
|
9
|
+
are installed.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @webx-ui/module-team
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createAdmin } from '@webx-ui/module-admin'
|
|
19
|
+
import { team } from '@webx-ui/module-team'
|
|
20
|
+
import '@webx-ui/module-team/style.css'
|
|
21
|
+
|
|
22
|
+
createAdmin({
|
|
23
|
+
basePath: '/cms',
|
|
24
|
+
modules: [team()],
|
|
25
|
+
}).mount()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`team()` is one section and one entry of the menu, at the top level — the team has no categories,
|
|
29
|
+
so there is nothing to group it with. `team({ path: '/staff' })` puts it somewhere else inside the
|
|
30
|
+
panel.
|
|
31
|
+
|
|
32
|
+
On the server, the block type the module offers is installed once:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
php artisan webx:blocks:offered --install --module=team
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## The team
|
|
39
|
+
|
|
40
|
+
A list and the form of one person side by side, without pages: the list is where people are put
|
|
41
|
+
in order — the one order every team block on the site shows — and a drag cannot cross a page
|
|
42
|
+
boundary. The open person is in the address (`?member=12`), so a link to them is a link to the list
|
|
43
|
+
around them too. **New person** is a row that opens an empty form; the person is created by its
|
|
44
|
+
first save. On a phone the form slides over the list and draws its own «Back».
|
|
45
|
+
|
|
46
|
+
A row shows the photo (or the initials), the name, the job title and a mark when the person is not
|
|
47
|
+
published. While a search narrows the list, the grips disappear. The bin lists who was deleted,
|
|
48
|
+
newest first, and brings them back to their place.
|
|
49
|
+
|
|
50
|
+
The form is the `team.form` screen, described in JSON on the server: the photo from the library,
|
|
51
|
+
the name, the job title and the text in every language of the site, the social links (a network
|
|
52
|
+
from the site's list and an address, in the order the site prints them), the services — only on a
|
|
53
|
+
site that has them — **Published** and a card for the project's own fields. Save with the button or
|
|
54
|
+
`Ctrl+S`; leaving with unsaved changes asks first. A person has no draft: a save is on the site.
|
|
55
|
+
|
|
56
|
+
## Words
|
|
57
|
+
|
|
58
|
+
The panel's words come from the server (`webx-team::*`, ten languages). `teamMessages` is the
|
|
59
|
+
English floor the section falls back on while they load, or when a key is missing.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MemberSummary } from './types';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** `null` — a person not written yet. */
|
|
4
|
+
id: number | null;
|
|
5
|
+
/** False in the drawer a narrow screen opens this in. */
|
|
6
|
+
inline?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
9
|
+
saved: (member: MemberSummary) => any;
|
|
10
|
+
back: () => any;
|
|
11
|
+
created: (member: MemberSummary) => any;
|
|
12
|
+
removed: (id: number) => any;
|
|
13
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
|
+
onSaved?: ((member: MemberSummary) => any) | undefined;
|
|
15
|
+
onBack?: (() => any) | undefined;
|
|
16
|
+
onCreated?: ((member: MemberSummary) => any) | undefined;
|
|
17
|
+
onRemoved?: ((id: number) => any) | undefined;
|
|
18
|
+
}>, {
|
|
19
|
+
inline: boolean;
|
|
20
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The team: everybody on the left, the person being written on the right (§5.6, decision 11).
|
|
3
|
+
*
|
|
4
|
+
* The shape of the reviews without their categories: a person is typed in from a list sent by
|
|
5
|
+
* the client in a minute, and the next one is usually the one under it. On a phone the form is
|
|
6
|
+
* the whole screen, in the drawer `WxListDetail` raises, and draws its own way back.
|
|
7
|
+
*
|
|
8
|
+
* One order, the general one (decision 5): a drag moves it; a search or the bin shows a
|
|
9
|
+
* selection, so the grips go away and the line under the list says why.
|
|
10
|
+
*
|
|
11
|
+
* Everything the screen is looking at lives in the address — the view, the search and the open
|
|
12
|
+
* person — so a link to a person is a link to them in their list.
|
|
13
|
+
*/
|
|
14
|
+
type __VLS_Props = {
|
|
15
|
+
base?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
18
|
+
base: string;
|
|
19
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
|
|
20
|
+
export default _default;
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AdminContext } from '@webx-ui/module-admin';
|
|
2
|
+
import { ScreenModel } from '@webx-ui/schema';
|
|
3
|
+
import { MemberDetail, MemberQuery, MemberRow, MembersList } from './types';
|
|
4
|
+
export interface TeamApi {
|
|
5
|
+
list(query?: MemberQuery): Promise<MembersList>;
|
|
6
|
+
get(id: number): Promise<MemberDetail>;
|
|
7
|
+
/**
|
|
8
|
+
* A new person out of the values of their form, in one request: the form is filled in first
|
|
9
|
+
* and the record made on the first save, so nobody leaves an empty person behind.
|
|
10
|
+
*/
|
|
11
|
+
create(values: ScreenModel): Promise<MemberDetail>;
|
|
12
|
+
/** Refused with a 422 under the name of the field — `socials.<n>.url` for a link. */
|
|
13
|
+
save(id: number, values: ScreenModel): Promise<MemberDetail>;
|
|
14
|
+
remove(id: number): Promise<void>;
|
|
15
|
+
restore(id: number): Promise<MemberRow>;
|
|
16
|
+
/** The one order there is (decision 5). */
|
|
17
|
+
reorder(ids: number[]): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/** The path of the section under the panel's API, and of its order. */
|
|
20
|
+
export declare const TEAM_API = "team";
|
|
21
|
+
/** Everything under `/team`, below the panel's API path (§5.7). */
|
|
22
|
+
export declare function createTeamApi(admin: AdminContext): TeamApi;
|
package/dist/i18n.d.ts
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { team, type TeamOptions } from './module';
|
|
2
|
+
export { createTeamApi, TEAM_API, type TeamApi } from './api';
|
|
3
|
+
export { teamMessages } from './messages';
|
|
4
|
+
export { default as WxTeamPage } from './TeamPage';
|
|
5
|
+
export { default as WxMemberPane } from './MemberPane';
|
|
6
|
+
export type { MemberDetail, MemberQuery, MemberRow, MembersList, MemberSummary, SocialLink, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { defineComponent as ae, ref as S, computed as v, onMounted as me, onBeforeUnmount as ne, openBlock as y, createElementBlock as A, createBlock as N, unref as n, Fragment as le, createVNode as b, createSlots as ve, withCtx as p, createTextVNode as E, toDisplayString as L, createCommentVNode as V, watch as Y, createElementVNode as K, normalizeClass as F, resolveDynamicComponent as be } from "vue";
|
|
2
|
+
import { onBeforeRouteUpdate as pe, onBeforeRouteLeave as he, useRoute as fe, useRouter as ye } from "vue-router";
|
|
3
|
+
import { useLocales as we, localizedValue as ge, WxSkeleton as re, WxBadge as oe, WxActionBar as xe, WxButton as _e, toast as x, confirm as G, WxCard as ke, WxListDetail as Te, WxEmpty as Z, WxInput as Se, WxIcon as ee, WxSortableList as We, WxAvatar as Ne, WxAlert as qe } from "@webx-ui/core";
|
|
4
|
+
import { reorderItems as Be, useAdmin as Q, useTranslate as se, useErrorText as ie, WxScreenHead as $e, WxScreen as Ce, WxSaveState as Pe, useItemOrder as Ve, WxListScreen as Ae, WxRowMenu as Le } from "@webx-ui/module-admin";
|
|
5
|
+
const H = "team";
|
|
6
|
+
function ue(i) {
|
|
7
|
+
const u = `${i.apiPath}/${H}`, s = (t) => t.data;
|
|
8
|
+
return {
|
|
9
|
+
list: (t = {}) => {
|
|
10
|
+
const c = new URLSearchParams();
|
|
11
|
+
return t.search && c.set("search", t.search), t.trashed && c.set("trashed", "1"), i.http.get(c.size > 0 ? `${u}?${c}` : u);
|
|
12
|
+
},
|
|
13
|
+
get: (t) => i.http.get(`${u}/${t}`).then(s),
|
|
14
|
+
create: (t) => i.http.post(u, { values: t }).then(s),
|
|
15
|
+
save: (t, c) => i.http.put(`${u}/${t}`, { values: c }).then(s),
|
|
16
|
+
remove: (t) => i.http.delete(`${u}/${t}`).then(() => {
|
|
17
|
+
}),
|
|
18
|
+
// The resource of the list line, and not the form's pair: a restored person stays closed.
|
|
19
|
+
restore: (t) => i.http.post(`${u}/${t}/restore`, {}).then(s),
|
|
20
|
+
reorder: (t) => Be(i, H, t, null)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const Me = {
|
|
24
|
+
module: {
|
|
25
|
+
team: "Team"
|
|
26
|
+
},
|
|
27
|
+
member: {
|
|
28
|
+
new: "New person",
|
|
29
|
+
untitled: "Untitled",
|
|
30
|
+
search: "Search the team",
|
|
31
|
+
empty: "Nobody on the team yet.",
|
|
32
|
+
"empty-help": "A person reaches the site in a team block, on any page.",
|
|
33
|
+
"empty-search": "Nothing matches that.",
|
|
34
|
+
"empty-bin": "The bin is empty.",
|
|
35
|
+
all: "All",
|
|
36
|
+
trashed: "Bin",
|
|
37
|
+
"not-published": "Not published",
|
|
38
|
+
"no-job-title": "No job title",
|
|
39
|
+
order: "Drag to change the order on the site.",
|
|
40
|
+
choose: "Choose a person",
|
|
41
|
+
"choose-help": "Or add a new one: the first line of the list.",
|
|
42
|
+
save: "Save",
|
|
43
|
+
saved: "Saved.",
|
|
44
|
+
"save-failed": "Not saved.",
|
|
45
|
+
cancel: "Cancel",
|
|
46
|
+
delete: "Delete",
|
|
47
|
+
"delete-title": "Delete “:name” from the team?",
|
|
48
|
+
"delete-text": "They go to the bin and leave every block on the site. Restored, they come back to their place.",
|
|
49
|
+
deleted: "Moved to the bin.",
|
|
50
|
+
restore: "Restore",
|
|
51
|
+
restored: "Restored.",
|
|
52
|
+
"reorder-failed": "The new order was not saved.",
|
|
53
|
+
"leave-title": "Leave without saving?",
|
|
54
|
+
"leave-text": "What you wrote about this person is not on the server.",
|
|
55
|
+
leave: "Leave",
|
|
56
|
+
back: "Back to the list"
|
|
57
|
+
},
|
|
58
|
+
screen: {
|
|
59
|
+
person: "Person",
|
|
60
|
+
photo: "Photo",
|
|
61
|
+
"photo-help": "Without one, the site shows the initials of the name.",
|
|
62
|
+
name: "Name",
|
|
63
|
+
"name-help": "Required in the default language. Not written in another, the default one is shown.",
|
|
64
|
+
"job-title": "Job title",
|
|
65
|
+
"job-title-help": "Printed under the name. Not written in a language, the default one is shown.",
|
|
66
|
+
about: "About",
|
|
67
|
+
text: "Text",
|
|
68
|
+
"text-help": "Printed only in the language it is written in. The person is shown either way.",
|
|
69
|
+
links: "Links",
|
|
70
|
+
socials: "Social networks",
|
|
71
|
+
"socials-help": "In the order the site prints them. A row left empty is dropped when you save.",
|
|
72
|
+
"socials-add": "Add a link",
|
|
73
|
+
"socials-empty": "No links.",
|
|
74
|
+
network: "Network",
|
|
75
|
+
"network-placeholder": "Choose a network",
|
|
76
|
+
url: "Address",
|
|
77
|
+
services: "Services",
|
|
78
|
+
"services-help": "What the person provides: a team block on a service’s page can show only them.",
|
|
79
|
+
settings: "Settings",
|
|
80
|
+
published: "Published",
|
|
81
|
+
"published-help": "On the site in every team block, from the moment it is saved."
|
|
82
|
+
}
|
|
83
|
+
}, te = /* @__PURE__ */ new WeakSet();
|
|
84
|
+
function de() {
|
|
85
|
+
try {
|
|
86
|
+
const { i18n: i } = Q();
|
|
87
|
+
te.has(i) || (i.defaults("webx-team", Me), te.add(i));
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const Re = /* @__PURE__ */ ae({
|
|
92
|
+
name: "WxMemberPane",
|
|
93
|
+
__name: "MemberPane",
|
|
94
|
+
props: {
|
|
95
|
+
id: {},
|
|
96
|
+
inline: { type: Boolean, default: !0 }
|
|
97
|
+
},
|
|
98
|
+
emits: ["back", "saved", "created", "removed"],
|
|
99
|
+
setup(i, { emit: u }) {
|
|
100
|
+
const s = i, t = u, c = Q(), l = ue(c), $ = we();
|
|
101
|
+
de();
|
|
102
|
+
const o = se("webx-team"), W = ie(), w = S(null), _ = S({}), k = S(""), h = S(s.id !== null), g = S(!1), q = S({}), f = v(() => c.can("team.manage")), M = v(() => JSON.stringify(_.value)), B = v(() => k.value !== "" && M.value !== k.value), R = v(() => g.value ? "saving" : B.value || s.id === null ? "unsaved" : "saved"), z = v(() => {
|
|
103
|
+
const a = _.value.name, d = s.id === null ? o("member.new") : w.value?.name ?? "";
|
|
104
|
+
return ge(a, $.active.value, d) || o("member.untitled");
|
|
105
|
+
});
|
|
106
|
+
function D(a) {
|
|
107
|
+
w.value = a.member, _.value = a.values, k.value = JSON.stringify(a.values);
|
|
108
|
+
}
|
|
109
|
+
if (s.id === null) {
|
|
110
|
+
const a = {
|
|
111
|
+
name: {},
|
|
112
|
+
job_title: {},
|
|
113
|
+
text: {},
|
|
114
|
+
photo: null,
|
|
115
|
+
socials: [],
|
|
116
|
+
published: !1
|
|
117
|
+
};
|
|
118
|
+
_.value = a, k.value = JSON.stringify(a);
|
|
119
|
+
}
|
|
120
|
+
async function T() {
|
|
121
|
+
if (s.id !== null) {
|
|
122
|
+
h.value = !0;
|
|
123
|
+
try {
|
|
124
|
+
D(await l.get(s.id));
|
|
125
|
+
} catch (a) {
|
|
126
|
+
x.danger(W(a)), t("back");
|
|
127
|
+
} finally {
|
|
128
|
+
h.value = !1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function C() {
|
|
133
|
+
if (!(g.value || !f.value)) {
|
|
134
|
+
g.value = !0, q.value = {};
|
|
135
|
+
try {
|
|
136
|
+
const a = s.id === null, d = a ? await l.create(_.value) : await l.save(s.id, _.value);
|
|
137
|
+
D(d), x.success(o("member.saved")), t(a ? "created" : "saved", d.member);
|
|
138
|
+
} catch (a) {
|
|
139
|
+
const d = a.body;
|
|
140
|
+
d?.errors ? (q.value = d.errors, x.danger(o("member.save-failed"))) : x.danger(W(a));
|
|
141
|
+
} finally {
|
|
142
|
+
g.value = !1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function U() {
|
|
147
|
+
const a = w.value;
|
|
148
|
+
if (!(!a || !await G({
|
|
149
|
+
title: o("member.delete-title", { name: a.name }),
|
|
150
|
+
message: o("member.delete-text"),
|
|
151
|
+
confirmText: o("member.delete"),
|
|
152
|
+
cancelText: o("member.cancel"),
|
|
153
|
+
tone: "danger"
|
|
154
|
+
})))
|
|
155
|
+
try {
|
|
156
|
+
await l.remove(a.id), x.success(o("member.deleted")), k.value = M.value, t("removed", a.id);
|
|
157
|
+
} catch (P) {
|
|
158
|
+
x.danger(W(P));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function j(a) {
|
|
162
|
+
B.value && a.preventDefault();
|
|
163
|
+
}
|
|
164
|
+
function O(a) {
|
|
165
|
+
(a.ctrlKey || a.metaKey) && a.key.toLowerCase() === "s" && (a.preventDefault(), (B.value || s.id === null) && C());
|
|
166
|
+
}
|
|
167
|
+
me(() => {
|
|
168
|
+
T(), window.addEventListener("beforeunload", j);
|
|
169
|
+
}), ne(() => {
|
|
170
|
+
window.removeEventListener("beforeunload", j);
|
|
171
|
+
});
|
|
172
|
+
async function I() {
|
|
173
|
+
return !B.value || !f.value ? !0 : await G({
|
|
174
|
+
title: o("member.leave-title"),
|
|
175
|
+
message: o("member.leave-text"),
|
|
176
|
+
confirmText: o("member.leave"),
|
|
177
|
+
cancelText: o("member.cancel"),
|
|
178
|
+
tone: "danger"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
pe(
|
|
182
|
+
async (a, d) => a.query.member === d.query.member ? !0 : await I()
|
|
183
|
+
), he(I);
|
|
184
|
+
const J = v(
|
|
185
|
+
() => f.value && w.value !== null ? [
|
|
186
|
+
{
|
|
187
|
+
key: "delete",
|
|
188
|
+
label: o("member.delete"),
|
|
189
|
+
icon: "trash",
|
|
190
|
+
danger: !0,
|
|
191
|
+
menu: !0,
|
|
192
|
+
run: () => {
|
|
193
|
+
U();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
] : []
|
|
197
|
+
);
|
|
198
|
+
return (a, d) => (y(), A("div", {
|
|
199
|
+
class: "wx-member",
|
|
200
|
+
onKeydown: O
|
|
201
|
+
}, [
|
|
202
|
+
h.value ? (y(), N(n(re), {
|
|
203
|
+
key: 0,
|
|
204
|
+
class: "wx-member__ghost",
|
|
205
|
+
title: "",
|
|
206
|
+
rows: 6
|
|
207
|
+
})) : (y(), A(le, { key: 1 }, [
|
|
208
|
+
b(n($e), {
|
|
209
|
+
level: 3,
|
|
210
|
+
back: !s.inline,
|
|
211
|
+
"back-label": n(o)("member.back"),
|
|
212
|
+
title: z.value,
|
|
213
|
+
actions: J.value,
|
|
214
|
+
"collapse-below": 480,
|
|
215
|
+
onBack: d[0] || (d[0] = (P) => t("back"))
|
|
216
|
+
}, ve({ _: 2 }, [
|
|
217
|
+
w.value && !w.value.published ? {
|
|
218
|
+
name: "title-after",
|
|
219
|
+
fn: p(() => [
|
|
220
|
+
b(n(oe), { dot: "" }, {
|
|
221
|
+
default: p(() => [
|
|
222
|
+
E(L(n(o)("member.not-published")), 1)
|
|
223
|
+
]),
|
|
224
|
+
_: 1
|
|
225
|
+
})
|
|
226
|
+
]),
|
|
227
|
+
key: "0"
|
|
228
|
+
} : void 0
|
|
229
|
+
]), 1032, ["back", "back-label", "title", "actions"]),
|
|
230
|
+
b(n(Ce), {
|
|
231
|
+
modelValue: _.value,
|
|
232
|
+
"onUpdate:modelValue": d[1] || (d[1] = (P) => _.value = P),
|
|
233
|
+
name: "team.form",
|
|
234
|
+
errors: q.value,
|
|
235
|
+
disabled: !f.value || g.value
|
|
236
|
+
}, null, 8, ["modelValue", "errors", "disabled"]),
|
|
237
|
+
f.value ? (y(), N(n(xe), { key: 0 }, {
|
|
238
|
+
state: p(() => [
|
|
239
|
+
b(n(Pe), { state: R.value }, null, 8, ["state"])
|
|
240
|
+
]),
|
|
241
|
+
default: p(() => [
|
|
242
|
+
b(n(_e), {
|
|
243
|
+
type: "primary",
|
|
244
|
+
loading: g.value,
|
|
245
|
+
disabled: !B.value && s.id !== null,
|
|
246
|
+
onClick: C
|
|
247
|
+
}, {
|
|
248
|
+
default: p(() => [
|
|
249
|
+
E(L(n(o)("member.save")), 1)
|
|
250
|
+
]),
|
|
251
|
+
_: 1
|
|
252
|
+
}, 8, ["loading", "disabled"])
|
|
253
|
+
]),
|
|
254
|
+
_: 1
|
|
255
|
+
})) : V("", !0)
|
|
256
|
+
], 64))
|
|
257
|
+
], 32));
|
|
258
|
+
}
|
|
259
|
+
}), ce = (i, u) => {
|
|
260
|
+
const s = i.__vccOpts || i;
|
|
261
|
+
for (const [t, c] of u)
|
|
262
|
+
s[t] = c;
|
|
263
|
+
return s;
|
|
264
|
+
}, Ue = /* @__PURE__ */ ce(Re, [["__scopeId", "data-v-fa99d370"]]), je = { class: "wx-team__bar" }, ze = { class: "wx-member-row__body" }, De = { class: "wx-member-row__name" }, Ie = {
|
|
265
|
+
key: 0,
|
|
266
|
+
class: "wx-member-row__job"
|
|
267
|
+
}, Ee = {
|
|
268
|
+
key: 1,
|
|
269
|
+
class: "wx-member-row__facts"
|
|
270
|
+
}, Oe = /* @__PURE__ */ ae({
|
|
271
|
+
__name: "TeamPage",
|
|
272
|
+
props: {
|
|
273
|
+
base: { default: "/team" }
|
|
274
|
+
},
|
|
275
|
+
setup(i) {
|
|
276
|
+
const u = Q(), s = ue(u), t = fe(), c = ye();
|
|
277
|
+
de();
|
|
278
|
+
const l = se("webx-team"), $ = ie(), o = S([]), W = S(!0), w = v(() => u.can("team.manage")), _ = v(
|
|
279
|
+
() => u.state.manifest?.modules.find((e) => e.id === "team")?.title ?? l("module.team")
|
|
280
|
+
), k = v({
|
|
281
|
+
get: () => t.query.view === "trashed" ? "trashed" : "",
|
|
282
|
+
set: (e) => {
|
|
283
|
+
const m = {
|
|
284
|
+
...t.query,
|
|
285
|
+
view: e === "" ? void 0 : String(e)
|
|
286
|
+
};
|
|
287
|
+
delete m.member, c.replace({ query: m });
|
|
288
|
+
}
|
|
289
|
+
}), h = v(() => k.value === "trashed"), g = v(() => typeof t.query.q == "string" ? t.query.q : ""), q = S(g.value), f = v(() => {
|
|
290
|
+
const e = t.query.member;
|
|
291
|
+
return e === "new" ? w.value ? "new" : null : typeof e == "string" && e !== "" && Number(e) || null;
|
|
292
|
+
}), M = v({
|
|
293
|
+
get: () => f.value !== null,
|
|
294
|
+
set: (e) => {
|
|
295
|
+
e || j();
|
|
296
|
+
}
|
|
297
|
+
}), B = v(() => [
|
|
298
|
+
{ value: "", label: l("member.all") },
|
|
299
|
+
{ value: "trashed", label: l("member.trashed"), icon: "trash" }
|
|
300
|
+
]), R = Ve(H, () => ({ q: g.value, filtered: h.value })), z = v(() => w.value && R.sortable.value), D = v(() => g.value !== "" ? l("member.empty-search") : h.value ? l("member.empty-bin") : l("member.empty"));
|
|
301
|
+
async function T() {
|
|
302
|
+
W.value = !0;
|
|
303
|
+
try {
|
|
304
|
+
const e = await s.list({ search: g.value, trashed: h.value });
|
|
305
|
+
o.value = e.data;
|
|
306
|
+
} catch (e) {
|
|
307
|
+
x.danger($(e));
|
|
308
|
+
} finally {
|
|
309
|
+
W.value = !1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
Y(
|
|
313
|
+
() => [k.value, g.value],
|
|
314
|
+
() => {
|
|
315
|
+
T();
|
|
316
|
+
}
|
|
317
|
+
), T();
|
|
318
|
+
let C;
|
|
319
|
+
Y(q, (e) => {
|
|
320
|
+
clearTimeout(C), C = setTimeout(() => {
|
|
321
|
+
c.replace({ query: { ...t.query, q: e.trim() === "" ? void 0 : e } });
|
|
322
|
+
}, 300);
|
|
323
|
+
}), ne(() => clearTimeout(C));
|
|
324
|
+
function U(e) {
|
|
325
|
+
c.replace({ query: { ...t.query, member: String(e) } });
|
|
326
|
+
}
|
|
327
|
+
function j() {
|
|
328
|
+
const e = { ...t.query };
|
|
329
|
+
delete e.member, c.replace({ query: e });
|
|
330
|
+
}
|
|
331
|
+
function O(e) {
|
|
332
|
+
U(e.id), T();
|
|
333
|
+
}
|
|
334
|
+
async function I() {
|
|
335
|
+
try {
|
|
336
|
+
await R.move(o.value.map((e) => e.id));
|
|
337
|
+
} catch (e) {
|
|
338
|
+
x.danger($(e, l("member.reorder-failed"))), await T();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function J(e) {
|
|
342
|
+
return w.value ? h.value ? [
|
|
343
|
+
{
|
|
344
|
+
key: "restore",
|
|
345
|
+
icon: "refresh",
|
|
346
|
+
label: l("member.restore"),
|
|
347
|
+
run: () => {
|
|
348
|
+
P(e);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
] : [
|
|
352
|
+
{
|
|
353
|
+
key: "delete",
|
|
354
|
+
icon: "trash",
|
|
355
|
+
label: l("member.delete"),
|
|
356
|
+
danger: !0,
|
|
357
|
+
run: () => {
|
|
358
|
+
a(e);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
] : [];
|
|
362
|
+
}
|
|
363
|
+
async function a(e) {
|
|
364
|
+
if (await G({
|
|
365
|
+
title: l("member.delete-title", { name: e.name }),
|
|
366
|
+
message: l("member.delete-text"),
|
|
367
|
+
confirmText: l("member.delete"),
|
|
368
|
+
cancelText: l("member.cancel"),
|
|
369
|
+
tone: "danger"
|
|
370
|
+
}))
|
|
371
|
+
try {
|
|
372
|
+
await s.remove(e.id), x.success(l("member.deleted")), d(e.id);
|
|
373
|
+
} catch (r) {
|
|
374
|
+
x.danger($(r));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function d(e) {
|
|
378
|
+
f.value === e && j(), T();
|
|
379
|
+
}
|
|
380
|
+
async function P(e) {
|
|
381
|
+
try {
|
|
382
|
+
await s.restore(e.id), x.success(l("member.restored"));
|
|
383
|
+
} catch (m) {
|
|
384
|
+
x.danger($(m));
|
|
385
|
+
} finally {
|
|
386
|
+
await T();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return (e, m) => (y(), N(n(Ae), {
|
|
390
|
+
view: k.value,
|
|
391
|
+
"onUpdate:view": m[4] || (m[4] = (r) => k.value = r),
|
|
392
|
+
title: _.value,
|
|
393
|
+
views: B.value,
|
|
394
|
+
card: !1
|
|
395
|
+
}, {
|
|
396
|
+
default: p(() => [
|
|
397
|
+
b(n(ke), {
|
|
398
|
+
class: "wx-team",
|
|
399
|
+
padding: "none"
|
|
400
|
+
}, {
|
|
401
|
+
default: p(() => [
|
|
402
|
+
b(n(Te), {
|
|
403
|
+
open: M.value,
|
|
404
|
+
"onUpdate:open": m[3] || (m[3] = (r) => M.value = r),
|
|
405
|
+
class: "wx-team__panes",
|
|
406
|
+
"list-width": 360,
|
|
407
|
+
"detail-min": 460,
|
|
408
|
+
"detail-label": n(l)("module.team")
|
|
409
|
+
}, {
|
|
410
|
+
list: p(() => [
|
|
411
|
+
K("div", je, [
|
|
412
|
+
b(n(Se), {
|
|
413
|
+
modelValue: q.value,
|
|
414
|
+
"onUpdate:modelValue": m[0] || (m[0] = (r) => q.value = r),
|
|
415
|
+
class: "wx-team__search",
|
|
416
|
+
size: "sm",
|
|
417
|
+
clearable: "",
|
|
418
|
+
placeholder: n(l)("member.search"),
|
|
419
|
+
"aria-label": n(l)("member.search")
|
|
420
|
+
}, {
|
|
421
|
+
prefix: p(() => [
|
|
422
|
+
b(n(ee), {
|
|
423
|
+
name: "search",
|
|
424
|
+
size: "sm"
|
|
425
|
+
})
|
|
426
|
+
]),
|
|
427
|
+
_: 1
|
|
428
|
+
}, 8, ["modelValue", "placeholder", "aria-label"])
|
|
429
|
+
]),
|
|
430
|
+
w.value && !h.value ? (y(), A("button", {
|
|
431
|
+
key: 0,
|
|
432
|
+
type: "button",
|
|
433
|
+
class: F(["wx-team__new", { "is-current": f.value === "new" }]),
|
|
434
|
+
onClick: m[1] || (m[1] = (r) => U("new"))
|
|
435
|
+
}, [
|
|
436
|
+
b(n(ee), {
|
|
437
|
+
name: "plus",
|
|
438
|
+
size: "sm"
|
|
439
|
+
}),
|
|
440
|
+
E(" " + L(n(l)("member.new")), 1)
|
|
441
|
+
], 2)) : V("", !0),
|
|
442
|
+
W.value && o.value.length === 0 ? (y(), N(n(re), {
|
|
443
|
+
key: 1,
|
|
444
|
+
class: "wx-team__loading",
|
|
445
|
+
rows: 5
|
|
446
|
+
})) : o.value.length === 0 ? (y(), N(n(Z), {
|
|
447
|
+
key: 2,
|
|
448
|
+
title: D.value,
|
|
449
|
+
description: !h.value && g.value === "" ? n(l)("member.empty-help") : void 0
|
|
450
|
+
}, null, 8, ["title", "description"])) : (y(), A(le, { key: 3 }, [
|
|
451
|
+
b(n(We), {
|
|
452
|
+
modelValue: o.value,
|
|
453
|
+
"onUpdate:modelValue": m[2] || (m[2] = (r) => o.value = r),
|
|
454
|
+
class: F(["wx-team__list", { "is-loading": W.value }]),
|
|
455
|
+
plain: "",
|
|
456
|
+
"item-key": "id",
|
|
457
|
+
handle: z.value ? "grip" : "row",
|
|
458
|
+
"item-label": (r) => r.name,
|
|
459
|
+
disabled: !z.value,
|
|
460
|
+
onMove: I
|
|
461
|
+
}, {
|
|
462
|
+
default: p(({ item: r }) => [
|
|
463
|
+
(y(), N(be(h.value ? "div" : "button"), {
|
|
464
|
+
type: h.value ? void 0 : "button",
|
|
465
|
+
class: F(["wx-member-row", { "is-current": f.value === r.id }]),
|
|
466
|
+
onClick: (X) => h.value ? void 0 : U(r.id)
|
|
467
|
+
}, {
|
|
468
|
+
default: p(() => [
|
|
469
|
+
b(n(Ne), {
|
|
470
|
+
class: "wx-member-row__photo",
|
|
471
|
+
src: r.photo?.thumb ?? void 0,
|
|
472
|
+
name: r.name,
|
|
473
|
+
size: "md"
|
|
474
|
+
}, null, 8, ["src", "name"]),
|
|
475
|
+
K("span", ze, [
|
|
476
|
+
K("span", De, L(r.name), 1),
|
|
477
|
+
r.job_title ? (y(), A("span", Ie, L(r.job_title), 1)) : V("", !0),
|
|
478
|
+
r.published ? V("", !0) : (y(), A("span", Ee, [
|
|
479
|
+
b(n(oe), {
|
|
480
|
+
dot: "",
|
|
481
|
+
size: "sm"
|
|
482
|
+
}, {
|
|
483
|
+
default: p(() => [
|
|
484
|
+
E(L(n(l)("member.not-published")), 1)
|
|
485
|
+
]),
|
|
486
|
+
_: 1
|
|
487
|
+
})
|
|
488
|
+
]))
|
|
489
|
+
])
|
|
490
|
+
]),
|
|
491
|
+
_: 2
|
|
492
|
+
}, 1032, ["type", "class", "onClick"]))
|
|
493
|
+
]),
|
|
494
|
+
actions: p(({ item: r }) => [
|
|
495
|
+
b(n(Le), {
|
|
496
|
+
actions: J(r),
|
|
497
|
+
label: r.name
|
|
498
|
+
}, null, 8, ["actions", "label"])
|
|
499
|
+
]),
|
|
500
|
+
_: 1
|
|
501
|
+
}, 8, ["modelValue", "class", "handle", "item-label", "disabled"]),
|
|
502
|
+
w.value && !h.value ? (y(), N(n(qe), {
|
|
503
|
+
key: 0,
|
|
504
|
+
class: "wx-team__note",
|
|
505
|
+
type: "info",
|
|
506
|
+
description: n(R).hint.value
|
|
507
|
+
}, null, 8, ["description"])) : V("", !0)
|
|
508
|
+
], 64))
|
|
509
|
+
]),
|
|
510
|
+
detail: p(({ inline: r, back: X }) => [
|
|
511
|
+
f.value !== null ? (y(), N(Ue, {
|
|
512
|
+
id: f.value === "new" ? null : f.value,
|
|
513
|
+
key: String(f.value),
|
|
514
|
+
inline: r,
|
|
515
|
+
onBack: X,
|
|
516
|
+
onSaved: T,
|
|
517
|
+
onCreated: O,
|
|
518
|
+
onRemoved: d
|
|
519
|
+
}, null, 8, ["id", "inline", "onBack"])) : V("", !0)
|
|
520
|
+
]),
|
|
521
|
+
empty: p(() => [
|
|
522
|
+
b(n(Z), {
|
|
523
|
+
title: n(l)("member.choose"),
|
|
524
|
+
description: w.value ? n(l)("member.choose-help") : void 0
|
|
525
|
+
}, null, 8, ["title", "description"])
|
|
526
|
+
]),
|
|
527
|
+
_: 1
|
|
528
|
+
}, 8, ["open", "detail-label"])
|
|
529
|
+
]),
|
|
530
|
+
_: 1
|
|
531
|
+
})
|
|
532
|
+
]),
|
|
533
|
+
_: 1
|
|
534
|
+
}, 8, ["view", "title", "views"]));
|
|
535
|
+
}
|
|
536
|
+
}), Je = /* @__PURE__ */ ce(Oe, [["__scopeId", "data-v-65fc44a2"]]);
|
|
537
|
+
function Qe(i = {}) {
|
|
538
|
+
const u = i.path ?? "/team";
|
|
539
|
+
return {
|
|
540
|
+
id: "team",
|
|
541
|
+
path: u,
|
|
542
|
+
// One route: the open person is in the address (`?member=`), beside the list they were
|
|
543
|
+
// opened from, so a link to them is a link to both.
|
|
544
|
+
routes: [{ path: u, name: "webx.team", component: Je, props: { base: u } }]
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
export {
|
|
548
|
+
H as TEAM_API,
|
|
549
|
+
Ue as WxMemberPane,
|
|
550
|
+
Je as WxTeamPage,
|
|
551
|
+
ue as createTeamApi,
|
|
552
|
+
Qe as team,
|
|
553
|
+
Me as teamMessages
|
|
554
|
+
};
|
|
555
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/api.ts","../src/messages.ts","../src/i18n.ts","../src/MemberPane.vue","../src/TeamPage.vue","../src/module.ts"],"sourcesContent":["import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport type { MemberDetail, MemberQuery, MemberRow, MembersList } from './types'\n\nexport interface TeamApi {\n list(query?: MemberQuery): Promise<MembersList>\n get(id: number): Promise<MemberDetail>\n /**\n * A new person out of the values of their form, in one request: the form is filled in first\n * and the record made on the first save, so nobody leaves an empty person behind.\n */\n create(values: ScreenModel): Promise<MemberDetail>\n /** Refused with a 422 under the name of the field — `socials.<n>.url` for a link. */\n save(id: number, values: ScreenModel): Promise<MemberDetail>\n remove(id: number): Promise<void>\n restore(id: number): Promise<MemberRow>\n /** The one order there is (decision 5). */\n reorder(ids: number[]): Promise<void>\n}\n\n/** The path of the section under the panel's API, and of its order. */\nexport const TEAM_API = 'team'\n\n/** Everything under `/team`, below the panel's API path (§5.7). */\nexport function createTeamApi(admin: AdminContext): TeamApi {\n const base = `${admin.apiPath}/${TEAM_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n if (query.search) search.set('search', query.search)\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<MembersList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: MemberDetail }>(`${base}/${id}`).then(data),\n create: (values) => admin.http.post<{ data: MemberDetail }>(base, { values }).then(data),\n save: (id, values) =>\n admin.http.put<{ data: MemberDetail }>(`${base}/${id}`, { values }).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n // The resource of the list line, and not the form's pair: a restored person stays closed.\n restore: (id) => admin.http.post<{ data: MemberRow }>(`${base}/${id}/restore`, {}).then(data),\n reorder: (ids) => reorderItems(admin, TEAM_API, ids, null),\n }\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-team::` and overrides these;\n * `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const teamMessages: Record<string, Messages> = {\n module: {\n team: 'Team',\n },\n member: {\n new: 'New person',\n untitled: 'Untitled',\n search: 'Search the team',\n empty: 'Nobody on the team yet.',\n 'empty-help': 'A person reaches the site in a team block, on any page.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n all: 'All',\n trashed: 'Bin',\n 'not-published': 'Not published',\n 'no-job-title': 'No job title',\n order: 'Drag to change the order on the site.',\n choose: 'Choose a person',\n 'choose-help': 'Or add a new one: the first line of the list.',\n save: 'Save',\n saved: 'Saved.',\n 'save-failed': 'Not saved.',\n cancel: 'Cancel',\n delete: 'Delete',\n 'delete-title': 'Delete “:name” from the team?',\n 'delete-text':\n 'They go to the bin and leave every block on the site. Restored, they come back to their place.',\n deleted: 'Moved to the bin.',\n restore: 'Restore',\n restored: 'Restored.',\n 'reorder-failed': 'The new order was not saved.',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'What you wrote about this person is not on the server.',\n leave: 'Leave',\n back: 'Back to the list',\n },\n screen: {\n person: 'Person',\n photo: 'Photo',\n 'photo-help': 'Without one, the site shows the initials of the name.',\n name: 'Name',\n 'name-help':\n 'Required in the default language. Not written in another, the default one is shown.',\n 'job-title': 'Job title',\n 'job-title-help':\n 'Printed under the name. Not written in a language, the default one is shown.',\n about: 'About',\n text: 'Text',\n 'text-help': 'Printed only in the language it is written in. The person is shown either way.',\n links: 'Links',\n socials: 'Social networks',\n 'socials-help': 'In the order the site prints them. A row left empty is dropped when you save.',\n 'socials-add': 'Add a link',\n 'socials-empty': 'No links.',\n network: 'Network',\n 'network-placeholder': 'Choose a network',\n url: 'Address',\n services: 'Services',\n 'services-help':\n 'What the person provides: a team block on a service’s page can show only them.',\n settings: 'Settings',\n published: 'Published',\n 'published-help': 'On the site in every team block, from the moment it is saved.',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { teamMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useTeamMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-team', teamMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { onBeforeRouteLeave, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxBadge,\n WxButton,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createTeamApi } from './api'\nimport { useTeamMessages } from './i18n'\nimport type { MemberDetail, MemberSummary } from './types'\n\n/**\n * One person: the pane beside the list, and the whole screen on a phone.\n *\n * The described screen `team.form` with a head over it and one button under it. One explicit\n * save, and no autosave: a person has no draft (decision 10), so what is saved is on the site at\n * once — the button is the moment somebody decides it is ready.\n *\n * A new person is a form first and a record on the first save: an empty row made by pressing\n * \"New person\" and walking away would be a nameless card on every team block for ever.\n *\n * The way back is drawn here and not by the pane around it: the drawer a narrow screen opens\n * this in has no close of its own on purpose (CLAUDE.md §4).\n */\ndefineOptions({ name: 'WxMemberPane' })\n\nconst props = withDefaults(\n defineProps<{\n /** `null` — a person not written yet. */\n id: number | null\n /** False in the drawer a narrow screen opens this in. */\n inline?: boolean\n }>(),\n { inline: true },\n)\n\nconst emit = defineEmits<{\n back: []\n /** The person was written; the list redraws their row. */\n saved: [member: MemberSummary]\n /** The first save of a new person: a record now, with an id to be opened by. */\n created: [member: MemberSummary]\n removed: [id: number]\n}>()\n\nconst context = useAdmin()\nconst api = createTeamApi(context)\nconst locales = useLocales()\nuseTeamMessages()\n\nconst t = useTranslate('webx-team')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst member = ref<MemberSummary | null>(null)\nconst values = ref<ScreenModel>({})\nconst snapshot = ref('')\n\nconst loading = ref(props.id !== null)\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst canManage = computed(() => context.can('team.manage'))\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 || props.id === null ? 'unsaved' : 'saved'\n})\n\n/** The name follows the field rather than the answer: an edit shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.name as LocalizedValue | string | null | undefined\n const fallback = props.id === null ? t('member.new') : (member.value?.name ?? '')\n\n return localizedValue(written, locales.active.value, fallback) || t('member.untitled')\n})\n\nfunction take(detail: MemberDetail): void {\n member.value = detail.member\n values.value = detail.values\n snapshot.value = JSON.stringify(detail.values)\n}\n\nif (props.id === null) {\n // What the server would answer for a person with nothing in them yet. No `services`: the field\n // is there only when the site has services (decision 3), and the screen fills its own default.\n const blank: ScreenModel = {\n name: {},\n job_title: {},\n text: {},\n photo: null,\n socials: [],\n published: false,\n }\n\n values.value = blank\n snapshot.value = JSON.stringify(blank)\n}\n\nasync function load(): Promise<void> {\n if (props.id === null) return\n\n loading.value = true\n\n try {\n take(await api.get(props.id))\n } catch (error) {\n toast.danger(message(error))\n emit('back')\n } finally {\n loading.value = false\n }\n}\n\nasync function save(): Promise<void> {\n if (saving.value || !canManage.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n const made = props.id === null\n const detail = made\n ? await api.create(values.value)\n : await api.save(props.id as number, values.value)\n\n take(detail)\n toast.success(t('member.saved'))\n if (made) emit('created', detail.member)\n else emit('saved', detail.member)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]> } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('member.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\nasync function remove(): Promise<void> {\n const record = member.value\n\n if (!record) return\n\n const agreed = await confirm({\n title: t('member.delete-title', { name: record.name }),\n message: t('member.delete-text'),\n confirmText: t('member.delete'),\n cancelText: t('member.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(record.id)\n toast.success(t('member.deleted'))\n // Nothing left to protect: the record is in the bin.\n snapshot.value = current.value\n emit('removed', record.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\n/* Ctrl+S saves, as it does everywhere else that has a save button. */\nfunction onKeydown(event: KeyboardEvent): void {\n if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {\n event.preventDefault()\n if (dirty.value || props.id === null) void save()\n }\n}\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\nasync function mayLeave(): Promise<boolean> {\n if (!dirty.value || !canManage.value) return true\n\n return await confirm({\n title: t('member.leave-title'),\n message: t('member.leave-text'),\n confirmText: t('member.leave'),\n cancelText: t('member.cancel'),\n tone: 'danger',\n })\n}\n\n/*\n * The list and this form are one route, so another person is an update of it rather than a way\n * out — and so is every keystroke in the search above the list, which goes into the address too.\n * Only a change of the person asks.\n */\nonBeforeRouteUpdate(async (to: RouteLocationNormalized, from: RouteLocationNormalized) =>\n to.query.member === from.query.member ? true : await mayLeave(),\n)\n\nonBeforeRouteLeave(mayLeave)\n\n/* What leads away from the person. On a narrow pane the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value && member.value !== null\n ? [\n {\n key: 'delete',\n label: t('member.delete'),\n icon: 'trash',\n danger: true,\n menu: true,\n run: () => void remove(),\n },\n ]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-member\" @keydown=\"onKeydown\">\n <wx-skeleton v-if=\"loading\" class=\"wx-member__ghost\" title :rows=\"6\" />\n\n <template v-else>\n <wx-screen-head\n :level=\"3\"\n :back=\"!props.inline\"\n :back-label=\"t('member.back')\"\n :title=\"title\"\n :actions=\"actions\"\n :collapse-below=\"480\"\n @back=\"emit('back')\"\n >\n <template v-if=\"member && !member.published\" #title-after>\n <wx-badge dot>{{ t('member.not-published') }}</wx-badge>\n </template>\n </wx-screen-head>\n\n <wx-screen\n v-model=\"values\"\n name=\"team.form\"\n :errors=\"errors\"\n :disabled=\"!canManage || saving\"\n />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button\n type=\"primary\"\n :loading=\"saving\"\n :disabled=\"!dirty && props.id !== null\"\n @click=\"save\"\n >\n {{ t('member.save') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n.wx-member {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-width: 0;\n padding: var(--wx-gap, var(--wx-space-16));\n container-type: inline-size;\n}\n\n.wx-member__ghost {\n max-width: 480px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n confirm,\n toast,\n WxAlert,\n WxAvatar,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\n WxSkeleton,\n WxSortableList,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useItemOrder,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n} from '@webx-ui/module-admin'\nimport { createTeamApi, TEAM_API } from './api'\nimport { useTeamMessages } from './i18n'\nimport MemberPane from './MemberPane.vue'\nimport type { MemberRow, MemberSummary } from './types'\n\n/**\n * The team: everybody on the left, the person being written on the right (§5.6, decision 11).\n *\n * The shape of the reviews without their categories: a person is typed in from a list sent by\n * the client in a minute, and the next one is usually the one under it. On a phone the form is\n * the whole screen, in the drawer `WxListDetail` raises, and draws its own way back.\n *\n * One order, the general one (decision 5): a drag moves it; a search or the bin shows a\n * selection, so the grips go away and the line under the list says why.\n *\n * Everything the screen is looking at lives in the address — the view, the search and the open\n * person — so a link to a person is a link to them in their list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/team' })\n\nconst context = useAdmin()\nconst api = createTeamApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseTeamMessages()\n\nconst t = useTranslate('webx-team')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<MemberRow[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('team.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'team')?.title ??\n t('module.team'),\n)\n\nconst view = computed<TabValue>({\n get: () => (route.query.view === 'trashed' ? 'trashed' : ''),\n set: (value) => {\n const query: LocationQueryRaw = {\n ...route.query,\n view: value === '' ? undefined : String(value),\n }\n\n // A person from the list is not a person from the bin: the form closes with the view.\n delete query.member\n void router.replace({ query })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\n/** The open person: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.member\n\n if (raw === 'new') return canManage.value ? 'new' : null\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\n/*\n * Open is \"a person is chosen\", and closing is un-choosing them — so the address, the list's\n * highlight and the drawer on a phone can never disagree about it.\n */\nconst open = computed({\n get: () => selected.value !== null,\n set: (value) => {\n if (!value) close()\n },\n})\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('member.all') },\n { value: 'trashed', label: t('member.trashed'), icon: 'trash' },\n])\n\nconst order = useItemOrder(TEAM_API, () => ({ q: q.value, filtered: inBin.value }))\n\n/* Dragged only by somebody who may write, and only in a list that is the whole order. */\nconst sortable = computed(() => canManage.value && order.sortable.value)\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('member.empty-search')\n\n return inBin.value ? t('member.empty-bin') : t('member.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({ search: q.value, trashed: inBin.value })\n\n rows.value = answer.data\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\n/* `replace` and not `push`: going through twenty people is not twenty steps of history. */\nfunction choose(id: number | 'new'): void {\n void router.replace({ query: { ...route.query, member: String(id) } })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.member\n void router.replace({ query })\n}\n\n/** A new person was saved: they are a record now, and the list has them. */\nfunction created(member: MemberSummary): void {\n choose(member.id)\n void load()\n}\n\n/**\n * The order on screen, written as a whole. The list is already in its new order\n * (`WxSortableList` reorders the model before it says so), so a failure has to put it back\n * rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await order.move(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('member.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(row: MemberRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('member.restore'),\n run: () => void restore(row),\n },\n ]\n }\n\n return [\n {\n key: 'delete',\n icon: 'trash',\n label: t('member.delete'),\n danger: true,\n run: () => void remove(row),\n },\n ]\n}\n\nasync function remove(row: MemberRow): Promise<void> {\n const agreed = await confirm({\n title: t('member.delete-title', { name: row.name }),\n message: t('member.delete-text'),\n confirmText: t('member.delete'),\n cancelText: t('member.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('member.deleted'))\n removed(row.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/** Gone from the list; and if they were the open one, the form goes with them. */\nfunction removed(id: number): void {\n if (selected.value === id) close()\n\n void load()\n}\n\nasync function restore(row: MemberRow): Promise<void> {\n try {\n await api.restore(row.id)\n toast.success(t('member.restored'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n</script>\n\n<template>\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :card=\"false\">\n <wx-card class=\"wx-team\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-team__panes\"\n :list-width=\"360\"\n :detail-min=\"460\"\n :detail-label=\"t('module.team')\"\n >\n <template #list>\n <div class=\"wx-team__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-team__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('member.search')\"\n :aria-label=\"t('member.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n </div>\n\n <!--\n A line of the list rather than a button in the head: the new person is written where\n they will stand, beside the ones they join — and the form opens the way any other does.\n -->\n <button\n v-if=\"canManage && !inBin\"\n type=\"button\"\n class=\"wx-team__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('member.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-team__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"!inBin && q === '' ? t('member.empty-help') : undefined\"\n />\n\n <template v-else>\n <!-- A grip and not the whole row: the row opens a person, and a row that both opens\n and drags is one where one of the two happens by accident. -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-team__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: MemberRow) => item.name\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'button'\"\n :type=\"inBin ? undefined : 'button'\"\n class=\"wx-member-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"inBin ? undefined : choose(item.id)\"\n >\n <wx-avatar\n class=\"wx-member-row__photo\"\n :src=\"item.photo?.thumb ?? undefined\"\n :name=\"item.name\"\n size=\"md\"\n />\n\n <span class=\"wx-member-row__body\">\n <span class=\"wx-member-row__name\">{{ item.name }}</span>\n\n <span v-if=\"item.job_title\" class=\"wx-member-row__job\">\n {{ item.job_title }}\n </span>\n\n <span v-if=\"!item.published\" class=\"wx-member-row__facts\">\n <wx-badge dot size=\"sm\">{{ t('member.not-published') }}</wx-badge>\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.name\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-team__note\"\n type=\"info\"\n :description=\"order.hint.value\"\n />\n </template>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <member-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\n :inline=\"inline\"\n @back=\"back\"\n @saved=\"load\"\n @created=\"created\"\n @removed=\"removed\"\n />\n </template>\n\n <template #empty>\n <wx-empty\n :title=\"t('member.choose')\"\n :description=\"canManage ? t('member.choose-help') : undefined\"\n />\n </template>\n </wx-list-detail>\n </wx-card>\n </wx-list-screen>\n</template>\n\n<style>\n/*\n * What scrolls here is the page, the way it does on every other list of the panel. The card's\n * corners are the screen's, and the panes paint their own background to the edge: `clip`, not\n * `hidden`, which would make this a scroll container and pin everything sticky inside to a box\n * that never moves (CLAUDE.md §4).\n */\n.wx-team > .wx-card__body {\n display: flex;\n flex-direction: column;\n overflow: clip;\n border-radius: inherit;\n}\n</style>\n\n<style scoped>\n.wx-team__bar {\n display: flex;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-team__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n.wx-team__new {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n width: calc(100% - 2 * var(--wx-space-8));\n margin: 0 var(--wx-space-8) var(--wx-space-4);\n padding: var(--wx-space-8);\n border: 1px dashed var(--wx-border-default);\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: var(--wx-color-primary);\n text-align: start;\n cursor: pointer;\n}\n\n.wx-team__new:hover,\n.wx-team__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-team__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-team__list {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-team__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-team__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-4);\n padding-inline: var(--wx-space-4);\n border-radius: var(--wx-radius-sm);\n}\n\n.wx-team__list :deep(.wx-sortable-list__row:hover),\n.wx-team__list :deep(.wx-sortable-list__row:has(.is-current)) {\n background: var(--wx-bg-subtle);\n}\n\n.wx-team__note {\n margin: var(--wx-space-8);\n}\n\n.wx-member-row {\n display: flex;\n align-items: flex-start;\n gap: var(--wx-space-10);\n /* The row is the target: a short name should not leave half the line dead — and a button,\n unlike a link, is only as wide as what is in it until it is told otherwise. */\n flex: 1 1 auto;\n width: 100%;\n min-width: 0;\n padding-block: var(--wx-space-8);\n padding-inline: 0;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n}\n\nbutton.wx-member-row {\n cursor: pointer;\n}\n\n.wx-member-row__photo {\n flex: none;\n}\n\n.wx-member-row__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--wx-space-2);\n min-width: 0;\n}\n\n.wx-member-row__name {\n min-width: 0;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-member-row.is-current .wx-member-row__name {\n color: var(--wx-color-primary);\n}\n\n.wx-member-row__job {\n overflow: hidden;\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-sm);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-member-row__facts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n margin-top: var(--wx-space-2);\n}\n</style>\n","import type { AdminModule } from '@webx-ui/module-admin'\nimport TeamPage from './TeamPage.vue'\n\nexport interface TeamOptions {\n /** Where the team lives inside the panel. */\n path?: string\n}\n\n/**\n * The team as a section of the panel (§5.6): one module and one entry, without a group of its\n * own — there are no categories to stand beside it (decision 1), and a group around a single\n * item is a heading over itself. A section whose server half is not installed never appears —\n * the entry is built from the manifest.\n */\nexport function team(options: TeamOptions = {}): AdminModule {\n const path = options.path ?? '/team'\n\n return {\n id: 'team',\n path,\n // One route: the open person is in the address (`?member=`), beside the list they were\n // opened from, so a link to them is a link to both.\n routes: [{ path, name: 'webx.team', component: TeamPage, props: { base: path } }],\n }\n}\n"],"names":["TEAM_API","createTeamApi","admin","base","data","body","query","search","id","values","ids","reorderItems","teamMessages","seeded","useTeamMessages","i18n","useAdmin","props","__props","emit","__emit","context","api","locales","useLocales","t","useTranslate","message","useErrorText","member","ref","snapshot","loading","saving","errors","canManage","computed","current","dirty","state","title","written","fallback","localizedValue","take","detail","blank","load","error","toast","save","made","remove","record","confirm","leaveGuard","event","onKeydown","onMounted","onBeforeUnmount","mayLeave","onBeforeRouteUpdate","to","from","onBeforeRouteLeave","actions","_createElementBlock","_createBlock","_unref","WxSkeleton","_Fragment","_createVNode","WxScreenHead","WxBadge","WxScreen","$event","WxActionBar","WxSaveState","WxButton","route","useRoute","router","useRouter","rows","module","view","value","inBin","q","typed","selected","raw","open","close","views","order","useItemOrder","sortable","emptyText","answer","watch","pause","choose","created","reorder","row","actionsFor","restore","removed","WxListScreen","WxCard","WxListDetail","_createElementVNode","_hoisted_1","WxInput","WxIcon","_normalizeClass","_createTextVNode","WxEmpty","WxSortableList","item","_withCtx","_openBlock","_resolveDynamicComponent","WxAvatar","_hoisted_2","_hoisted_3","_toDisplayString","_hoisted_4","_hoisted_5","WxRowMenu","WxAlert","inline","back","MemberPane","team","options","path","TeamPage"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Messages } from '@webx-ui/module-admin';
|
|
2
|
+
/**
|
|
3
|
+
* The English this package speaks on its own, before the server's dictionary arrives, or without
|
|
4
|
+
* one. The server ships the same lines in ten languages under `webx-team::` and overrides these;
|
|
5
|
+
* `messages.test.ts` keeps the two sets of keys equal.
|
|
6
|
+
*/
|
|
7
|
+
export declare const teamMessages: Record<string, Messages>;
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AdminModule } from '@webx-ui/module-admin';
|
|
2
|
+
export interface TeamOptions {
|
|
3
|
+
/** Where the team lives inside the panel. */
|
|
4
|
+
path?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The team as a section of the panel (§5.6): one module and one entry, without a group of its
|
|
8
|
+
* own — there are no categories to stand beside it (decision 1), and a group around a single
|
|
9
|
+
* item is a heading over itself. A section whose server half is not installed never appears —
|
|
10
|
+
* the entry is built from the manifest.
|
|
11
|
+
*/
|
|
12
|
+
export declare function team(options?: TeamOptions): AdminModule;
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.wx-member[data-v-fa99d370]{display:flex;flex-direction:column;gap:var(--wx-gap, var(--wx-space-16));min-width:0;padding:var(--wx-gap, var(--wx-space-16));container-type:inline-size}.wx-member__ghost[data-v-fa99d370]{max-width:480px}.wx-team>.wx-card__body{display:flex;flex-direction:column;overflow:clip;border-radius:inherit}.wx-team__bar[data-v-65fc44a2]{display:flex;gap:var(--wx-space-8);padding:var(--wx-space-12) var(--wx-space-12) var(--wx-space-8)}.wx-team__search[data-v-65fc44a2]{flex:1 1 180px;min-width:0}.wx-team__new[data-v-65fc44a2]{display:flex;align-items:center;gap:var(--wx-space-8);width:calc(100% - 2 * var(--wx-space-8));margin:0 var(--wx-space-8) var(--wx-space-4);padding:var(--wx-space-8);border:1px dashed var(--wx-border-default);border-radius:var(--wx-radius-sm);background:none;font:inherit;color:var(--wx-color-primary);text-align:start;cursor:pointer}.wx-team__new[data-v-65fc44a2]:hover,.wx-team__new.is-current[data-v-65fc44a2]{background:var(--wx-bg-subtle)}.wx-team__loading[data-v-65fc44a2]{padding:var(--wx-space-12)}.wx-team__list[data-v-65fc44a2]{padding-inline:var(--wx-space-8)}.wx-team__list.is-loading[data-v-65fc44a2]{opacity:.6}.wx-team__list[data-v-65fc44a2] .wx-sortable-list__row{gap:var(--wx-space-4);padding-inline:var(--wx-space-4);border-radius:var(--wx-radius-sm)}.wx-team__list[data-v-65fc44a2] .wx-sortable-list__row:hover,.wx-team__list[data-v-65fc44a2] .wx-sortable-list__row:has(.is-current){background:var(--wx-bg-subtle)}.wx-team__note[data-v-65fc44a2]{margin:var(--wx-space-8)}.wx-member-row[data-v-65fc44a2]{display:flex;align-items:flex-start;gap:var(--wx-space-10);flex:1 1 auto;width:100%;min-width:0;padding-block:var(--wx-space-8);padding-inline:0;border:0;background:none;font:inherit;color:inherit;text-align:start}button.wx-member-row[data-v-65fc44a2]{cursor:pointer}.wx-member-row__photo[data-v-65fc44a2]{flex:none}.wx-member-row__body[data-v-65fc44a2]{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--wx-space-2);min-width:0}.wx-member-row__name[data-v-65fc44a2]{min-width:0;overflow:hidden;font-weight:var(--wx-font-weight-medium);text-overflow:ellipsis;white-space:nowrap}.wx-member-row.is-current .wx-member-row__name[data-v-65fc44a2]{color:var(--wx-color-primary)}.wx-member-row__job[data-v-65fc44a2]{overflow:hidden;color:var(--wx-text-muted);font-size:var(--wx-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.wx-member-row__facts[data-v-65fc44a2]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-4);min-width:0;margin-top:var(--wx-space-2)}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ScreenModel } from '@webx-ui/schema';
|
|
2
|
+
/**
|
|
3
|
+
* One person as the section lists them (§5.7).
|
|
4
|
+
*
|
|
5
|
+
* No `locales` beside `published`, unlike a review: a person is seen in every language once
|
|
6
|
+
* published (decision 8) — an untranslated text leaves the text out, not the person.
|
|
7
|
+
*/
|
|
8
|
+
export interface MemberRow {
|
|
9
|
+
id: number;
|
|
10
|
+
/**
|
|
11
|
+
* In the language the panel is open in, in the default one where there is none, `#id` where
|
|
12
|
+
* neither — a name is usually the same in every language.
|
|
13
|
+
*/
|
|
14
|
+
name: string;
|
|
15
|
+
/** The same way; `''` or `null` when nobody wrote one. */
|
|
16
|
+
job_title: string | null;
|
|
17
|
+
/** The first letters of the name, for a row without a photo. */
|
|
18
|
+
initials: string;
|
|
19
|
+
photo: {
|
|
20
|
+
thumb: string | null;
|
|
21
|
+
} | null;
|
|
22
|
+
published: boolean;
|
|
23
|
+
/** Their place in the one order (decision 5). */
|
|
24
|
+
position: number;
|
|
25
|
+
updated_at: string | null;
|
|
26
|
+
deleted_at: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** Everybody — no pages, no filters: the list is where the order is dragged (decision 5). */
|
|
29
|
+
export interface MembersList {
|
|
30
|
+
data: MemberRow[];
|
|
31
|
+
}
|
|
32
|
+
export interface MemberQuery {
|
|
33
|
+
/** The name, the job title or the text, in any language the site has. */
|
|
34
|
+
search?: string;
|
|
35
|
+
/** What was deleted. The only way back to a person in the bin is through this list. */
|
|
36
|
+
trashed?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** The record half of the form's answer — what the head of the form needs. */
|
|
39
|
+
export interface MemberSummary {
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
published: boolean;
|
|
43
|
+
deleted_at: string | null;
|
|
44
|
+
}
|
|
45
|
+
/** One person as their form opens them: the record, and the values of `team.form` by field name. */
|
|
46
|
+
export interface MemberDetail {
|
|
47
|
+
member: MemberSummary;
|
|
48
|
+
values: ScreenModel;
|
|
49
|
+
}
|
|
50
|
+
/** One row of `socials` in the form's values (§5.4): a network of the site's list and an address. */
|
|
51
|
+
export interface SocialLink {
|
|
52
|
+
network: string | null;
|
|
53
|
+
url: string | null;
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webx-ui/module-team",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The team for the WebX UI admin panel: people with a photo, a job title, a short text and their social links, ordered by hand, shown on the site by a block.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"webx-ui",
|
|
12
|
+
"vue",
|
|
13
|
+
"admin",
|
|
14
|
+
"cms",
|
|
15
|
+
"team",
|
|
16
|
+
"staff"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/webx-ui/webx-ui.git",
|
|
21
|
+
"directory": "packages/module-team"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://webx-ui.github.io/webx-ui/guide/team.html",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./style.css": "./dist/style.css",
|
|
39
|
+
"./dist/style.css": "./dist/style.css",
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"vue": "^3.5.0",
|
|
44
|
+
"vue-router": "^4.5.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@webx-ui/core": "^0.34.0",
|
|
48
|
+
"@webx-ui/module-admin": "^0.18.1",
|
|
49
|
+
"@webx-ui/schema": "^0.6.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^24.10.1",
|
|
53
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vite": "^7.1.14",
|
|
56
|
+
"vite-plugin-dts": "^4.5.4",
|
|
57
|
+
"vue": "^3.5.24",
|
|
58
|
+
"vue-router": "^4.5.1",
|
|
59
|
+
"vue-tsc": "^3.1.3"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "vite build",
|
|
63
|
+
"dev": "vite build --watch",
|
|
64
|
+
"typecheck": "vue-tsc -p tsconfig.json --noEmit"
|
|
65
|
+
}
|
|
66
|
+
}
|