Added announcements.
This commit is contained in:
parent
490806aaee
commit
fec713f15e
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE announcement (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
type VARCHAR(64) NOT NULL,
|
||||||
|
message VARCHAR(2000) NOT NULL
|
||||||
|
);
|
|
@ -0,0 +1,82 @@
|
||||||
|
module api_modules.announcement;
|
||||||
|
|
||||||
|
import handy_httpd;
|
||||||
|
import handy_httpd.handlers.path_handler;
|
||||||
|
import slf4d;
|
||||||
|
import ddbc;
|
||||||
|
|
||||||
|
import db;
|
||||||
|
import data_utils;
|
||||||
|
import api_modules.auth : getAdminUserOrThrow;
|
||||||
|
|
||||||
|
struct Announcement {
|
||||||
|
const ulong id;
|
||||||
|
const string type;
|
||||||
|
const string message;
|
||||||
|
|
||||||
|
static Announcement parse(DataSetReader r) {
|
||||||
|
return Announcement(
|
||||||
|
r.getUlong(1),
|
||||||
|
r.getString(2),
|
||||||
|
r.getString(3)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerApiEndpoints(PathHandler handler) {
|
||||||
|
handler.addMapping(Method.GET, "api/announcement", &getAnnouncementsEndpoint);
|
||||||
|
handler.addMapping(Method.POST, "api/announcement", &createAnnouncementAdminEndpoint);
|
||||||
|
handler.addMapping(Method.DELETE, "api/announcement/:id:ulong", &deleteAnnouncementAdminEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void getAnnouncementsEndpoint(ref HttpRequestContext ctx) {
|
||||||
|
Connection conn = getDb();
|
||||||
|
scope(exit) conn.close();
|
||||||
|
const announcements = findAll(
|
||||||
|
conn,
|
||||||
|
"SELECT * FROM announcement ORDER BY id DESC",
|
||||||
|
&Announcement.parse
|
||||||
|
);
|
||||||
|
writeJsonBody(ctx, announcements);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createAnnouncementAdminEndpoint(ref HttpRequestContext ctx) {
|
||||||
|
Connection conn = getDb();
|
||||||
|
scope(exit) conn.close();
|
||||||
|
auto user = getAdminUserOrThrow(ctx, conn);
|
||||||
|
struct Payload {
|
||||||
|
string type;
|
||||||
|
string message;
|
||||||
|
}
|
||||||
|
Payload payload = readJsonPayload!(Payload)(ctx);
|
||||||
|
if (payload.type is null || (payload.type != "INFO" && payload.type != "ERROR")) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid type.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (payload.message is null || payload.message.length < 1 || payload.message.length > 2000) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ulong id = insertOne(
|
||||||
|
conn,
|
||||||
|
"INSERT INTO announcement (type, message) VALUES (?, ?) RETURNING id",
|
||||||
|
payload.type, payload.message
|
||||||
|
);
|
||||||
|
const announcement = findOne(
|
||||||
|
conn,
|
||||||
|
"SELECT * FROM announcement WHERE id = ?",
|
||||||
|
&Announcement.parse,
|
||||||
|
id
|
||||||
|
).orElseThrow();
|
||||||
|
writeJsonBody(ctx, announcement);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteAnnouncementAdminEndpoint(ref HttpRequestContext ctx) {
|
||||||
|
Connection conn = getDb();
|
||||||
|
scope(exit) conn.close();
|
||||||
|
auto user = getAdminUserOrThrow(ctx, conn);
|
||||||
|
ulong id = ctx.request.getPathParamAs!ulong("id");
|
||||||
|
update(conn, "DELETE FROM announcement WHERE id = ?", id);
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import handy_httpd.handlers.path_handler;
|
||||||
import std.process;
|
import std.process;
|
||||||
|
|
||||||
static import api_modules.auth;
|
static import api_modules.auth;
|
||||||
|
static import api_modules.announcement;
|
||||||
static import api_modules.classroom_compliance.api;
|
static import api_modules.classroom_compliance.api;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
@ -25,7 +26,12 @@ void main() {
|
||||||
|
|
||||||
PathHandler handler = new PathHandler();
|
PathHandler handler = new PathHandler();
|
||||||
handler.addMapping(Method.OPTIONS, "/api/**", &optionsEndpoint);
|
handler.addMapping(Method.OPTIONS, "/api/**", &optionsEndpoint);
|
||||||
|
if (env == "DEV") {
|
||||||
|
handler.addMapping("/api/initialize-schema", &initializeSchemaEndpoint);
|
||||||
|
handler.addMapping("/api/generate-sample-data", &sampleDataEndpoint);
|
||||||
|
}
|
||||||
api_modules.auth.registerApiEndpoints(handler);
|
api_modules.auth.registerApiEndpoints(handler);
|
||||||
|
api_modules.announcement.registerApiEndpoints(handler);
|
||||||
api_modules.classroom_compliance.api.registerApiEndpoints(handler);
|
api_modules.classroom_compliance.api.registerApiEndpoints(handler);
|
||||||
|
|
||||||
HttpServer server = new HttpServer(handler, config);
|
HttpServer server = new HttpServer(handler, config);
|
||||||
|
@ -35,3 +41,13 @@ void main() {
|
||||||
void optionsEndpoint(ref HttpRequestContext ctx) {
|
void optionsEndpoint(ref HttpRequestContext ctx) {
|
||||||
ctx.response.status = HttpStatus.OK;
|
ctx.response.status = HttpStatus.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void initializeSchemaEndpoint(ref HttpRequestContext ctx) {
|
||||||
|
import db : initializeSchema;
|
||||||
|
initializeSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sampleDataEndpoint(ref HttpRequestContext ctx) {
|
||||||
|
import sample_data : insertSampleData;
|
||||||
|
insertSampleData();
|
||||||
|
}
|
||||||
|
|
|
@ -4,10 +4,12 @@ import std.algorithm;
|
||||||
import std.array;
|
import std.array;
|
||||||
import std.typecons;
|
import std.typecons;
|
||||||
import std.conv;
|
import std.conv;
|
||||||
|
import std.string : split, strip;
|
||||||
|
|
||||||
import ddbc;
|
import ddbc;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
import handy_httpd.components.optional;
|
import handy_httpd.components.optional;
|
||||||
|
import handy_httpd;
|
||||||
|
|
||||||
private DataSource dataSource;
|
private DataSource dataSource;
|
||||||
|
|
||||||
|
@ -25,6 +27,37 @@ Connection getDb() {
|
||||||
return dataSource.getConnection();
|
return dataSource.getConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void initializeSchema() {
|
||||||
|
info("Initializing database schema.");
|
||||||
|
Connection conn = getDb();
|
||||||
|
scope(exit) conn.close();
|
||||||
|
Statement stmt = conn.createStatement();
|
||||||
|
scope(exit) stmt.close();
|
||||||
|
const string AUTH_SCHEMA = import("schema/auth.sql");
|
||||||
|
const string CLASSROOM_COMPLIANCE_SCHEMA = import("schema/classroom_compliance.sql");
|
||||||
|
const schemas = [AUTH_SCHEMA, CLASSROOM_COMPLIANCE_SCHEMA];
|
||||||
|
uint schemaNumber = 1;
|
||||||
|
foreach (schema; schemas) {
|
||||||
|
infoF!"Intializing schema #%d."(schemaNumber++);
|
||||||
|
auto statements = schema.split(";")
|
||||||
|
.map!(s => strip(s))
|
||||||
|
.filter!(s => s.length > 0);
|
||||||
|
uint stmtNumber = 1;
|
||||||
|
foreach (statementStr; statements) {
|
||||||
|
infoF!"Executing statement #%d."(stmtNumber++);
|
||||||
|
try {
|
||||||
|
stmt.executeUpdate(statementStr);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
error(e, "Failed to execute schema statement.");
|
||||||
|
throw new HttpStatusException(
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to initialize schema. See logs for more info."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
T[] findAll(T, Args...)(
|
T[] findAll(T, Args...)(
|
||||||
Connection conn,
|
Connection conn,
|
||||||
string query,
|
string query,
|
||||||
|
@ -111,55 +144,3 @@ void bindAllArgs(Args...)(PreparedStatement ps, Args args) {
|
||||||
else static assert(false, "Unsupported argument type: " ~ (typeof(arg).stringof));
|
else static assert(false, "Unsupported argument type: " ~ (typeof(arg).stringof));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string toSnakeCase(string camelCase) {
|
|
||||||
import std.uni;
|
|
||||||
if (camelCase.length == 0) return camelCase;
|
|
||||||
auto app = appender!string;
|
|
||||||
app ~= toLower(camelCase[0]);
|
|
||||||
for (int i = 1; i < camelCase.length; i++) {
|
|
||||||
if (isUpper(camelCase[i])) {
|
|
||||||
app ~= '_';
|
|
||||||
app ~= toLower(camelCase[i]);
|
|
||||||
} else {
|
|
||||||
app ~= camelCase[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return app[];
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
assert(toSnakeCase("testValue") == "test_value");
|
|
||||||
}
|
|
||||||
|
|
||||||
private string[] getColumnNames(T)() {
|
|
||||||
import std.string : toLower;
|
|
||||||
alias members = __traits(allMembers, T);
|
|
||||||
string[members.length] columnNames;
|
|
||||||
static foreach (i; 0 .. members.length) {
|
|
||||||
static if (__traits(getAttributes, __traits(getMember, T, members[i])).length > 0) {
|
|
||||||
columnNames[i] = toLower(__traits(getAttributes, __traits(getMember, T, members[i]))[0].name);
|
|
||||||
} else {
|
|
||||||
columnNames[i] = toLower(toSnakeCase(members[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return columnNames.dup;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string getArgsStr(T)() {
|
|
||||||
import std.traits : Fields;
|
|
||||||
alias types = Fields!T;
|
|
||||||
string argsStr = "";
|
|
||||||
static foreach (i, type; types) {
|
|
||||||
argsStr ~= "row.peek!(" ~ type.stringof ~ ")(" ~ i.to!string ~ ")";
|
|
||||||
static if (i + 1 < types.length) {
|
|
||||||
argsStr ~= ", ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return argsStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// T parseRow(T)(Row row) {
|
|
||||||
// mixin("T t = T(" ~ getArgsStr!T ~ ");");
|
|
||||||
// return t;
|
|
||||||
// }
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { RouterLink, RouterView, useRouter } from 'vue-router'
|
import { RouterLink, RouterView, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from './stores/auth'
|
import { useAuthStore } from './stores/auth'
|
||||||
import AlertDialog from './components/AlertDialog.vue'
|
import AlertDialog from './components/AlertDialog.vue'
|
||||||
|
import AnnouncementsBanner from './components/AnnouncementsBanner.vue'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -33,6 +34,8 @@ async function logOut() {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<AnnouncementsBanner />
|
||||||
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
|
|
||||||
<!-- Global dialog elements are included here below, hidden by default. -->
|
<!-- Global dialog elements are included here below, hidden by default. -->
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { APIClient, APIResponse, type AuthStoreType } from './base'
|
||||||
|
|
||||||
|
const BASE_URL = import.meta.env.VITE_API_URL + '/announcement'
|
||||||
|
|
||||||
|
export interface Announcement {
|
||||||
|
id: number
|
||||||
|
type: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AnnouncementAPIClient extends APIClient {
|
||||||
|
constructor(authStore: AuthStoreType | null) {
|
||||||
|
super(BASE_URL, authStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnnouncements(): APIResponse<Announcement[]> {
|
||||||
|
return super.get('')
|
||||||
|
}
|
||||||
|
|
||||||
|
createAnnouncement(type: string, message: string): APIResponse<Announcement> {
|
||||||
|
return super.post('', { type: type, message: message })
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAnnouncement(id: number): APIResponse<void> {
|
||||||
|
return super.delete(`/${id}`)
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,8 +47,8 @@ export type AuthStoreType = ReturnType<typeof useAuthStore>
|
||||||
|
|
||||||
export abstract class APIClient {
|
export abstract class APIClient {
|
||||||
readonly baseUrl: string
|
readonly baseUrl: string
|
||||||
authStore: AuthStoreType
|
authStore: AuthStoreType | null
|
||||||
constructor(baseUrl: string, authStore: AuthStoreType) {
|
constructor(baseUrl: string, authStore: AuthStoreType | null) {
|
||||||
this.baseUrl = baseUrl
|
this.baseUrl = baseUrl
|
||||||
this.authStore = authStore
|
this.authStore = authStore
|
||||||
}
|
}
|
||||||
|
@ -93,11 +93,14 @@ export abstract class APIClient {
|
||||||
return new APIResponse(this.handleAPIResponseWithNoBody(promise))
|
return new APIResponse(this.handleAPIResponseWithNoBody(promise))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getAuthHeaders() {
|
protected getAuthHeaders(): HeadersInit {
|
||||||
|
if (this.authStore !== null && this.authStore.state) {
|
||||||
return {
|
return {
|
||||||
Authorization: 'Basic ' + this.authStore.getBasicAuth(),
|
Authorization: 'Basic ' + this.authStore.getBasicAuth(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
protected async handleAPIResponse<T>(promise: Promise<Response>): Promise<T | APIError> {
|
protected async handleAPIResponse<T>(promise: Promise<Response>): Promise<T | APIError> {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { AnnouncementAPIClient, type Announcement } from '@/api/announcement';
|
||||||
|
import { APIError } from '@/api/base';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { onMounted, onUnmounted, ref, useTemplateRef, type Ref } from 'vue';
|
||||||
|
import ConfirmDialog from './ConfirmDialog.vue';
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL_MS = 5000
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const client = new AnnouncementAPIClient(authStore)
|
||||||
|
const announcements: Ref<Announcement[]> = ref([])
|
||||||
|
const dismissedIds: Ref<number[]> = ref([])
|
||||||
|
const refreshTimerId: Ref<number | undefined> = ref(undefined)
|
||||||
|
const deleteAnnouncementConfirmDialog = useTemplateRef('deleteAnnouncementConfirmDialog')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshAnnouncements()
|
||||||
|
refreshTimerId.value = setInterval(refreshAnnouncements, REFRESH_INTERVAL_MS)
|
||||||
|
})
|
||||||
|
onUnmounted(() => clearInterval(refreshTimerId.value))
|
||||||
|
|
||||||
|
function refreshAnnouncements() {
|
||||||
|
client.getAnnouncements().result.then(value => {
|
||||||
|
if (value instanceof APIError) {
|
||||||
|
console.warn('Failed to get announcements: ', value.message)
|
||||||
|
announcements.value = []
|
||||||
|
} else {
|
||||||
|
announcements.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleAnnouncements(): Announcement[] {
|
||||||
|
return announcements.value.filter(a => !dismissedIds.value.includes(a.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBannerClasses(a: Announcement) {
|
||||||
|
return {
|
||||||
|
'announcement-banner-info': a.type === 'INFO',
|
||||||
|
'announcement-banner-error': a.type === 'ERROR'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss(a: Announcement) {
|
||||||
|
dismissedIds.value.push(a.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAnnouncement(a: Announcement) {
|
||||||
|
const result = await deleteAnnouncementConfirmDialog.value?.show()
|
||||||
|
if (result) {
|
||||||
|
await client.deleteAnnouncement(a.id).handleErrorsWithAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="a in getVisibleAnnouncements()" :key="a.id" class="announcement-banner" :class="getBannerClasses(a)">
|
||||||
|
<p class="announcement-banner-message">{{ a.message }}</p>
|
||||||
|
<button class="announcement-banner-button" @click="dismiss(a)">Dismiss</button>
|
||||||
|
<button v-if="authStore.admin" class="announcement-banner-button" @click="deleteAnnouncement(a)">Delete</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog ref="deleteAnnouncementConfirmDialog">
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete this announcement? It will take a few
|
||||||
|
seconds for the announcement to disappear from users' screens.
|
||||||
|
</p>
|
||||||
|
</ConfirmDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.announcement-banner {
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 1rem 0 1rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-banner-message {
|
||||||
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-banner-button {
|
||||||
|
background-color: inherit;
|
||||||
|
border: none;
|
||||||
|
font-size: small;
|
||||||
|
color: rgb(207, 207, 207);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-banner-button:hover {
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-banner-info {
|
||||||
|
background-color: rgb(19, 19, 85);
|
||||||
|
border-color: rgb(70, 70, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-banner-error {
|
||||||
|
background-color: rgb(146, 14, 14);
|
||||||
|
border-color: rgb(228, 110, 106);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { showAlert } from '@/alerts';
|
||||||
|
import { AnnouncementAPIClient } from '@/api/announcement';
|
||||||
import { AuthenticationAPIClient, type User, type UserUpdatePayload } from '@/api/auth';
|
import { AuthenticationAPIClient, type User, type UserUpdatePayload } from '@/api/auth';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
@ -13,6 +15,12 @@ interface CreateUserData {
|
||||||
}
|
}
|
||||||
const createUserFormData: Ref<CreateUserData> = ref({ username: '', password: '' })
|
const createUserFormData: Ref<CreateUserData> = ref({ username: '', password: '' })
|
||||||
|
|
||||||
|
interface CreateAnnouncementFormData {
|
||||||
|
type: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
const createAnnouncementFormData: Ref<CreateAnnouncementFormData> = ref({ type: 'INFO', message: '' })
|
||||||
|
|
||||||
const users: Ref<User[]> = ref([])
|
const users: Ref<User[]> = ref([])
|
||||||
const usersPage: Ref<number> = ref(0)
|
const usersPage: Ref<number> = ref(0)
|
||||||
const usersPageSize: Ref<number> = ref(50)
|
const usersPageSize: Ref<number> = ref(50)
|
||||||
|
@ -57,6 +65,17 @@ async function doCreateUser() {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doCreateAnnouncement() {
|
||||||
|
const client = new AnnouncementAPIClient(authStore)
|
||||||
|
const a = await client.createAnnouncement(
|
||||||
|
createAnnouncementFormData.value.type, createAnnouncementFormData.value.message
|
||||||
|
).handleErrorsWithAlert()
|
||||||
|
if (a !== null) {
|
||||||
|
await showAlert('Created announcement. It will appear shortly.')
|
||||||
|
createAnnouncementFormData.value = { type: 'INFO', message: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
|
@ -122,6 +141,25 @@ async function doCreateUser() {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form @submit.prevent="doCreateAnnouncement">
|
||||||
|
<h3>Create Announcement</h3>
|
||||||
|
<div>
|
||||||
|
<label for="create-announcement-type">Type</label>
|
||||||
|
<select id="create-announcement-type" v-model="createAnnouncementFormData.type">
|
||||||
|
<option value="INFO" selected>INFO</option>
|
||||||
|
<option value="ERROR">ERROR</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="create-announcement-message">Message</label>
|
||||||
|
<textarea id="create-announcement-message" v-model="createAnnouncementFormData.message" maxlength="2000"
|
||||||
|
minlength="1" required style="min-width: 300px; min-height: 100px;"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<ConfirmDialog ref="deleteUserConfirmDialog">
|
<ConfirmDialog ref="deleteUserConfirmDialog">
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to delete this user?
|
Are you sure you want to delete this user?
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
meta {
|
||||||
|
name: Generate Sample Data
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/generate-sample-data
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
meta {
|
||||||
|
name: Initialize Schema
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/initialize-schema
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
Loading…
Reference in New Issue