@cloudron/pankow 4.4.4 → 4.4.6

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.
@@ -128,6 +128,7 @@ defineExpose({ open, close });
128
128
  </div>
129
129
  <div class="pankow-dialog-buttons" v-if="rejectLabel || confirmLabel || alternateLabel">
130
130
  <Button :[alternateStyle]="!!alternateStyle" @click="onAlternateAction" v-show="alternateLabel" :loading="alternateBusy" :disabled="alternateBusy || !alternateActive">{{ alternateLabel }}</Button>
131
+ <slot name="footer"/>
131
132
  <div style="flex-grow: 1;"></div>
132
133
  <Button :[rejectStyle]="!!rejectStyle" @click="onReject" v-show="rejectLabel" :disabled="!rejectActive">{{ rejectLabel }}</Button>
133
134
  <Button ref="confirmButton" :[confirmStyle]="!!confirmStyle" @click="onConfirm" v-show="confirmLabel" :loading="confirmBusy" :disabled="confirmBusy || !confirmActive">{{ confirmLabel }}</Button>
@@ -0,0 +1,49 @@
1
+ <script setup>
2
+ </script>
3
+
4
+ <template>
5
+ <div class="pankow-settings-item">
6
+ <div class="pankow-settings-item-body">
7
+ <slot></slot>
8
+ </div>
9
+ <div class="pankow-settings-item-bottom" v-if="$slots.bottom">
10
+ <slot name="bottom"></slot>
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <style>
16
+
17
+ .pankow-settings-item {
18
+ border-radius: var(--pankow-border-radius);
19
+ padding: 0;
20
+ margin-bottom: 20px;
21
+ }
22
+
23
+ .pankow-settings-item-body {
24
+ display: flex;
25
+ justify-content: space-between;
26
+ align-items: center;
27
+ gap: 20px;
28
+ }
29
+
30
+ .pankow-settings-item-body > :first-child {
31
+ max-width: 720px;
32
+ }
33
+
34
+ @media (max-width: 576px) {
35
+ .pankow-settings-item[wrap] .pankow-settings-item-body {
36
+ flex-wrap: wrap;
37
+ }
38
+ }
39
+
40
+ .pankow-settings-item label {
41
+ margin-top: 0;
42
+ }
43
+
44
+ .pankow-settings-item-bottom {
45
+ width: 100%;
46
+ margin-top: 10px;
47
+ }
48
+
49
+ </style>
package/fetcher.js CHANGED
@@ -30,7 +30,12 @@ async function request(uri, method, headers, query, body, options) {
30
30
  let response, responseBody;
31
31
 
32
32
  const url = new URL(uri, typeof window !== 'undefined' ? window.location.origin : undefined);
33
- url.search = new URLSearchParams(query).toString();
33
+ const uriParams = new URLSearchParams(url.search);
34
+ const queryParams = new URLSearchParams(query);
35
+ for (const [key, value] of queryParams) {
36
+ uriParams.set(key, value);
37
+ }
38
+ url.search = uriParams.toString();
34
39
 
35
40
  const fetchOptions = Object.assign({}, globalOptions, options, { method, headers });
36
41
 
@@ -73,61 +78,135 @@ async function request(uri, method, headers, query, body, options) {
73
78
  return { status: response.status, body: responseBody };
74
79
  }
75
80
 
81
+ /**
82
+ * Disambiguates positional query/options arguments from the new named-parameter style.
83
+ *
84
+ * New style (when the second/third arg has a `query` property):
85
+ * get(uri, { query: { page: 1 }, signal }) → query={page:1}, options={signal}
86
+ * post(uri, body, { query: { t: 'x' }, signal }) → query={t:'x'}, options={signal}
87
+ *
88
+ * Legacy style (positional, unchanged):
89
+ * get(uri, { page: 1 })
90
+ * get(uri, { page: 1 }, { signal })
91
+ * post(uri, body, { t: 'x' }, { signal })
92
+ *
93
+ * @param {Object} queryOrOpts - Either a query object or an options object with a `query` key
94
+ * @param {Object} [secondOpts={}] - Additional options (for legacy positional style)
95
+ * @returns {{ query: Object, options: Object }}
96
+ */
97
+ function splitQueryOptions(queryOrOpts, secondOpts = {}) {
98
+ if (queryOrOpts && typeof queryOrOpts === 'object' && !(queryOrOpts instanceof FormData) && 'query' in queryOrOpts) {
99
+ const { query, ...opts } = queryOrOpts;
100
+ return { query, options: Object.assign(opts, secondOpts) };
101
+ }
102
+ return { query: queryOrOpts || {}, options: secondOpts };
103
+ }
104
+
76
105
  /**
77
106
  * Sends a HEAD request.
107
+ *
108
+ * @example
109
+ * // legacy style (positional query)
110
+ * await fetcher.head('/api/items', { sort: 'name' }, { signal: abortController.signal });
111
+ *
112
+ * @example
113
+ * // new style (named query in options object)
114
+ * await fetcher.head('/api/items', { query: { sort: 'name' }, signal: abortController.signal });
115
+ *
78
116
  * @param {string} uri
79
- * @param {Object} [query={}] - Query parameters serialized as URL query string
80
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
117
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
118
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
81
119
  * @returns {Promise<{ status: number, body: * }>}
82
120
  */
83
- async function head(uri, query = {}, options = {}) {
121
+ async function head(uri, queryOrOpts = {}, moreOpts = {}) {
122
+ const { query, options } = splitQueryOptions(queryOrOpts, moreOpts);
84
123
  return await request(uri, 'HEAD', {}, query, null, options);
85
124
  }
86
125
 
87
126
  /**
88
127
  * Sends a GET request.
128
+ *
129
+ * @example
130
+ * // legacy style (positional query)
131
+ * await fetcher.get('/api/items', { page: 1 }, { signal: abortController.signal });
132
+ *
133
+ * @example
134
+ * // new style (named query in options object)
135
+ * await fetcher.get('/api/items', { query: { page: 1 }, signal: abortController.signal });
136
+ *
89
137
  * @param {string} uri
90
- * @param {Object} [query={}] - Query parameters serialized as URL query string
91
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
138
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
139
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
92
140
  * @returns {Promise<{ status: number, body: * }>}
93
141
  */
94
- async function get(uri, query = {}, options = {}) {
142
+ async function get(uri, queryOrOpts = {}, moreOpts = {}) {
143
+ const { query, options } = splitQueryOptions(queryOrOpts, moreOpts);
95
144
  return await request(uri, 'GET', {}, query, null, options);
96
145
  }
97
146
 
98
147
  /**
99
148
  * Sends a POST request with a JSON or FormData body.
149
+ *
150
+ * @example
151
+ * // legacy style (positional query)
152
+ * await fetcher.post('/api/items', { name: 'Foo' }, { dryRun: true }, { signal: abortController.signal });
153
+ *
154
+ * @example
155
+ * // new style (named query in options object)
156
+ * await fetcher.post('/api/items', { name: 'Foo' }, { query: { dryRun: true }, signal: abortController.signal });
157
+ *
100
158
  * @param {string} uri
101
159
  * @param {Object|FormData} body - Request body; objects are JSON-stringified
102
- * @param {Object} [query={}] - Query parameters serialized as URL query string
103
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
160
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
161
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
104
162
  * @returns {Promise<{ status: number, body: * }>}
105
163
  */
106
- async function post(uri, body, query = {}, options = {}) {
164
+ async function post(uri, body, queryOrOpts = {}, moreOpts = {}) {
165
+ const { query, options } = splitQueryOptions(queryOrOpts, moreOpts);
107
166
  return await request(uri, 'POST', {}, query, body, options);
108
167
  }
109
168
 
110
169
  /**
111
170
  * Sends a PUT request with a JSON or FormData body.
171
+ *
172
+ * @example
173
+ * // legacy style (positional query)
174
+ * await fetcher.put('/api/items/42', { name: 'Foo' }, { dryRun: true }, { signal: abortController.signal });
175
+ *
176
+ * @example
177
+ * // new style (named query in options object)
178
+ * await fetcher.put('/api/items/42', { name: 'Foo' }, { query: { dryRun: true }, signal: abortController.signal });
179
+ *
112
180
  * @param {string} uri
113
181
  * @param {Object|FormData} body - Request body; objects are JSON-stringified
114
- * @param {Object} [query={}] - Query parameters serialized as URL query string
115
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
182
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
183
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
116
184
  * @returns {Promise<{ status: number, body: * }>}
117
185
  */
118
- async function put(uri, body, query = {}, options = {}) {
186
+ async function put(uri, body, queryOrOpts = {}, moreOpts = {}) {
187
+ const { query, options } = splitQueryOptions(queryOrOpts, moreOpts);
119
188
  return await request(uri, 'PUT', {}, query, body, options);
120
189
  }
121
190
 
122
191
  /**
123
192
  * Sends a DELETE request with an optional body.
193
+ *
194
+ * @example
195
+ * // legacy style — no body, with query and options
196
+ * await fetcher.del('/api/items/42', null, { force: true }, { signal: abortController.signal });
197
+ *
198
+ * @example
199
+ * // new style — named query in options object, no body
200
+ * await fetcher.del('/api/items/42', null, { query: { force: true }, signal: abortController.signal });
201
+ *
124
202
  * @param {string} uri
125
- * @param {Object|FormData} body - Request body; objects are JSON-stringified
126
- * @param {Object} [query={}] - Query parameters serialized as URL query string
127
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
203
+ * @param {Object|FormData|null} body - Request body; objects are JSON-stringified. Pass `null` for no body.
204
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
205
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
128
206
  * @returns {Promise<{ status: number, body: * }>}
129
207
  */
130
- async function del(uri, body, query = {}, options = {}) {
208
+ async function del(uri, body, queryOrOpts = {}, moreOpts = {}) {
209
+ const { query, options } = splitQueryOptions(queryOrOpts, moreOpts);
131
210
  return await request(uri, 'DELETE', {}, query, body, options);
132
211
  }
133
212
 
package/index.js CHANGED
@@ -40,6 +40,7 @@ import ProgressBar from './components/ProgressBar.vue';
40
40
  import RadioButton from './components/RadioButton.vue';
41
41
  import SaveIndicator from './components/SaveIndicator.vue';
42
42
  import SectionItem from './components/SectionItem.vue';
43
+ import SettingsItem from './components/SettingsItem.vue';
43
44
  import SideBar from './components/SideBar.vue';
44
45
  import SplitLayout from './components/SplitLayout.vue';
45
46
  import Spinner from './components/Spinner.vue';
@@ -95,6 +96,7 @@ export {
95
96
  RadioButton,
96
97
  SaveIndicator,
97
98
  SectionItem,
99
+ SettingsItem,
98
100
  SideBar,
99
101
  SplitLayout,
100
102
  Spinner,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloudron/pankow",
3
3
  "private": false,
4
- "version": "4.4.4",
4
+ "version": "4.4.6",
5
5
  "description": "Vue 3 UI component library for Cloudron apps",
6
6
  "main": "index.js",
7
7
  "types": "types/index.d.ts",
@@ -48,13 +48,13 @@
48
48
  },
49
49
  "devDependencies": {
50
50
  "@highlightjs/vue-plugin": "^2.1.0",
51
- "@unhead/vue": "^3.2.3",
51
+ "@unhead/vue": "^3.3.1",
52
52
  "@vitejs/plugin-vue": "^6.0.8",
53
53
  "highlight.js": "^11.11.1",
54
54
  "typescript": "^7.0.2",
55
- "vite": "^8.1.5",
55
+ "vite": "^8.2.1",
56
56
  "vite-ssg": "^28.3.0",
57
- "vue": "^3.5.40",
57
+ "vue": "^3.5.41",
58
58
  "vue-router": "^5.2.0"
59
59
  },
60
60
  "allowScripts": {
@@ -21,59 +21,104 @@ export type FetcherGlobalOptions = {
21
21
  };
22
22
  /**
23
23
  * Sends a HEAD request.
24
+ *
25
+ * @example
26
+ * // legacy style (positional query)
27
+ * await fetcher.head('/api/items', { sort: 'name' }, { signal: abortController.signal });
28
+ *
29
+ * @example
30
+ * // new style (named query in options object)
31
+ * await fetcher.head('/api/items', { query: { sort: 'name' }, signal: abortController.signal });
32
+ *
24
33
  * @param {string} uri
25
- * @param {Object} [query={}] - Query parameters serialized as URL query string
26
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
34
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
35
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
27
36
  * @returns {Promise<{ status: number, body: * }>}
28
37
  */
29
- declare function head(uri: string, query?: Object, options?: RequestInit): Promise<{
38
+ declare function head(uri: string, queryOrOpts?: Object, moreOpts?: RequestInit): Promise<{
30
39
  status: number;
31
40
  body: any;
32
41
  }>;
33
42
  /**
34
43
  * Sends a GET request.
44
+ *
45
+ * @example
46
+ * // legacy style (positional query)
47
+ * await fetcher.get('/api/items', { page: 1 }, { signal: abortController.signal });
48
+ *
49
+ * @example
50
+ * // new style (named query in options object)
51
+ * await fetcher.get('/api/items', { query: { page: 1 }, signal: abortController.signal });
52
+ *
35
53
  * @param {string} uri
36
- * @param {Object} [query={}] - Query parameters serialized as URL query string
37
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
54
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
55
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
38
56
  * @returns {Promise<{ status: number, body: * }>}
39
57
  */
40
- declare function get(uri: string, query?: Object, options?: RequestInit): Promise<{
58
+ declare function get(uri: string, queryOrOpts?: Object, moreOpts?: RequestInit): Promise<{
41
59
  status: number;
42
60
  body: any;
43
61
  }>;
44
62
  /**
45
63
  * Sends a POST request with a JSON or FormData body.
64
+ *
65
+ * @example
66
+ * // legacy style (positional query)
67
+ * await fetcher.post('/api/items', { name: 'Foo' }, { dryRun: true }, { signal: abortController.signal });
68
+ *
69
+ * @example
70
+ * // new style (named query in options object)
71
+ * await fetcher.post('/api/items', { name: 'Foo' }, { query: { dryRun: true }, signal: abortController.signal });
72
+ *
46
73
  * @param {string} uri
47
74
  * @param {Object|FormData} body - Request body; objects are JSON-stringified
48
- * @param {Object} [query={}] - Query parameters serialized as URL query string
49
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
75
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
76
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
50
77
  * @returns {Promise<{ status: number, body: * }>}
51
78
  */
52
- declare function post(uri: string, body: Object | FormData, query?: Object, options?: RequestInit): Promise<{
79
+ declare function post(uri: string, body: Object | FormData, queryOrOpts?: Object, moreOpts?: RequestInit): Promise<{
53
80
  status: number;
54
81
  body: any;
55
82
  }>;
56
83
  /**
57
84
  * Sends a PUT request with a JSON or FormData body.
85
+ *
86
+ * @example
87
+ * // legacy style (positional query)
88
+ * await fetcher.put('/api/items/42', { name: 'Foo' }, { dryRun: true }, { signal: abortController.signal });
89
+ *
90
+ * @example
91
+ * // new style (named query in options object)
92
+ * await fetcher.put('/api/items/42', { name: 'Foo' }, { query: { dryRun: true }, signal: abortController.signal });
93
+ *
58
94
  * @param {string} uri
59
95
  * @param {Object|FormData} body - Request body; objects are JSON-stringified
60
- * @param {Object} [query={}] - Query parameters serialized as URL query string
61
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
96
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
97
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
62
98
  * @returns {Promise<{ status: number, body: * }>}
63
99
  */
64
- declare function put(uri: string, body: Object | FormData, query?: Object, options?: RequestInit): Promise<{
100
+ declare function put(uri: string, body: Object | FormData, queryOrOpts?: Object, moreOpts?: RequestInit): Promise<{
65
101
  status: number;
66
102
  body: any;
67
103
  }>;
68
104
  /**
69
105
  * Sends a DELETE request with an optional body.
106
+ *
107
+ * @example
108
+ * // legacy style — no body, with query and options
109
+ * await fetcher.del('/api/items/42', null, { force: true }, { signal: abortController.signal });
110
+ *
111
+ * @example
112
+ * // new style — named query in options object, no body
113
+ * await fetcher.del('/api/items/42', null, { query: { force: true }, signal: abortController.signal });
114
+ *
70
115
  * @param {string} uri
71
- * @param {Object|FormData} body - Request body; objects are JSON-stringified
72
- * @param {Object} [query={}] - Query parameters serialized as URL query string
73
- * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
116
+ * @param {Object|FormData|null} body - Request body; objects are JSON-stringified. Pass `null` for no body.
117
+ * @param {Object} [queryOrOpts={}] - Query parameters OR options object with optional `query` key
118
+ * @param {RequestInit} [moreOpts={}] - Merged into `fetch()`; use `{ signal }` to abort
74
119
  * @returns {Promise<{ status: number, body: * }>}
75
120
  */
76
- declare function del(uri: string, body: Object | FormData, query?: Object, options?: RequestInit): Promise<{
121
+ declare function del(uri: string, body: Object | FormData | null, queryOrOpts?: Object, moreOpts?: RequestInit): Promise<{
77
122
  status: number;
78
123
  body: any;
79
124
  }>;
package/types/index.d.ts CHANGED
@@ -35,6 +35,7 @@ import ProgressBar from './components/ProgressBar.vue';
35
35
  import RadioButton from './components/RadioButton.vue';
36
36
  import SaveIndicator from './components/SaveIndicator.vue';
37
37
  import SectionItem from './components/SectionItem.vue';
38
+ import SettingsItem from './components/SettingsItem.vue';
38
39
  import SideBar from './components/SideBar.vue';
39
40
  import SplitLayout from './components/SplitLayout.vue';
40
41
  import Spinner from './components/Spinner.vue';
@@ -54,7 +55,7 @@ import utils from './utils.js';
54
55
  import tooltip from './tooltip.js';
55
56
  import fallbackImage from './fallbackImage.js';
56
57
  import textOverflow from './text-overflow.js';
57
- export { BottomBar, BreadCrumb, Button, ButtonGroup, ClipboardAction, ClipboardButton, CheckBox, DateTimeInput, Dialog, DirectoryView, EmailInput, FileUploader, FormGroup, InputGroup, Icon, ListItem, InputDialog, LoginView, MainLayout, MaskedInput, Menu, MenuItem, SingleSelect, MultiSelect, Notification, NumberInput, OfflineBanner, PasswordInput, Popover, ProgressBar, RadioButton, SaveIndicator, SectionItem, SideBar, SplitLayout, Spinner, Switch, TableView, TableViewActionBar, TabView, TagInput, TextInput, TextInputRaw, TopBar, TreeView, fetcher, gestures, tooltip, fallbackImage, textOverflow, utils, };
58
+ export { BottomBar, BreadCrumb, Button, ButtonGroup, ClipboardAction, ClipboardButton, CheckBox, DateTimeInput, Dialog, DirectoryView, EmailInput, FileUploader, FormGroup, InputGroup, Icon, ListItem, InputDialog, LoginView, MainLayout, MaskedInput, Menu, MenuItem, SingleSelect, MultiSelect, Notification, NumberInput, OfflineBanner, PasswordInput, Popover, ProgressBar, RadioButton, SaveIndicator, SectionItem, SettingsItem, SideBar, SplitLayout, Spinner, Switch, TableView, TableViewActionBar, TabView, TagInput, TextInput, TextInputRaw, TopBar, TreeView, fetcher, gestures, tooltip, fallbackImage, textOverflow, utils, };
58
59
  export { CheckBox as Checkbox };
59
60
  export { RadioButton as Radiobutton };
60
61
  export { BreadCrumb as Breadcrumb };