Skip to content

Commit 7495bea

Browse files
committed
Componentize settings and add team configuration
1 parent e25cc91 commit 7495bea

29 files changed

Lines changed: 1973 additions & 492 deletions

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ FROM node as builder
66
ARG VITE_TDEI_API_URL
77
ARG VITE_TDEI_USER_API_URL
88
ARG VITE_API_URL
9+
ARG VITE_NEW_API_URL
910
ARG VITE_OSM_URL
1011
ARG VITE_RAPID_URL
1112
ARG VITE_PATHWAYS_EDITOR_URL
@@ -29,8 +30,8 @@ COPY --from=builder /app/.output/public /usr/share/nginx/html/
2930
# https://stackoverflow.com/questions/44438637/arg-substitution-in-run-command-not-working-for-dockerfile
3031
ARG CODE_VERSION
3132

32-
RUN echo "This is (frontend, cgimap, osmrails, pathways, rapid, taskingmanager) $CODE_VERSION"
33-
RUN echo "This is (frontend, cgimap, osmrails, pathways, rapid, taskingmanager) $CODE_VERSION" > /usr/share/nginx/html/VERSION
33+
RUN echo "This is (frontend, api, cgimap, osmrails, pathways, rapid, taskingmanager) $CODE_VERSION"
34+
RUN echo "This is (frontend, api, cgimap, osmrails, pathways, rapid, taskingmanager) $CODE_VERSION" > /usr/share/nginx/html/VERSION
3435

3536
RUN chown -R nginx:nginx /usr/share/nginx/html/
3637

assets/scss/main.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@import "theme.scss";
22
@import "bootstrap/scss/bootstrap.scss";
33
@import "maplibre-gl/dist/maplibre-gl.css";
4+
@import "vue3-toastify/dist/index.css";
45

56
:root {
67
--ws-create-color: $review-create-color;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<b-button
3+
:variant="props.variant"
4+
to="/dashboard"
5+
>
6+
Return to Dashboard
7+
</b-button>
8+
</template>
9+
10+
<script setup lang="ts">
11+
import type { BButtonProps } from 'bootstrap-vue-next/types';
12+
13+
interface Props {
14+
variant?: BButtonProps['variant'];
15+
}
16+
17+
const props = withDefaults(defineProps<Props>(), {
18+
variant: 'primary',
19+
});
20+
</script>

components/settings/Nav.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<template>
2+
<nav class="list-group mb-4">
3+
<settings-nav-link
4+
to=""
5+
icon="settings"
6+
>
7+
General
8+
</settings-nav-link>
9+
<settings-nav-link
10+
to="/teams"
11+
icon="group"
12+
>
13+
Teams
14+
</settings-nav-link>
15+
</nav>
16+
</template>
17+
18+
<script setup lang="ts">
19+
</script>

components/settings/NavLink.vue

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<template>
2+
<nuxt-link
3+
class="list-group-item"
4+
:class="{ active }"
5+
:to="to"
6+
>
7+
<app-icon :variant="props.icon" />
8+
<slot />
9+
</nuxt-link>
10+
</template>
11+
12+
<script setup lang="ts">
13+
import type { Workspace } from '~/types/workspaces';
14+
15+
interface Props {
16+
to: string;
17+
icon: string;
18+
}
19+
20+
const props = defineProps<Props>();
21+
const route = useRoute();
22+
23+
const workspace = inject<Workspace>('workspace')!;
24+
const to = computed(() => `/workspace/${workspace.id}/settings${props.to}`);
25+
26+
const active = computed(() =>
27+
(route.path.endsWith('/settings') && to.value.endsWith('/settings'))
28+
|| (!to.value.endsWith('/settings') && route.path.startsWith(to.value)));
29+
</script>

components/settings/panel/Apps.vue

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
<template>
2+
<form
3+
class="card mb-4"
4+
@submit.prevent="saveExternalAppConfiguration"
5+
>
6+
<div class="card-body border-bottom">
7+
<h3 class="card-title mb-3">
8+
External Apps
9+
</h3>
10+
11+
<div class="form-check form-switch">
12+
<label class="form-check-label">
13+
<input
14+
v-model="workspace.externalAppAccess"
15+
type="checkbox"
16+
class="form-check-input"
17+
:true-value="1"
18+
:false-value="0"
19+
>
20+
Publish this workspace for external apps
21+
</label>
22+
</div>
23+
24+
<hr>
25+
26+
<h4 class="h5">
27+
AVIV ScoutRoute Long Form Quest Definitions
28+
</h4>
29+
30+
<div class="form-check">
31+
<label class="form-check-label">
32+
Define quests in Workspaces
33+
<input
34+
v-model="longFormQuestType"
35+
class="form-check-input"
36+
type="radio"
37+
name="longFormQuestType"
38+
value="JSON"
39+
>
40+
</label>
41+
</div>
42+
<div class="form-check">
43+
<label class="form-check-label">
44+
Load quest definitions from an external URL
45+
<input
46+
v-model="longFormQuestType"
47+
class="form-check-input"
48+
type="radio"
49+
name="longFormQuestType"
50+
value="URL"
51+
>
52+
</label>
53+
</div>
54+
55+
<template v-if="longFormQuestType === 'JSON'">
56+
<label class="d-block form-label mt-3">
57+
JSON Quest Definition
58+
<textarea
59+
v-model.trim="longFormQuestDef"
60+
class="form-control"
61+
:class="{ 'drag-over': isDraggingQuest }"
62+
rows="5"
63+
placeholder="Optional"
64+
@dragover.prevent="isDraggingQuest = true"
65+
@dragleave.prevent="isDraggingQuest = false"
66+
@drop.prevent="onQuestFileDrop"
67+
/>
68+
</label>
69+
<div
70+
id="imagery-help"
71+
class="form-text"
72+
>
73+
Paste the JSON content directly or drag and drop a JSON file.
74+
See the
75+
<a
76+
:href="longFormQuestSchemaUrl"
77+
target="_blank"
78+
>
79+
JSON Schema
80+
</a>
81+
for the required format and an
82+
<a
83+
:href="longFormQuestExampleUrl"
84+
target="_blank"
85+
>
86+
example
87+
</a>.
88+
</div>
89+
</template>
90+
91+
<template v-else-if="longFormQuestType === 'URL'">
92+
<label class="d-block form-label mt-3">
93+
Quest Definition URL
94+
<input
95+
v-model.trim="longFormQuestUrl"
96+
type="text"
97+
class="form-control"
98+
placeholder="https://..."
99+
>
100+
</label>
101+
<div
102+
id="imagery-help"
103+
class="form-text"
104+
>
105+
Enter the address of a quest definition JSON document
106+
See the
107+
<a
108+
:href="longFormQuestSchemaUrl"
109+
target="_blank"
110+
>
111+
JSON Schema
112+
</a>
113+
for the required format and an
114+
<a
115+
:href="longFormQuestExampleUrl"
116+
target="_blank"
117+
>
118+
example
119+
</a>.
120+
</div>
121+
</template>
122+
123+
<div
124+
v-if="longFormQuestError"
125+
class="form-text text-danger"
126+
>
127+
{{ longFormQuestError }}
128+
</div>
129+
130+
<hr>
131+
<button
132+
type="submit"
133+
class="btn btn-primary"
134+
>
135+
Save
136+
</button>
137+
<div
138+
v-if="externalAppSaveStatus"
139+
:class="`mt-2 form-text text-${
140+
externalAppSaveStatus.type === 'success' ? 'success' : 'danger'
141+
}`"
142+
>
143+
{{ externalAppSaveStatus.message }}
144+
</div>
145+
</div><!-- .card-body -->
146+
</form><!-- .card -->
147+
</template>
148+
149+
<script setup lang="ts">
150+
import { workspacesClient } from '~/services/index';
151+
import { handleFileDrop, validateJson } from '~/util/schema';
152+
import { isHttpUrl, normalizeUrl } from '~/util/url';
153+
154+
import type { Workspace } from '~/types/workspaces';
155+
156+
const longFormQuestSchemaUrl = import.meta.env.VITE_LONG_FORM_QUEST_SCHEMA;
157+
const longFormQuestExampleUrl = import.meta.env.VITE_LONG_FORM_QUEST_EXAMPLE_URL;
158+
159+
const workspace = inject<Workspace>('workspace')!;
160+
161+
const [longFormQuestSettings] = await Promise.all([
162+
workspacesClient.getLongFormQuestSettings(workspace.id),
163+
]);
164+
165+
const longFormQuestSchema = ref<object | undefined>();
166+
const longFormQuestType = ref(longFormQuestSettings.type);
167+
const longFormQuestDef = ref(longFormQuestSettings.definition);
168+
const longFormQuestUrl = ref(longFormQuestSettings.url);
169+
const longFormQuestError = ref<string | null>(null);
170+
const externalAppSaveStatus = ref<{ type: 'success' | 'error'; message: string } | null>(null);
171+
const isDraggingQuest = ref(false);
172+
173+
watch(
174+
[
175+
longFormQuestType,
176+
longFormQuestDef,
177+
longFormQuestUrl,
178+
() => workspace.externalAppAccess,
179+
],
180+
() => { clearExternalAppMessages(); },
181+
);
182+
183+
function clearExternalAppMessages() {
184+
longFormQuestError.value = null;
185+
externalAppSaveStatus.value = null;
186+
}
187+
188+
function onQuestFileDrop(event: DragEvent) {
189+
handleFileDrop(event, longFormQuestDef, isDraggingQuest);
190+
}
191+
192+
async function saveExternalAppConfiguration() {
193+
clearExternalAppMessages();
194+
195+
let type = longFormQuestType.value;
196+
let definition = longFormQuestDef.value;
197+
let url = longFormQuestUrl.value;
198+
199+
if (type === 'JSON') {
200+
url = undefined;
201+
202+
if (!definition) {
203+
type = 'NONE';
204+
}
205+
else {
206+
const validationResult = await validateJson(
207+
definition,
208+
longFormQuestSchemaUrl,
209+
longFormQuestSchema,
210+
'Long form quest definition',
211+
);
212+
213+
if (validationResult.error) {
214+
longFormQuestError.value = validationResult.error;
215+
return;
216+
}
217+
}
218+
}
219+
else if (type === 'URL') {
220+
definition = undefined;
221+
222+
if (!url) {
223+
type = 'NONE';
224+
}
225+
else if (!isHttpUrl(url)) {
226+
longFormQuestError.value = 'The URL is not valid.';
227+
return;
228+
}
229+
else {
230+
url = normalizeUrl(url);
231+
}
232+
}
233+
234+
try {
235+
await Promise.all([
236+
workspacesClient.updateWorkspace(workspace.id, {
237+
externalAppAccess: workspace.externalAppAccess,
238+
}),
239+
workspacesClient.saveLongFormQuestSettings(workspace.id, {
240+
type,
241+
definition,
242+
url,
243+
}),
244+
]);
245+
246+
externalAppSaveStatus.value = { type: 'success', message: 'Changes saved.' };
247+
longFormQuestType.value = type;
248+
longFormQuestDef.value = definition;
249+
longFormQuestUrl.value = url;
250+
}
251+
catch (e) {
252+
const errorMessage = e instanceof Error ? e.message : 'unexpected error';
253+
externalAppSaveStatus.value = {
254+
type: 'error',
255+
message: 'Failed to save changes: ' + errorMessage,
256+
};
257+
}
258+
}
259+
</script>

0 commit comments

Comments
 (0)