Add dialog showing diff of changes to save
Add a save dialog at the bottom of the screen that is present whenever there are unsaved changes. This dialog provides a diff between the client and server state so that the user can easily confirm the changes they are about to make are the correct changes before applying them to the server.
This commit is contained in:
parent
60f898e986
commit
1d2edf7535
12 changed files with 630 additions and 7 deletions
48
components/DiffEntry.vue
Normal file
48
components/DiffEntry.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="entries.length"
|
||||
class="diffEntry"
|
||||
>
|
||||
<div class="title">
|
||||
{{ title }}:
|
||||
</div>
|
||||
<div
|
||||
v-for="[type, text], index in entries"
|
||||
:key="index"
|
||||
:class="type"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
title: string,
|
||||
entries: (["removed" | "added", string][]);
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.diffEntry {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem 1fr;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
.removed {
|
||||
grid-column: 2 / 2;
|
||||
color: color-mix(in srgb, CanvasText, red 40%);
|
||||
}
|
||||
.removed::before {
|
||||
content: "- ";
|
||||
font-family: monospace;
|
||||
}
|
||||
.added {
|
||||
grid-column: 2 / 2;
|
||||
color: color-mix(in srgb, CanvasText, green 40%);
|
||||
}
|
||||
.added::before {
|
||||
content: "+ ";
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
43
components/DiffFieldEntityId.vue
Normal file
43
components/DiffFieldEntityId.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<DiffEntry
|
||||
v-if='state !== "modified" || after !== before'
|
||||
:title
|
||||
:entries
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApiEntity } from '~/shared/types/api';
|
||||
import type { Id } from '~/shared/types/common';
|
||||
import type { ClientEntity } from '~/utils/client-entity';
|
||||
|
||||
const props = defineProps<{
|
||||
title: string,
|
||||
before: Id | undefined,
|
||||
after: Id | undefined,
|
||||
entities: ClientMap<ClientEntity<ApiEntity> & { name?: string }>,
|
||||
state: "deleted" | "created" | "modified",
|
||||
}>();
|
||||
|
||||
function getName(id?: Id) {
|
||||
if (id === undefined)
|
||||
return undefined;
|
||||
const entity = props.entities.get(id);
|
||||
if (entity?.name !== undefined)
|
||||
return entity.name;
|
||||
return String(id);
|
||||
}
|
||||
|
||||
const entries = computed(() => {
|
||||
const result: ["removed" | "added", string][] = [];
|
||||
const beforeName = getName(props.before);
|
||||
if (props.state !== "created" && beforeName) {
|
||||
result.push(["removed", beforeName]);
|
||||
}
|
||||
const afterName = getName(props.after);
|
||||
if (props.state !== "deleted" && afterName) {
|
||||
result.push(["added", afterName]);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
</script>
|
56
components/DiffFieldSetEntityId.vue
Normal file
56
components/DiffFieldSetEntityId.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<DiffEntry
|
||||
:title
|
||||
:entries
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApiEntity } from '~/shared/types/api';
|
||||
import type { Id } from '~/shared/types/common';
|
||||
import type { ClientEntity } from '~/utils/client-entity';
|
||||
|
||||
const props = defineProps<{
|
||||
title: string,
|
||||
before: Set<Id>,
|
||||
after: Set<Id>,
|
||||
entities: ClientMap<ClientEntity<ApiEntity> & { name?: string }>,
|
||||
state: "deleted" | "created" | "modified",
|
||||
}>();
|
||||
|
||||
function getName(id?: Id) {
|
||||
if (id === undefined)
|
||||
return undefined;
|
||||
const entity = props.entities.get(id);
|
||||
if (entity?.name !== undefined)
|
||||
return entity.name;
|
||||
return String(id);
|
||||
}
|
||||
|
||||
const added = computed(() => {
|
||||
if (props.state === "deleted")
|
||||
return new Set<number>();
|
||||
if (props.state === "created")
|
||||
return props.after;
|
||||
return toRaw(props.after).difference(props.before)
|
||||
});
|
||||
const removed = computed(() => {
|
||||
if (props.state === "deleted")
|
||||
return props.before;
|
||||
if (props.state === "created")
|
||||
return new Set<number>();
|
||||
return toRaw(props.before).difference(props.after);
|
||||
});
|
||||
const entries = computed((): ["added" | "removed", string][] => {
|
||||
return [
|
||||
...[...removed.value]
|
||||
.map(getName)
|
||||
.filter(name => name !== undefined)
|
||||
.map((name) => ["removed", name] as ["removed", string]),
|
||||
...[...added.value]
|
||||
.map(getName)
|
||||
.filter(name => name !== undefined)
|
||||
.map((name) => ["added", name] as ["added", string]),
|
||||
];
|
||||
});
|
||||
</script>
|
36
components/DiffFieldString.vue
Normal file
36
components/DiffFieldString.vue
Normal file
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<DiffEntry
|
||||
:title
|
||||
:entries
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
title: string,
|
||||
before: string,
|
||||
after: string,
|
||||
state: "deleted" | "created" | "modified",
|
||||
}>();
|
||||
const entries = computed((): ["added" | "removed", string][] => {
|
||||
if (props.state === "created") {
|
||||
return props.after ? [["added", props.after]] : [];
|
||||
}
|
||||
if (props.state === "deleted") {
|
||||
return props.before ? [["removed", props.before]] : [];
|
||||
}
|
||||
if (props.state === "modified") {
|
||||
if (props.after === props.before)
|
||||
return [];
|
||||
if (!props.after)
|
||||
return [["removed", props.before]];
|
||||
if (!props.before)
|
||||
return [["added", props.after]];
|
||||
return [
|
||||
["removed", props.before],
|
||||
["added", props.after],
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
</script>
|
64
components/DiffSchedule.vue
Normal file
64
components/DiffSchedule.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2>Changes</h2>
|
||||
<section v-if="locations.length">
|
||||
<h3>Locations</h3>
|
||||
<DiffScheduleLocation
|
||||
v-for="location in locations"
|
||||
:key="location.id"
|
||||
:location
|
||||
/>
|
||||
</section>
|
||||
<section v-if="events.length">
|
||||
<h3>Events</h3>
|
||||
<DiffScheduleEvent
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
:event
|
||||
:schedule
|
||||
/>
|
||||
</section>
|
||||
<section v-if="roles.length">
|
||||
<h3>Roles</h3>
|
||||
<DiffScheduleRole
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:role
|
||||
/>
|
||||
</section>
|
||||
<section v-if="shifts.length">
|
||||
<h3>Shifts</h3>
|
||||
<DiffScheduleShift
|
||||
v-for="shift in shifts"
|
||||
:key="shift.id"
|
||||
:shift
|
||||
:schedule
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
schedule: ClientSchedule
|
||||
}>();
|
||||
|
||||
const locations = computed(() => {
|
||||
return [...props.schedule.locations.values()].filter(location => location.isModified());
|
||||
});
|
||||
const events = computed(() => {
|
||||
return [...props.schedule.events.values()].filter(event => event.isModified());
|
||||
});
|
||||
const roles = computed(() => {
|
||||
return [...props.schedule.roles.values()].filter(role => role.isModified());
|
||||
});
|
||||
const shifts = computed(() => {
|
||||
return [...props.schedule.shifts.values()].filter(shift => shift.isModified());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
margin-block-start: 0.2rem;
|
||||
}
|
||||
</style>
|
95
components/DiffScheduleEvent.vue
Normal file
95
components/DiffScheduleEvent.vue
Normal file
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div>
|
||||
<h4>{{ state }} {{ event.name }}</h4>
|
||||
<DiffFieldString
|
||||
title="Name"
|
||||
:before="event.serverName"
|
||||
:after="event.name"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="Public"
|
||||
:before='event.serverCrew ? "No" : "Yes"'
|
||||
:after='event.crew ? "No" : "Yes"'
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="Description"
|
||||
:before="event.serverDescription"
|
||||
:after="event.description"
|
||||
:state
|
||||
/>
|
||||
<DiffScheduleEventSlot
|
||||
v-for="[state, slot] in slots"
|
||||
:key="slot.id"
|
||||
:slot
|
||||
:schedule
|
||||
:state
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
event: ClientScheduleEvent,
|
||||
schedule: ClientSchedule,
|
||||
}>();
|
||||
const state = computed(() => {
|
||||
if (props.event.deleted) return "deleted";
|
||||
if (props.event.isNew()) return "created";
|
||||
return "modified";
|
||||
});
|
||||
const slots = computed((): [
|
||||
"deleted" | "created" | "modified", ClientScheduleEventSlot
|
||||
][] => {
|
||||
const afterIds = props.event.slotIds;
|
||||
const beforeIds = props.event.serverSlotIds;
|
||||
if (state.value === "deleted") {
|
||||
return (
|
||||
[...beforeIds]
|
||||
.map(id => props.schedule.eventSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.map(slot => ["deleted", slot] as ["deleted", ClientScheduleEventSlot])
|
||||
);
|
||||
}
|
||||
if (state.value === "created") {
|
||||
return (
|
||||
[...afterIds]
|
||||
.map(id => props.schedule.eventSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.map(slot => ["created", slot] as ["created", ClientScheduleEventSlot])
|
||||
);
|
||||
}
|
||||
const added = [...toRaw(afterIds).difference(beforeIds)]
|
||||
.map(id => props.schedule.eventSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.filter(slot => !slot.deleted)
|
||||
.map(slot => ["created", slot] as ["created", ClientScheduleEventSlot])
|
||||
;
|
||||
const removed = [...new Set(
|
||||
[...toRaw(beforeIds).difference(afterIds)]
|
||||
.map(id => props.schedule.eventSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
).union(
|
||||
new Set(
|
||||
[...afterIds]
|
||||
.map(id => props.schedule.eventSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.filter(slot => slot.deleted)
|
||||
)
|
||||
)]
|
||||
.map(slot => ["deleted", slot] as ["deleted", ClientScheduleEventSlot])
|
||||
;
|
||||
const modified = [...toRaw(afterIds).intersection(beforeIds)]
|
||||
.map(id => props.schedule.eventSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.filter(slot => slot.isModified() && !slot.deleted)
|
||||
.map(slot => ["modified", slot] as ["modified", ClientScheduleEventSlot])
|
||||
;
|
||||
return [
|
||||
...added,
|
||||
...removed,
|
||||
...modified,
|
||||
];
|
||||
});
|
||||
</script>
|
41
components/DiffScheduleEventSlot.vue
Normal file
41
components/DiffScheduleEventSlot.vue
Normal file
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div>
|
||||
<h5>{{ state }} slot at {{ slot.start.toFormat("yyyy-LL-dd HH:mm") }}</h5>
|
||||
<DiffFieldString
|
||||
title="Start"
|
||||
:before='slot.serverStart.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:after='slot.start.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="End"
|
||||
:before='slot.serverEnd.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:after='slot.end.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:state
|
||||
/>
|
||||
<DiffFieldSetEntityId
|
||||
title="Locations"
|
||||
:before="slot.serverLocationIds"
|
||||
:after="slot.locationIds"
|
||||
:entities="schedule.locations"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldSetEntityId
|
||||
title="Assigned"
|
||||
:before="slot.serverAssigned"
|
||||
:after="slot.assigned"
|
||||
:entities="usersStore.users"
|
||||
:state
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
schedule: ClientSchedule,
|
||||
slot: ClientScheduleEventSlot,
|
||||
state: "deleted" | "created" | "modified",
|
||||
}>();
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
</script>
|
28
components/DiffScheduleLocation.vue
Normal file
28
components/DiffScheduleLocation.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<div>
|
||||
<h4>{{ state }} {{ location.name }}</h4>
|
||||
<DiffFieldString
|
||||
title="Name"
|
||||
:before="location.serverName"
|
||||
:after="location.name"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="Description"
|
||||
:before="location.serverDescription"
|
||||
:after="location.description"
|
||||
:state
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
location: ClientScheduleLocation,
|
||||
}>();
|
||||
const state = computed(() => {
|
||||
if (props.location.deleted) return "deleted";
|
||||
if (props.location.isNew()) return "created";
|
||||
return "modified";
|
||||
});
|
||||
</script>
|
28
components/DiffScheduleRole.vue
Normal file
28
components/DiffScheduleRole.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<div>
|
||||
<h4>{{ state }} {{ role.name }}</h4>
|
||||
<DiffFieldString
|
||||
title="Name"
|
||||
:before="role.serverName"
|
||||
:after="role.name"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="Description"
|
||||
:before="role.serverDescription"
|
||||
:after="role.description"
|
||||
:state
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
role: ClientScheduleRole,
|
||||
}>();
|
||||
const state = computed(() => {
|
||||
if (props.role.deleted) return "deleted";
|
||||
if (props.role.isNew()) return "created";
|
||||
return "modified";
|
||||
});
|
||||
</script>
|
96
components/DiffScheduleShift.vue
Normal file
96
components/DiffScheduleShift.vue
Normal file
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div>
|
||||
<h4>{{ state }} {{ shift.name }}</h4>
|
||||
<DiffFieldString
|
||||
title="Name"
|
||||
:before="shift.serverName"
|
||||
:after="shift.name"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldEntityId
|
||||
title="Role"
|
||||
:before="shift.serverRoleId"
|
||||
:after="shift.roleId"
|
||||
:entities="schedule.roles"
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="Description"
|
||||
:before="shift.serverDescription"
|
||||
:after="shift.description"
|
||||
:state
|
||||
/>
|
||||
<DiffScheduleShiftSlot
|
||||
v-for="[state, slot] in slots"
|
||||
:key="slot.id"
|
||||
:slot
|
||||
:schedule
|
||||
:state
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
shift: ClientScheduleShift,
|
||||
schedule: ClientSchedule,
|
||||
}>();
|
||||
const state = computed(() => {
|
||||
if (props.shift.deleted) return "deleted";
|
||||
if (props.shift.isNew()) return "created";
|
||||
return "modified";
|
||||
});
|
||||
const slots = computed((): [
|
||||
"deleted" | "created" | "modified", ClientScheduleShiftSlot
|
||||
][] => {
|
||||
const afterIds = props.shift.slotIds;
|
||||
const beforeIds = props.shift.serverSlotIds;
|
||||
if (state.value === "deleted") {
|
||||
return (
|
||||
[...beforeIds]
|
||||
.map(id => props.schedule.shiftSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.map(slot => ["deleted", slot] as ["deleted", ClientScheduleShiftSlot])
|
||||
);
|
||||
}
|
||||
if (state.value === "created") {
|
||||
return (
|
||||
[...afterIds]
|
||||
.map(id => props.schedule.shiftSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.map(slot => ["created", slot] as ["created", ClientScheduleShiftSlot])
|
||||
);
|
||||
}
|
||||
const added = [...toRaw(afterIds).difference(beforeIds)]
|
||||
.map(id => props.schedule.shiftSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.filter(slot => !slot.deleted)
|
||||
.map(slot => ["created", slot] as ["created", ClientScheduleShiftSlot])
|
||||
;
|
||||
const removed = [...new Set(
|
||||
[...toRaw(beforeIds).difference(afterIds)]
|
||||
.map(id => props.schedule.shiftSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
).union(
|
||||
new Set(
|
||||
[...afterIds]
|
||||
.map(id => props.schedule.shiftSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.filter(slot => slot.deleted)
|
||||
)
|
||||
)]
|
||||
.map(slot => ["deleted", slot] as ["deleted", ClientScheduleShiftSlot])
|
||||
;
|
||||
const modified = [...toRaw(afterIds).intersection(beforeIds)]
|
||||
.map(id => props.schedule.shiftSlots.get(id))
|
||||
.filter(slot => slot !== undefined)
|
||||
.filter(slot => slot.isModified() && !slot.deleted)
|
||||
.map(slot => ["modified", slot] as ["modified", ClientScheduleShiftSlot])
|
||||
;
|
||||
return [
|
||||
...added,
|
||||
...removed,
|
||||
...modified,
|
||||
];
|
||||
});
|
||||
</script>
|
34
components/DiffScheduleShiftSlot.vue
Normal file
34
components/DiffScheduleShiftSlot.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<div>
|
||||
<h5>{{ state }} slot at {{ slot.start.toFormat("yyyy-LL-dd HH:mm") }}</h5>
|
||||
<DiffFieldString
|
||||
title="Start"
|
||||
:before='slot.serverStart.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:after='slot.start.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:state
|
||||
/>
|
||||
<DiffFieldString
|
||||
title="End"
|
||||
:before='slot.serverEnd.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:after='slot.end.toFormat("yyyy-LL-dd HH:mm")'
|
||||
:state
|
||||
/>
|
||||
<DiffFieldSetEntityId
|
||||
title="Assigned"
|
||||
:before="slot.serverAssigned"
|
||||
:after="slot.assigned"
|
||||
:entities="usersStore.users"
|
||||
:state
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
schedule: ClientSchedule,
|
||||
slot: ClientScheduleShiftSlot,
|
||||
state: "deleted" | "created" | "modified",
|
||||
}>();
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
</script>
|
|
@ -78,13 +78,31 @@
|
|||
<TableScheduleShiftSlots :edit="true" :roleId="roleFilter" :shiftSlotFilter />
|
||||
</template>
|
||||
</Tabs>
|
||||
<p v-if="schedule.isModified()">
|
||||
Changes are not saved yet.
|
||||
<button
|
||||
type="button"
|
||||
@click="saveChanges"
|
||||
>Save Changes</button>
|
||||
</p>
|
||||
<section
|
||||
class="saveDialog"
|
||||
:class="{ visible: schedule.isModified() }"
|
||||
>
|
||||
<DiffSchedule
|
||||
v-if="reviewOpen"
|
||||
:schedule
|
||||
/>
|
||||
<hr v-if="reviewOpen">
|
||||
<section class="saveLine">
|
||||
<span>Changes are not saved yet.</span>
|
||||
<button
|
||||
type="button"
|
||||
v-if="reviewOpen"
|
||||
@click="saveChanges"
|
||||
>Save changes</button>
|
||||
<button
|
||||
type="button"
|
||||
class="review"
|
||||
@click="reviewOpen = !reviewOpen"
|
||||
>
|
||||
{{ reviewOpen ? "Close" : "Review" }}
|
||||
</button>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
@ -156,6 +174,8 @@ const roleFilter = computed({
|
|||
}),
|
||||
});
|
||||
|
||||
const reviewOpen = ref(false);
|
||||
|
||||
async function saveChanges() {
|
||||
try {
|
||||
await $fetch("/api/schedule", {
|
||||
|
@ -168,3 +188,37 @@ async function saveChanges() {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.saveDialog {
|
||||
visibility: hidden;
|
||||
max-height: calc(100dvh - 2rem);
|
||||
overflow-y: auto;
|
||||
max-width: 30rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border: 1px solid CanvasText;
|
||||
background-color: color-mix(in srgb, Canvas, CanvasText 10%);
|
||||
margin-block-start: 1rem;
|
||||
margin-block-end: 0.5rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
.saveDialog.visible {
|
||||
visibility: visible;
|
||||
position: sticky;
|
||||
bottom: 0.5rem;
|
||||
}
|
||||
.saveDialog hr {
|
||||
margin-block-start: 0.4rem;
|
||||
margin-block-end: 0.2rem;
|
||||
}
|
||||
.saveLine {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.saveLine span {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
button.review {
|
||||
width: 4.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue